Skip to content
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
09950e7
feat: add branching path based on feature flag
cspath1 Nov 25, 2025
18d581d
refactor: update toolbar state to be context instead of hook
cspath1 Nov 25, 2025
3b6440a
feat: scaffold out new layout
cspath1 Nov 25, 2025
39d95b8
feat: click to drag + collapse toolbar
cspath1 Nov 25, 2025
62bee8e
feat: add feature flag for the interactive tab
cspath1 Nov 25, 2025
bd61df4
refactor: remove unused icon
cspath1 Nov 25, 2025
a435286
feat: add FilterTuneIcon
cspath1 Nov 25, 2025
2de9199
refactor: move things around
cspath1 Nov 25, 2025
e2c039a
feat: set active tab for new UI
cspath1 Nov 25, 2025
c1b71a1
feat: scaffold out dynamic content renderer
cspath1 Nov 25, 2025
a68abb4
feat: a whole bunch more refactoring
cspath1 Nov 25, 2025
3994ddd
feat: even more refactoring
cspath1 Nov 25, 2025
a280d12
feat: clean up new Settings tab
cspath1 Nov 25, 2025
5aac8b5
feat: work on Flag item styling
cspath1 Nov 25, 2025
f546a4b
refactor: clean up placeholders
cspath1 Nov 25, 2025
5ed377d
refactor: remove placeholder Toggle component
cspath1 Nov 25, 2025
b9831bf
feat: scaffold out rough new Events/Monitoring tab
cspath1 Nov 25, 2025
d484b6c
feat: Add EventsContent
cspath1 Nov 25, 2025
9d69ec1
refactor: remove extra dummy data
cspath1 Nov 25, 2025
3678b0a
refactor: clean up
cspath1 Nov 25, 2025
8853739
fix: fix bug
cspath1 Nov 25, 2025
e6a3829
fix: set default tab if missing
cspath1 Nov 25, 2025
edf9620
fix: fix failing e2e tests
cspath1 Nov 25, 2025
f0dea3e
test: add tests
cspath1 Nov 25, 2025
53e2eb6
Merge branch 'main' into cspath/REL-10965-toolbar-design-revamp
cspath1 Nov 25, 2025
a225bcc
Merge branch 'cspath/REL-10965-tests' into cspath/REL-10965-toolbar-d…
cspath1 Nov 25, 2025
2efefdd
feat: show all icons, disable based on feature flag
cspath1 Nov 25, 2025
350d9d5
chore: run pnpm format
cspath1 Nov 25, 2025
7bfe76f
feat: add new dropdown for sub-tab selection
cspath1 Nov 25, 2025
59b6b88
chore: run pnpm format
cspath1 Nov 25, 2025
5ca65e0
fix: remove unused import
cspath1 Nov 25, 2025
09ab5d4
Merge branch 'main' into cspath/REL-10965-toolbar-design-revamp
cspath1 Nov 26, 2025
5881a30
refactor: restructure project (#361)
cspath1 Nov 26, 2025
0982d8e
refactor: minor updates
cspath1 Nov 26, 2025
d341e8d
feat: wire up search functionality
cspath1 Nov 26, 2025
326184c
test: add tests for TabSearchProvider
cspath1 Nov 26, 2025
1ddb128
test: fix tests
cspath1 Nov 26, 2025
969d7c1
Merge branch 'cspath/REL-10965-toolbar-design-revamp' into cspath/REL…
cspath1 Nov 26, 2025
215a2ce
feat: filter events based on search term with new UI
cspath1 Nov 26, 2025
cd7474b
feat: setting filtering + tests
cspath1 Nov 26, 2025
06e4f7a
feat: pull flags dynamically for new ui
cspath1 Nov 26, 2025
e54b880
feat: get closer to matching parity for feature flag list
cspath1 Nov 26, 2025
d5e252f
chore: run pnpm format
cspath1 Nov 26, 2025
18b56c4
feat: almost get things working
cspath1 Nov 26, 2025
1634908
feat: get height working for JSON
cspath1 Nov 26, 2025
4e589c4
feat: add override indicator next to flag name
cspath1 Nov 26, 2025
25344db
feat: more styling updates
cspath1 Nov 26, 2025
c8d974d
feat: pretty much matching parity now (sdk mode)
cspath1 Nov 26, 2025
6a4fdd0
fix: fix dev server mode
cspath1 Nov 26, 2025
c7fee76
test: add tests
cspath1 Nov 26, 2025
ac4fe53
refactor: clean up
cspath1 Nov 26, 2025
b241a19
refactor: clean up
cspath1 Nov 26, 2025
0a95515
refactor: consistent components
cspath1 Nov 26, 2025
9e3e6d1
Merge branch 'main' into cspath/REL-10965-part-two
cspath1 Dec 2, 2025
52fb2e9
fix: fix failing tests
cspath1 Dec 2, 2025
b79716a
refactor: address some PR feedback
cspath1 Dec 2, 2025
b8f97f5
fix: fix issue
cspath1 Dec 2, 2025
bd54f4e
fix: actually fix this time
cspath1 Dec 2, 2025
70d8e6b
refactor: update border colors
cspath1 Dec 2, 2025
a0087e3
refactor: big refactor of context definitions
cspath1 Dec 2, 2025
d878306
refactor: final clean up
cspath1 Dec 3, 2025
35ddf95
feat: create dynamic filter overlay
cspath1 Dec 3, 2025
446d444
test: add tests
cspath1 Dec 3, 2025
d3f0e4e
feat: wire up filter functionality
cspath1 Dec 3, 2025
93345fe
feat: add starred functionality to new design
cspath1 Dec 3, 2025
e2e019c
fix: fix filter overlay stutter
cspath1 Dec 3, 2025
7d49156
refactor: clean up
cspath1 Dec 3, 2025
f7d5aad
test: fix test errors
cspath1 Dec 3, 2025
7236346
refactor: providers throw errors if used improperly
cspath1 Dec 3, 2025
31ac806
Merge branch 'cspath/REL-10965-part-two' into cspath/REL-10965-part-t…
cspath1 Dec 3, 2025
ac98da9
feat: add EnvironmentSelector to settings tab
cspath1 Dec 3, 2025
719adcc
feat: add linking to flag in LD
cspath1 Dec 3, 2025
03bb64d
refactor: slight clean up
cspath1 Dec 3, 2025
7e6c7da
Merge branch 'main' into cspath/REL-10965-part-three
cspath1 Dec 3, 2025
2356743
refactor: standardize list item styling
cspath1 Dec 3, 2025
6714ea5
feat: add sync button for dev-server mode
cspath1 Dec 4, 2025
42a459d
chore: run pnpm format
cspath1 Dec 4, 2025
81451a0
Merge branch 'main' into cspath/REL-10965-part-three
cspath1 Dec 4, 2025
7839386
fix: pass clientSideId into environment
cspath1 Dec 4, 2025
f2f4cdd
refactor: styling clean up
cspath1 Dec 4, 2025
4186b69
Merge branch 'main' into cspath/REL-10965-part-three
cspath1 Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 376 additions & 0 deletions packages/toolbar/src/core/tests/EnvironmentProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@
import { render, screen, waitFor, act } from '@testing-library/react';
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
import { EnvironmentProvider, useEnvironmentContext } from '../ui/Toolbar/context/api/EnvironmentProvider';
import '@testing-library/jest-dom/vitest';
import React from 'react';

// Mock the ProjectProvider
vi.mock('../ui/Toolbar/context/api/ProjectProvider', () => ({
useProjectContext: vi.fn(),
}));

import { useProjectContext } from '../ui/Toolbar/context/api/ProjectProvider';

// Test component that uses the Environment context
function TestConsumer() {
const { environment } = useEnvironmentContext();

return (
<div>
<div data-testid="environment">{environment || 'none'}</div>
</div>
);
}

// Test component with environment setter
function TestConsumerWithSetter() {
const { environment, setEnvironment } = useEnvironmentContext();

return (
<div>
<div data-testid="environment">{environment || 'none'}</div>
<button data-testid="set-staging" onClick={() => setEnvironment('staging')}>
Set Staging
</button>
<button data-testid="set-production" onClick={() => setEnvironment('production')}>
Set Production
</button>
</div>
);
}

describe('EnvironmentProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();

// Default mock - no environments
(useProjectContext as any).mockReturnValue({
environments: [],
});
});

afterEach(() => {
localStorage.clear();
});

describe('Environment Auto-Detection - First Time User', () => {
test('defaults to production when no environments available and no saved value', async () => {
// GIVEN: New developer with no saved environment and no environments from project
(useProjectContext as any).mockReturnValue({
environments: [],
});

// WHEN: Provider initializes
render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Defaults to production
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});
});

test('auto-selects first available environment when no environment is saved', async () => {
// GIVEN: New developer using toolbar for first time (no saved environment)
(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'env-1', key: 'staging', name: 'Staging' },
{ _id: 'env-2', key: 'production', name: 'Production' },
],
});

// WHEN: They open the toolbar
render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: System auto-selects the first environment
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
});

// AND: Saves it to localStorage for next time
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');
});

test('selects environment matching provided clientSideId', async () => {
// GIVEN: Developer initializes toolbar with a specific clientSideId
const clientSideId = 'sdk-key-production';

(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'sdk-key-staging', key: 'staging', name: 'Staging' },
{ _id: clientSideId, key: 'production', name: 'Production' },
],
});

// WHEN: Toolbar initializes with clientSideId
render(
<EnvironmentProvider clientSideId={clientSideId}>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Correct environment is auto-selected based on SDK key
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});
});

test('falls back to first environment when clientSideId does not match', async () => {
// GIVEN: Developer provides a clientSideId that doesn't match any environment
(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'env-1', key: 'staging', name: 'Staging' },
{ _id: 'env-2', key: 'production', name: 'Production' },
],
});

// WHEN: Toolbar initializes with non-matching clientSideId
render(
<EnvironmentProvider clientSideId="non-existent-sdk-key">
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Falls back to first available environment
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
});
});
});

describe('Environment Persistence - Returning User', () => {
test('restores previously selected environment from localStorage', async () => {
// GIVEN: Developer has used toolbar before and selected an environment
localStorage.setItem('ld-toolbar-environment', 'qa');

(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'env-1', key: 'production', name: 'Production' },
{ _id: 'env-2', key: 'qa', name: 'QA' },
],
});

// WHEN: They open toolbar again
render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Their previous environment selection is restored
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('qa');
});
});

test('localStorage takes precedence over clientSideId', async () => {
// GIVEN: localStorage has one environment, but clientSideId points to another
localStorage.setItem('ld-toolbar-environment', 'staging');

(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'sdk-key-production', key: 'production', name: 'Production' },
{ _id: 'sdk-key-staging', key: 'staging', name: 'Staging' },
],
});

// WHEN: Developer opens toolbar with a clientSideId
render(
<EnvironmentProvider clientSideId="sdk-key-production">
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Saved environment from localStorage is used (not the one from clientSideId)
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
});
});
});

describe('Environment Selection Workflow', () => {
test('allows changing environment after initial selection', async () => {
// GIVEN: User has an environment selected
(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'env-1', key: 'production', name: 'Production' },
{ _id: 'env-2', key: 'staging', name: 'Staging' },
],
});

render(
<EnvironmentProvider>
<TestConsumerWithSetter />
</EnvironmentProvider>,
);

// Initial environment auto-selected (first one)
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});

// WHEN: User switches to different environment
const switchButton = screen.getByTestId('set-staging');
act(() => {
switchButton.click();
});

// THEN: Environment updates
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
});

// AND: Change is persisted to localStorage
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');
});

test('persists environment changes to localStorage', async () => {
// GIVEN: User is using the toolbar
(useProjectContext as any).mockReturnValue({
environments: [{ _id: 'env-1', key: 'production', name: 'Production' }],
});

render(
<EnvironmentProvider>
<TestConsumerWithSetter />
</EnvironmentProvider>,
);

await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});

// WHEN: User changes environment multiple times
const stagingButton = screen.getByTestId('set-staging');
act(() => {
stagingButton.click();
});

// THEN: Each change is persisted
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');

const productionButton = screen.getByTestId('set-production');
act(() => {
productionButton.click();
});

expect(localStorage.getItem('ld-toolbar-environment')).toBe('production');
});
});

describe('Context Hook - useEnvironmentContext', () => {
test('throws error when used without EnvironmentProvider', () => {
// GIVEN: Component uses context without provider
const TestDefault = () => {
const { environment } = useEnvironmentContext();
return <div data-testid="environment">{environment}</div>;
};

// WHEN: Rendered without provider
// THEN: Should throw an error
expect(() => {
render(<TestDefault />);
}).toThrow('useEnvironmentContext must be used within an EnvironmentProvider');
});
});

describe('Edge Cases', () => {
test('handles empty environments array gracefully', async () => {
// GIVEN: Project has no environments configured
(useProjectContext as any).mockReturnValue({
environments: [],
});

// WHEN: Provider initializes
render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Defaults to production without crashing
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});
});

test('handles environments being updated after initial render', async () => {
// GIVEN: Initially no environments
const mockUseProjectContext = useProjectContext as any;
mockUseProjectContext.mockReturnValue({
environments: [],
});

const { rerender } = render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// Initially defaults to production
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('production');
});

// WHEN: Environments become available
mockUseProjectContext.mockReturnValue({
environments: [
{ _id: 'env-1', key: 'staging', name: 'Staging' },
{ _id: 'env-2', key: 'production', name: 'Production' },
],
});

rerender(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

// THEN: Environment updates to first available
await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
});
});

test('does not overwrite localStorage when restoring saved value', async () => {
// GIVEN: A saved environment in localStorage
localStorage.setItem('ld-toolbar-environment', 'qa');

(useProjectContext as any).mockReturnValue({
environments: [
{ _id: 'env-1', key: 'production', name: 'Production' },
{ _id: 'env-2', key: 'qa', name: 'QA' },
],
});

// Spy on localStorage.setItem
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');

// WHEN: Provider initializes
render(
<EnvironmentProvider>
<TestConsumer />
</EnvironmentProvider>,
);

await waitFor(() => {
expect(screen.getByTestId('environment')).toHaveTextContent('qa');
});

// THEN: localStorage.setItem was not called (value was restored, not set)
// Note: setItem might be called 0 times if restoring from localStorage
const environmentSetCalls = setItemSpy.mock.calls.filter((call) => call[0] === 'ld-toolbar-environment');
expect(environmentSetCalls.length).toBe(0);

setItemSpy.mockRestore();
});
});
});
Loading
Loading