Skip to content

Commit ede229c

Browse files
authored
chore: [REL-10965] Finish porting over new design functionality (#379)
* feat: add branching path based on feature flag * refactor: update toolbar state to be context instead of hook * feat: scaffold out new layout * feat: click to drag + collapse toolbar * feat: add feature flag for the interactive tab * refactor: remove unused icon * feat: add FilterTuneIcon * refactor: move things around * feat: set active tab for new UI * feat: scaffold out dynamic content renderer * feat: a whole bunch more refactoring * feat: even more refactoring * feat: clean up new Settings tab * feat: work on Flag item styling * refactor: clean up placeholders * refactor: remove placeholder Toggle component * feat: scaffold out rough new Events/Monitoring tab * feat: Add EventsContent * refactor: remove extra dummy data * refactor: clean up * fix: fix bug * fix: set default tab if missing * fix: fix failing e2e tests * test: add tests * feat: show all icons, disable based on feature flag * chore: run pnpm format * feat: add new dropdown for sub-tab selection * chore: run pnpm format * fix: remove unused import * refactor: restructure project (#361) * refactor: minor updates * feat: wire up search functionality * test: add tests for TabSearchProvider * test: fix tests * feat: filter events based on search term with new UI * feat: setting filtering + tests * feat: pull flags dynamically for new ui * feat: get closer to matching parity for feature flag list * chore: run pnpm format * feat: almost get things working * feat: get height working for JSON * feat: add override indicator next to flag name * feat: more styling updates * feat: pretty much matching parity now (sdk mode) * fix: fix dev server mode * test: add tests * refactor: clean up * refactor: clean up * refactor: consistent components * fix: fix failing tests * refactor: address some PR feedback * fix: fix issue * fix: actually fix this time * refactor: update border colors * refactor: big refactor of context definitions * refactor: final clean up * feat: create dynamic filter overlay * test: add tests * feat: wire up filter functionality * feat: add starred functionality to new design * fix: fix filter overlay stutter * refactor: clean up * test: fix test errors * refactor: providers throw errors if used improperly * feat: add EnvironmentSelector to settings tab * feat: add linking to flag in LD * refactor: slight clean up * refactor: standardize list item styling * feat: add sync button for dev-server mode * chore: run pnpm format * fix: pass clientSideId into environment * refactor: styling clean up
1 parent e23cc81 commit ede229c

36 files changed

+2198
-164
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import { render, screen, waitFor, act } from '@testing-library/react';
2+
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
3+
import { EnvironmentProvider, useEnvironmentContext } from '../ui/Toolbar/context/api/EnvironmentProvider';
4+
import '@testing-library/jest-dom/vitest';
5+
import React from 'react';
6+
7+
// Mock the ProjectProvider
8+
vi.mock('../ui/Toolbar/context/api/ProjectProvider', () => ({
9+
useProjectContext: vi.fn(),
10+
}));
11+
12+
import { useProjectContext } from '../ui/Toolbar/context/api/ProjectProvider';
13+
14+
// Test component that uses the Environment context
15+
function TestConsumer() {
16+
const { environment } = useEnvironmentContext();
17+
18+
return (
19+
<div>
20+
<div data-testid="environment">{environment || 'none'}</div>
21+
</div>
22+
);
23+
}
24+
25+
// Test component with environment setter
26+
function TestConsumerWithSetter() {
27+
const { environment, setEnvironment } = useEnvironmentContext();
28+
29+
return (
30+
<div>
31+
<div data-testid="environment">{environment || 'none'}</div>
32+
<button data-testid="set-staging" onClick={() => setEnvironment('staging')}>
33+
Set Staging
34+
</button>
35+
<button data-testid="set-production" onClick={() => setEnvironment('production')}>
36+
Set Production
37+
</button>
38+
</div>
39+
);
40+
}
41+
42+
describe('EnvironmentProvider', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks();
45+
localStorage.clear();
46+
47+
// Default mock - no environments
48+
(useProjectContext as any).mockReturnValue({
49+
environments: [],
50+
});
51+
});
52+
53+
afterEach(() => {
54+
localStorage.clear();
55+
});
56+
57+
describe('Environment Auto-Detection - First Time User', () => {
58+
test('defaults to production when no environments available and no saved value', async () => {
59+
// GIVEN: New developer with no saved environment and no environments from project
60+
(useProjectContext as any).mockReturnValue({
61+
environments: [],
62+
});
63+
64+
// WHEN: Provider initializes
65+
render(
66+
<EnvironmentProvider>
67+
<TestConsumer />
68+
</EnvironmentProvider>,
69+
);
70+
71+
// THEN: Defaults to production
72+
await waitFor(() => {
73+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
74+
});
75+
});
76+
77+
test('auto-selects first available environment when no environment is saved', async () => {
78+
// GIVEN: New developer using toolbar for first time (no saved environment)
79+
(useProjectContext as any).mockReturnValue({
80+
environments: [
81+
{ _id: 'env-1', key: 'staging', name: 'Staging' },
82+
{ _id: 'env-2', key: 'production', name: 'Production' },
83+
],
84+
});
85+
86+
// WHEN: They open the toolbar
87+
render(
88+
<EnvironmentProvider>
89+
<TestConsumer />
90+
</EnvironmentProvider>,
91+
);
92+
93+
// THEN: System auto-selects the first environment
94+
await waitFor(() => {
95+
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
96+
});
97+
98+
// AND: Saves it to localStorage for next time
99+
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');
100+
});
101+
102+
test('selects environment matching provided clientSideId', async () => {
103+
// GIVEN: Developer initializes toolbar with a specific clientSideId
104+
const clientSideId = 'sdk-key-production';
105+
106+
(useProjectContext as any).mockReturnValue({
107+
environments: [
108+
{ _id: 'sdk-key-staging', key: 'staging', name: 'Staging' },
109+
{ _id: clientSideId, key: 'production', name: 'Production' },
110+
],
111+
});
112+
113+
// WHEN: Toolbar initializes with clientSideId
114+
render(
115+
<EnvironmentProvider clientSideId={clientSideId}>
116+
<TestConsumer />
117+
</EnvironmentProvider>,
118+
);
119+
120+
// THEN: Correct environment is auto-selected based on SDK key
121+
await waitFor(() => {
122+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
123+
});
124+
});
125+
126+
test('falls back to first environment when clientSideId does not match', async () => {
127+
// GIVEN: Developer provides a clientSideId that doesn't match any environment
128+
(useProjectContext as any).mockReturnValue({
129+
environments: [
130+
{ _id: 'env-1', key: 'staging', name: 'Staging' },
131+
{ _id: 'env-2', key: 'production', name: 'Production' },
132+
],
133+
});
134+
135+
// WHEN: Toolbar initializes with non-matching clientSideId
136+
render(
137+
<EnvironmentProvider clientSideId="non-existent-sdk-key">
138+
<TestConsumer />
139+
</EnvironmentProvider>,
140+
);
141+
142+
// THEN: Falls back to first available environment
143+
await waitFor(() => {
144+
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
145+
});
146+
});
147+
});
148+
149+
describe('Environment Persistence - Returning User', () => {
150+
test('restores previously selected environment from localStorage', async () => {
151+
// GIVEN: Developer has used toolbar before and selected an environment
152+
localStorage.setItem('ld-toolbar-environment', 'qa');
153+
154+
(useProjectContext as any).mockReturnValue({
155+
environments: [
156+
{ _id: 'env-1', key: 'production', name: 'Production' },
157+
{ _id: 'env-2', key: 'qa', name: 'QA' },
158+
],
159+
});
160+
161+
// WHEN: They open toolbar again
162+
render(
163+
<EnvironmentProvider>
164+
<TestConsumer />
165+
</EnvironmentProvider>,
166+
);
167+
168+
// THEN: Their previous environment selection is restored
169+
await waitFor(() => {
170+
expect(screen.getByTestId('environment')).toHaveTextContent('qa');
171+
});
172+
});
173+
174+
test('localStorage takes precedence over clientSideId', async () => {
175+
// GIVEN: localStorage has one environment, but clientSideId points to another
176+
localStorage.setItem('ld-toolbar-environment', 'staging');
177+
178+
(useProjectContext as any).mockReturnValue({
179+
environments: [
180+
{ _id: 'sdk-key-production', key: 'production', name: 'Production' },
181+
{ _id: 'sdk-key-staging', key: 'staging', name: 'Staging' },
182+
],
183+
});
184+
185+
// WHEN: Developer opens toolbar with a clientSideId
186+
render(
187+
<EnvironmentProvider clientSideId="sdk-key-production">
188+
<TestConsumer />
189+
</EnvironmentProvider>,
190+
);
191+
192+
// THEN: Saved environment from localStorage is used (not the one from clientSideId)
193+
await waitFor(() => {
194+
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
195+
});
196+
});
197+
});
198+
199+
describe('Environment Selection Workflow', () => {
200+
test('allows changing environment after initial selection', async () => {
201+
// GIVEN: User has an environment selected
202+
(useProjectContext as any).mockReturnValue({
203+
environments: [
204+
{ _id: 'env-1', key: 'production', name: 'Production' },
205+
{ _id: 'env-2', key: 'staging', name: 'Staging' },
206+
],
207+
});
208+
209+
render(
210+
<EnvironmentProvider>
211+
<TestConsumerWithSetter />
212+
</EnvironmentProvider>,
213+
);
214+
215+
// Initial environment auto-selected (first one)
216+
await waitFor(() => {
217+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
218+
});
219+
220+
// WHEN: User switches to different environment
221+
const switchButton = screen.getByTestId('set-staging');
222+
act(() => {
223+
switchButton.click();
224+
});
225+
226+
// THEN: Environment updates
227+
await waitFor(() => {
228+
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
229+
});
230+
231+
// AND: Change is persisted to localStorage
232+
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');
233+
});
234+
235+
test('persists environment changes to localStorage', async () => {
236+
// GIVEN: User is using the toolbar
237+
(useProjectContext as any).mockReturnValue({
238+
environments: [{ _id: 'env-1', key: 'production', name: 'Production' }],
239+
});
240+
241+
render(
242+
<EnvironmentProvider>
243+
<TestConsumerWithSetter />
244+
</EnvironmentProvider>,
245+
);
246+
247+
await waitFor(() => {
248+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
249+
});
250+
251+
// WHEN: User changes environment multiple times
252+
const stagingButton = screen.getByTestId('set-staging');
253+
act(() => {
254+
stagingButton.click();
255+
});
256+
257+
// THEN: Each change is persisted
258+
expect(localStorage.getItem('ld-toolbar-environment')).toBe('staging');
259+
260+
const productionButton = screen.getByTestId('set-production');
261+
act(() => {
262+
productionButton.click();
263+
});
264+
265+
expect(localStorage.getItem('ld-toolbar-environment')).toBe('production');
266+
});
267+
});
268+
269+
describe('Context Hook - useEnvironmentContext', () => {
270+
test('throws error when used without EnvironmentProvider', () => {
271+
// GIVEN: Component uses context without provider
272+
const TestDefault = () => {
273+
const { environment } = useEnvironmentContext();
274+
return <div data-testid="environment">{environment}</div>;
275+
};
276+
277+
// WHEN: Rendered without provider
278+
// THEN: Should throw an error
279+
expect(() => {
280+
render(<TestDefault />);
281+
}).toThrow('useEnvironmentContext must be used within an EnvironmentProvider');
282+
});
283+
});
284+
285+
describe('Edge Cases', () => {
286+
test('handles empty environments array gracefully', async () => {
287+
// GIVEN: Project has no environments configured
288+
(useProjectContext as any).mockReturnValue({
289+
environments: [],
290+
});
291+
292+
// WHEN: Provider initializes
293+
render(
294+
<EnvironmentProvider>
295+
<TestConsumer />
296+
</EnvironmentProvider>,
297+
);
298+
299+
// THEN: Defaults to production without crashing
300+
await waitFor(() => {
301+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
302+
});
303+
});
304+
305+
test('handles environments being updated after initial render', async () => {
306+
// GIVEN: Initially no environments
307+
const mockUseProjectContext = useProjectContext as any;
308+
mockUseProjectContext.mockReturnValue({
309+
environments: [],
310+
});
311+
312+
const { rerender } = render(
313+
<EnvironmentProvider>
314+
<TestConsumer />
315+
</EnvironmentProvider>,
316+
);
317+
318+
// Initially defaults to production
319+
await waitFor(() => {
320+
expect(screen.getByTestId('environment')).toHaveTextContent('production');
321+
});
322+
323+
// WHEN: Environments become available
324+
mockUseProjectContext.mockReturnValue({
325+
environments: [
326+
{ _id: 'env-1', key: 'staging', name: 'Staging' },
327+
{ _id: 'env-2', key: 'production', name: 'Production' },
328+
],
329+
});
330+
331+
rerender(
332+
<EnvironmentProvider>
333+
<TestConsumer />
334+
</EnvironmentProvider>,
335+
);
336+
337+
// THEN: Environment updates to first available
338+
await waitFor(() => {
339+
expect(screen.getByTestId('environment')).toHaveTextContent('staging');
340+
});
341+
});
342+
343+
test('does not overwrite localStorage when restoring saved value', async () => {
344+
// GIVEN: A saved environment in localStorage
345+
localStorage.setItem('ld-toolbar-environment', 'qa');
346+
347+
(useProjectContext as any).mockReturnValue({
348+
environments: [
349+
{ _id: 'env-1', key: 'production', name: 'Production' },
350+
{ _id: 'env-2', key: 'qa', name: 'QA' },
351+
],
352+
});
353+
354+
// Spy on localStorage.setItem
355+
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
356+
357+
// WHEN: Provider initializes
358+
render(
359+
<EnvironmentProvider>
360+
<TestConsumer />
361+
</EnvironmentProvider>,
362+
);
363+
364+
await waitFor(() => {
365+
expect(screen.getByTestId('environment')).toHaveTextContent('qa');
366+
});
367+
368+
// THEN: localStorage.setItem was not called (value was restored, not set)
369+
// Note: setItem might be called 0 times if restoring from localStorage
370+
const environmentSetCalls = setItemSpy.mock.calls.filter((call) => call[0] === 'ld-toolbar-environment');
371+
expect(environmentSetCalls.length).toBe(0);
372+
373+
setItemSpy.mockRestore();
374+
});
375+
});
376+
});

0 commit comments

Comments
 (0)