Skip to content

Commit 263ea04

Browse files
test: add unit tests for FilterTabs and StarredFlagsProvider components
1 parent 6cab550 commit 263ea04

File tree

4 files changed

+344
-2
lines changed

4 files changed

+344
-2
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { expect, test, describe, vi } from 'vitest';
3+
4+
import { FilterTabs, type FilterTabsProps } from '../ui/Toolbar/components/FilterTabs/FilterTabs';
5+
import { FilterTabsContext, type FilterMode } from '../ui/Toolbar/components/FilterTabs/useFilterTabsContext';
6+
7+
describe('FilterTabs', () => {
8+
const defaultProps: FilterTabsProps = {
9+
totalFlags: 5,
10+
filteredFlags: 5,
11+
totalOverriddenFlags: 2,
12+
starredCount: 1,
13+
onClearOverrides: vi.fn(),
14+
onClearStarred: vi.fn(),
15+
isLoading: false,
16+
};
17+
18+
const renderWithContext = (props: FilterTabsProps = defaultProps, activeFilter: FilterMode = 'all') => {
19+
const onFilterChange = vi.fn();
20+
21+
return {
22+
onFilterChange,
23+
...render(
24+
<FilterTabsContext.Provider value={{ activeFilter, onFilterChange }}>
25+
<FilterTabs {...props} />
26+
</FilterTabsContext.Provider>,
27+
),
28+
};
29+
};
30+
31+
describe('Status Text', () => {
32+
test('shows "Showing all X flags" when on All filter with no search', () => {
33+
renderWithContext({ ...defaultProps, filteredFlags: 5, totalFlags: 5 }, 'all');
34+
35+
expect(screen.getByText('Showing all 5 flags')).toBeInTheDocument();
36+
});
37+
38+
test('shows "Showing X of Y flags" when searching on All filter', () => {
39+
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, 'all');
40+
41+
expect(screen.getByText('Showing 2 of 5 flags')).toBeInTheDocument();
42+
});
43+
44+
test('shows "Showing X of Y flags" when on Overrides filter', () => {
45+
renderWithContext({ ...defaultProps, filteredFlags: 2, totalFlags: 5 }, 'overrides');
46+
47+
expect(screen.getByText('Showing 2 of 5 flags')).toBeInTheDocument();
48+
});
49+
50+
test('shows "Showing X of Y flags" when on Starred filter', () => {
51+
renderWithContext({ ...defaultProps, filteredFlags: 1, totalFlags: 5 }, 'starred');
52+
53+
expect(screen.getByText('Showing 1 of 5 flags')).toBeInTheDocument();
54+
});
55+
});
56+
57+
describe('Clear Buttons', () => {
58+
test('shows clear button on Overrides filter when totalOverriddenFlags > 0', () => {
59+
renderWithContext({ ...defaultProps, totalOverriddenFlags: 2 }, 'overrides');
60+
61+
expect(screen.getByText(/Clear Overrides \(2\)/)).toBeInTheDocument();
62+
});
63+
64+
test('does not show clear button on Overrides filter when totalOverriddenFlags = 0', () => {
65+
renderWithContext({ ...defaultProps, totalOverriddenFlags: 0 }, 'overrides');
66+
67+
expect(screen.queryByText(/Clear Overrides/)).not.toBeInTheDocument();
68+
});
69+
70+
test('shows clear button on Starred filter when starredCount > 0', () => {
71+
renderWithContext({ ...defaultProps, starredCount: 3 }, 'starred');
72+
73+
expect(screen.getByText(/Clear Starred \(3\)/)).toBeInTheDocument();
74+
});
75+
76+
test('does not show clear button on Starred filter when starredCount = 0', () => {
77+
renderWithContext({ ...defaultProps, starredCount: 0 }, 'starred');
78+
79+
expect(screen.queryByText(/Clear Starred/)).not.toBeInTheDocument();
80+
});
81+
82+
test('does not show clear button on All filter', () => {
83+
renderWithContext(defaultProps, 'all');
84+
85+
expect(screen.queryByText(/Clear Overrides/)).not.toBeInTheDocument();
86+
expect(screen.queryByText(/Clear Starred/)).not.toBeInTheDocument();
87+
});
88+
89+
test('disables clear button when isLoading is true', () => {
90+
renderWithContext({ ...defaultProps, isLoading: true }, 'overrides');
91+
92+
const clearButton = screen.getByText(/Clear Overrides/);
93+
expect(clearButton).toBeDisabled();
94+
});
95+
});
96+
});
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import { expect, test, describe, vi, beforeEach } from 'vitest';
3+
4+
import { StarredFlagsProvider, useStarredFlags } from '../ui/Toolbar/context/StarredFlagsProvider';
5+
import { TOOLBAR_STORAGE_KEYS } from '../ui/Toolbar/utils/localStorage';
6+
7+
// Test component that consumes the context
8+
function TestConsumer() {
9+
const { isStarred, toggleStarred, clearAllStarred, starredCount } = useStarredFlags();
10+
11+
return (
12+
<div>
13+
<div data-testid="starred-count">{starredCount}</div>
14+
<div data-testid="flag-1-starred">{isStarred('flag-1').toString()}</div>
15+
<div data-testid="flag-2-starred">{isStarred('flag-2').toString()}</div>
16+
<button onClick={() => toggleStarred('flag-1')}>Toggle Flag 1</button>
17+
<button onClick={() => toggleStarred('flag-2')}>Toggle Flag 2</button>
18+
<button onClick={clearAllStarred}>Clear All</button>
19+
</div>
20+
);
21+
}
22+
23+
describe('StarredFlagsProvider', () => {
24+
beforeEach(() => {
25+
vi.clearAllMocks();
26+
localStorage.clear();
27+
});
28+
29+
describe('Initialization', () => {
30+
test('initializes with empty starred flags when localStorage is empty', () => {
31+
render(
32+
<StarredFlagsProvider>
33+
<TestConsumer />
34+
</StarredFlagsProvider>,
35+
);
36+
37+
expect(screen.getByTestId('starred-count')).toHaveTextContent('0');
38+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('false');
39+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('false');
40+
});
41+
42+
test('loads starred flags from localStorage', () => {
43+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1', 'flag-3']));
44+
45+
render(
46+
<StarredFlagsProvider>
47+
<TestConsumer />
48+
</StarredFlagsProvider>,
49+
);
50+
51+
expect(screen.getByTestId('starred-count')).toHaveTextContent('2');
52+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
53+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('false');
54+
});
55+
});
56+
57+
describe('starredCount', () => {
58+
test('starredCount updates when flags are starred', () => {
59+
render(
60+
<StarredFlagsProvider>
61+
<TestConsumer />
62+
</StarredFlagsProvider>,
63+
);
64+
65+
expect(screen.getByTestId('starred-count')).toHaveTextContent('0');
66+
67+
fireEvent.click(screen.getByText('Toggle Flag 1'));
68+
expect(screen.getByTestId('starred-count')).toHaveTextContent('1');
69+
70+
fireEvent.click(screen.getByText('Toggle Flag 2'));
71+
expect(screen.getByTestId('starred-count')).toHaveTextContent('2');
72+
});
73+
74+
test('starredCount updates when flags are unstarred', () => {
75+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1', 'flag-2']));
76+
77+
render(
78+
<StarredFlagsProvider>
79+
<TestConsumer />
80+
</StarredFlagsProvider>,
81+
);
82+
83+
expect(screen.getByTestId('starred-count')).toHaveTextContent('2');
84+
85+
fireEvent.click(screen.getByText('Toggle Flag 1'));
86+
expect(screen.getByTestId('starred-count')).toHaveTextContent('1');
87+
});
88+
89+
test('starredCount is 0 after clearAllStarred', () => {
90+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1', 'flag-2']));
91+
92+
render(
93+
<StarredFlagsProvider>
94+
<TestConsumer />
95+
</StarredFlagsProvider>,
96+
);
97+
98+
expect(screen.getByTestId('starred-count')).toHaveTextContent('2');
99+
100+
fireEvent.click(screen.getByText('Clear All'));
101+
expect(screen.getByTestId('starred-count')).toHaveTextContent('0');
102+
});
103+
});
104+
105+
describe('toggleStarred', () => {
106+
test('adds flag to starred when not starred', () => {
107+
render(
108+
<StarredFlagsProvider>
109+
<TestConsumer />
110+
</StarredFlagsProvider>,
111+
);
112+
113+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('false');
114+
115+
fireEvent.click(screen.getByText('Toggle Flag 1'));
116+
117+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
118+
});
119+
120+
test('removes flag from starred when already starred', () => {
121+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1']));
122+
123+
render(
124+
<StarredFlagsProvider>
125+
<TestConsumer />
126+
</StarredFlagsProvider>,
127+
);
128+
129+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
130+
131+
fireEvent.click(screen.getByText('Toggle Flag 1'));
132+
133+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('false');
134+
});
135+
136+
test('persists changes to localStorage', () => {
137+
render(
138+
<StarredFlagsProvider>
139+
<TestConsumer />
140+
</StarredFlagsProvider>,
141+
);
142+
143+
fireEvent.click(screen.getByText('Toggle Flag 1'));
144+
145+
const stored = localStorage.getItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS);
146+
expect(stored).toBe(JSON.stringify(['flag-1']));
147+
});
148+
149+
test('can toggle multiple flags independently', () => {
150+
render(
151+
<StarredFlagsProvider>
152+
<TestConsumer />
153+
</StarredFlagsProvider>,
154+
);
155+
156+
fireEvent.click(screen.getByText('Toggle Flag 1'));
157+
fireEvent.click(screen.getByText('Toggle Flag 2'));
158+
159+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
160+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('true');
161+
162+
const stored = localStorage.getItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS);
163+
const parsed = JSON.parse(stored!);
164+
expect(parsed).toContain('flag-1');
165+
expect(parsed).toContain('flag-2');
166+
});
167+
});
168+
169+
describe('isStarred', () => {
170+
test('returns false for non-starred flag', () => {
171+
render(
172+
<StarredFlagsProvider>
173+
<TestConsumer />
174+
</StarredFlagsProvider>,
175+
);
176+
177+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('false');
178+
});
179+
180+
test('returns true for starred flag', () => {
181+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1']));
182+
183+
render(
184+
<StarredFlagsProvider>
185+
<TestConsumer />
186+
</StarredFlagsProvider>,
187+
);
188+
189+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
190+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('false');
191+
});
192+
});
193+
194+
describe('clearAllStarred', () => {
195+
test('removes all starred flags', () => {
196+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1', 'flag-2']));
197+
198+
render(
199+
<StarredFlagsProvider>
200+
<TestConsumer />
201+
</StarredFlagsProvider>,
202+
);
203+
204+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('true');
205+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('true');
206+
207+
fireEvent.click(screen.getByText('Clear All'));
208+
209+
expect(screen.getByTestId('flag-1-starred')).toHaveTextContent('false');
210+
expect(screen.getByTestId('flag-2-starred')).toHaveTextContent('false');
211+
});
212+
213+
test('persists empty state to localStorage', () => {
214+
localStorage.setItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS, JSON.stringify(['flag-1', 'flag-2']));
215+
216+
render(
217+
<StarredFlagsProvider>
218+
<TestConsumer />
219+
</StarredFlagsProvider>,
220+
);
221+
222+
fireEvent.click(screen.getByText('Clear All'));
223+
224+
const stored = localStorage.getItem(TOOLBAR_STORAGE_KEYS.STARRED_FLAGS);
225+
expect(stored).toBe(JSON.stringify([]));
226+
});
227+
228+
test('works when no flags are starred', () => {
229+
render(
230+
<StarredFlagsProvider>
231+
<TestConsumer />
232+
</StarredFlagsProvider>,
233+
);
234+
235+
fireEvent.click(screen.getByText('Clear All'));
236+
237+
expect(screen.getByTestId('starred-count')).toHaveTextContent('0');
238+
});
239+
});
240+
});

packages/toolbar/src/core/ui/Toolbar/components/FilterTabs/FilterTabs.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const FILTER_TABS = [
88
{ id: 'starred' as const, label: 'Starred' },
99
] satisfies { id: FilterMode; label: string }[];
1010

11-
interface FilterTabsProps {
11+
export interface FilterTabsProps {
1212
totalFlags: number;
1313
filteredFlags: number;
1414
totalOverriddenFlags: number;
@@ -53,7 +53,12 @@ export function FilterTabs(props: FilterTabsProps) {
5353
: `Showing ${filteredFlags} of ${totalFlags} flags`}
5454
</div>
5555
{isOverridesActive && totalOverriddenFlags > 0 && onClearOverrides && (
56-
<ClearButton label="Overrides" count={totalOverriddenFlags} onClick={onClearOverrides} isLoading={isLoading} />
56+
<ClearButton
57+
label="Overrides"
58+
count={totalOverriddenFlags}
59+
onClick={onClearOverrides}
60+
isLoading={isLoading}
61+
/>
5762
)}
5863
{isStarredActive && starredCount > 0 && onClearStarred && (
5964
<ClearButton label="Starred" count={starredCount} onClick={onClearStarred} isLoading={isLoading} />

packages/toolbar/tsconfig.test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
44
"noUncheckedIndexedAccess": false,
5+
"jsx": "react-jsx",
56
"types": ["vitest/globals", "@testing-library/jest-dom"]
67
},
78
"include": ["src/**/*.test.ts", "src/**/*.test.tsx"],

0 commit comments

Comments
 (0)