Skip to content

Commit 6fc20da

Browse files
talissoncostaclaude
andcommitted
test(components): add integration tests for main components
Add integration tests for main components with mocked API responses, testing loading states, data display, error handling, and user interactions. - FlagsTab: search filtering, feature display, error states - FlagsmithOverviewCard: stats display, pagination - FlagsmithUsageCard: chart rendering, usage data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 10479f4 commit 6fc20da

File tree

3 files changed

+584
-0
lines changed

3 files changed

+584
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
2+
import { screen, waitFor, fireEvent } from '@testing-library/react';
3+
import { FlagsTab } from '../index';
4+
import {
5+
renderWithBackstage,
6+
mockProject,
7+
mockEnvironments,
8+
mockFeatures,
9+
mockEntity,
10+
mockEntityNoAnnotations,
11+
} from '../../../__tests__/fixtures';
12+
13+
describe('FlagsTab', () => {
14+
const defaultFetchResponses = {
15+
'/projects/123/': mockProject,
16+
'/projects/123/environments/': { results: mockEnvironments },
17+
'/projects/123/features/': { results: mockFeatures },
18+
};
19+
20+
it('shows loading state initially', () => {
21+
renderWithBackstage(<FlagsTab />, {
22+
fetchResponses: defaultFetchResponses,
23+
});
24+
25+
expect(screen.getByRole('status')).toBeInTheDocument();
26+
expect(screen.getByText('Loading feature flags...')).toBeInTheDocument();
27+
});
28+
29+
it('displays feature flags after loading', async () => {
30+
renderWithBackstage(<FlagsTab />, {
31+
fetchResponses: defaultFetchResponses,
32+
});
33+
34+
await waitFor(() => {
35+
expect(screen.queryByText('Loading feature flags...')).not.toBeInTheDocument();
36+
});
37+
38+
// Should display project name and flag count
39+
expect(screen.getByText(/Test Project/)).toBeInTheDocument();
40+
expect(screen.getByText(/3 flags/)).toBeInTheDocument();
41+
42+
// Should display feature names
43+
expect(screen.getByText('feature-one')).toBeInTheDocument();
44+
expect(screen.getByText('feature-two')).toBeInTheDocument();
45+
expect(screen.getByText('feature-three')).toBeInTheDocument();
46+
});
47+
48+
it('displays error when project ID is missing', async () => {
49+
renderWithBackstage(<FlagsTab />, {
50+
entity: mockEntityNoAnnotations,
51+
});
52+
53+
await waitFor(() => {
54+
expect(screen.queryByText('Loading feature flags...')).not.toBeInTheDocument();
55+
});
56+
57+
expect(screen.getByText(/No Flagsmith project ID found/)).toBeInTheDocument();
58+
expect(screen.getByText(/flagsmith.com\/project-id/)).toBeInTheDocument();
59+
});
60+
61+
it('displays error on API failure', async () => {
62+
const fetchApi = {
63+
fetch: jest.fn().mockResolvedValue({
64+
ok: false,
65+
statusText: 'Internal Server Error',
66+
}),
67+
};
68+
69+
renderWithBackstage(<FlagsTab />, {
70+
fetchApi,
71+
});
72+
73+
await waitFor(() => {
74+
expect(screen.queryByText('Loading feature flags...')).not.toBeInTheDocument();
75+
});
76+
77+
expect(screen.getByText(/Error:/)).toBeInTheDocument();
78+
});
79+
80+
it('filters features by search query', async () => {
81+
renderWithBackstage(<FlagsTab />, {
82+
fetchResponses: defaultFetchResponses,
83+
});
84+
85+
await waitFor(() => {
86+
expect(screen.getByText('feature-one')).toBeInTheDocument();
87+
});
88+
89+
const searchInput = screen.getByRole('searchbox');
90+
fireEvent.change(searchInput, { target: { value: 'feature-one' } });
91+
92+
// Wait for debounce
93+
await waitFor(() => {
94+
expect(screen.getByText('feature-one')).toBeInTheDocument();
95+
expect(screen.queryByText('feature-two')).not.toBeInTheDocument();
96+
expect(screen.queryByText('feature-three')).not.toBeInTheDocument();
97+
}, { timeout: 500 });
98+
});
99+
100+
it('shows no results message when search has no matches', async () => {
101+
renderWithBackstage(<FlagsTab />, {
102+
fetchResponses: defaultFetchResponses,
103+
});
104+
105+
await waitFor(() => {
106+
expect(screen.getByText('feature-one')).toBeInTheDocument();
107+
});
108+
109+
const searchInput = screen.getByRole('searchbox');
110+
fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
111+
112+
await waitFor(() => {
113+
expect(screen.getByText('No flags match your search')).toBeInTheDocument();
114+
}, { timeout: 500 });
115+
});
116+
117+
it('displays table headers', async () => {
118+
renderWithBackstage(<FlagsTab />, {
119+
fetchResponses: defaultFetchResponses,
120+
});
121+
122+
await waitFor(() => {
123+
expect(screen.getByText('Flag Name')).toBeInTheDocument();
124+
});
125+
126+
expect(screen.getByText('Type')).toBeInTheDocument();
127+
expect(screen.getByText('Created')).toBeInTheDocument();
128+
});
129+
130+
it('displays feature types', async () => {
131+
renderWithBackstage(<FlagsTab />, {
132+
fetchResponses: defaultFetchResponses,
133+
});
134+
135+
await waitFor(() => {
136+
expect(screen.getByText('feature-one')).toBeInTheDocument();
137+
});
138+
139+
expect(screen.getAllByText('FLAG').length).toBeGreaterThan(0);
140+
expect(screen.getByText('CONFIG')).toBeInTheDocument();
141+
});
142+
143+
it('has expandable rows', async () => {
144+
renderWithBackstage(<FlagsTab />, {
145+
fetchResponses: defaultFetchResponses,
146+
});
147+
148+
await waitFor(() => {
149+
expect(screen.getByText('feature-one')).toBeInTheDocument();
150+
});
151+
152+
// Should have expand buttons
153+
const expandButtons = screen.getAllByRole('button', { name: /expand/i });
154+
expect(expandButtons.length).toBeGreaterThan(0);
155+
});
156+
157+
it('displays truncated description', async () => {
158+
renderWithBackstage(<FlagsTab />, {
159+
fetchResponses: defaultFetchResponses,
160+
});
161+
162+
await waitFor(() => {
163+
expect(screen.getByText('First test feature')).toBeInTheDocument();
164+
});
165+
});
166+
167+
it('renders Flagsmith dashboard link', async () => {
168+
renderWithBackstage(<FlagsTab />, {
169+
fetchResponses: defaultFetchResponses,
170+
});
171+
172+
await waitFor(() => {
173+
expect(screen.getByText('feature-one')).toBeInTheDocument();
174+
});
175+
176+
// Should have Open Dashboard button
177+
const dashboardButton = screen.getByRole('button', { name: /open dashboard/i });
178+
expect(dashboardButton).toBeInTheDocument();
179+
});
180+
});
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
2+
import { screen, waitFor, fireEvent } from '@testing-library/react';
3+
import { FlagsmithOverviewCard } from '../index';
4+
import {
5+
renderWithBackstage,
6+
mockProject,
7+
mockEnvironments,
8+
mockFeatures,
9+
mockEntityNoAnnotations,
10+
} from '../../../__tests__/fixtures';
11+
12+
describe('FlagsmithOverviewCard', () => {
13+
const defaultFetchResponses = {
14+
'/projects/123/': mockProject,
15+
'/projects/123/environments/': { results: mockEnvironments },
16+
'/projects/123/features/': { results: mockFeatures },
17+
};
18+
19+
it('shows loading state initially', () => {
20+
renderWithBackstage(<FlagsmithOverviewCard />, {
21+
fetchResponses: defaultFetchResponses,
22+
});
23+
24+
expect(screen.getByRole('status')).toBeInTheDocument();
25+
expect(screen.getByText('Loading Flagsmith data...')).toBeInTheDocument();
26+
});
27+
28+
it('displays feature flag stats after loading', async () => {
29+
renderWithBackstage(<FlagsmithOverviewCard />, {
30+
fetchResponses: defaultFetchResponses,
31+
});
32+
33+
await waitFor(() => {
34+
expect(screen.queryByText('Loading Flagsmith data...')).not.toBeInTheDocument();
35+
});
36+
37+
// Should show total flags
38+
expect(screen.getByText('3')).toBeInTheDocument(); // Total flags
39+
expect(screen.getByText('Total Flags')).toBeInTheDocument();
40+
});
41+
42+
it('displays enabled/disabled counts', async () => {
43+
renderWithBackstage(<FlagsmithOverviewCard />, {
44+
fetchResponses: defaultFetchResponses,
45+
});
46+
47+
await waitFor(() => {
48+
expect(screen.getByText('Total Flags')).toBeInTheDocument();
49+
});
50+
51+
// mockFeatures: 2 enabled (feature-one, feature-three), 1 disabled (feature-two)
52+
expect(screen.getByText('2')).toBeInTheDocument(); // Enabled count
53+
expect(screen.getByText('1')).toBeInTheDocument(); // Disabled count
54+
expect(screen.getByText('Enabled')).toBeInTheDocument();
55+
expect(screen.getByText('Disabled')).toBeInTheDocument();
56+
});
57+
58+
it('displays error when project ID is missing', async () => {
59+
renderWithBackstage(<FlagsmithOverviewCard />, {
60+
entity: mockEntityNoAnnotations,
61+
});
62+
63+
await waitFor(() => {
64+
expect(screen.queryByText('Loading Flagsmith data...')).not.toBeInTheDocument();
65+
});
66+
67+
expect(screen.getByText(/No Flagsmith project ID found/)).toBeInTheDocument();
68+
});
69+
70+
it('displays error on API failure', async () => {
71+
const fetchApi = {
72+
fetch: jest.fn().mockResolvedValue({
73+
ok: false,
74+
statusText: 'Forbidden',
75+
}),
76+
};
77+
78+
renderWithBackstage(<FlagsmithOverviewCard />, {
79+
fetchApi,
80+
});
81+
82+
await waitFor(() => {
83+
expect(screen.queryByText('Loading Flagsmith data...')).not.toBeInTheDocument();
84+
});
85+
86+
expect(screen.getByText(/Error:/)).toBeInTheDocument();
87+
});
88+
89+
it('displays feature list', async () => {
90+
renderWithBackstage(<FlagsmithOverviewCard />, {
91+
fetchResponses: defaultFetchResponses,
92+
});
93+
94+
await waitFor(() => {
95+
expect(screen.getByText('feature-one')).toBeInTheDocument();
96+
});
97+
98+
// At least some features should be visible
99+
expect(screen.getByText('feature-one')).toBeInTheDocument();
100+
});
101+
102+
it('renders card with title', async () => {
103+
renderWithBackstage(<FlagsmithOverviewCard />, {
104+
fetchResponses: defaultFetchResponses,
105+
});
106+
107+
expect(screen.getByText('Flagsmith Feature Flags')).toBeInTheDocument();
108+
});
109+
110+
it('renders dashboard link', async () => {
111+
renderWithBackstage(<FlagsmithOverviewCard />, {
112+
fetchResponses: defaultFetchResponses,
113+
});
114+
115+
await waitFor(() => {
116+
expect(screen.getByText('feature-one')).toBeInTheDocument();
117+
});
118+
119+
const dashboardButton = screen.getByRole('button', { name: /open dashboard/i });
120+
expect(dashboardButton).toBeInTheDocument();
121+
});
122+
123+
it('shows environments count', async () => {
124+
renderWithBackstage(<FlagsmithOverviewCard />, {
125+
fetchResponses: defaultFetchResponses,
126+
});
127+
128+
await waitFor(() => {
129+
expect(screen.getByText('Total Flags')).toBeInTheDocument();
130+
});
131+
132+
// Should show number of environments
133+
expect(screen.getByText('Environments')).toBeInTheDocument();
134+
});
135+
136+
it('handles many features with pagination', async () => {
137+
// Create more than 5 features to trigger pagination
138+
const manyFeatures = Array.from({ length: 10 }, (_, i) => ({
139+
id: i + 1,
140+
name: `feature-${i + 1}`,
141+
description: `Feature ${i + 1}`,
142+
created_date: '2024-01-01T00:00:00Z',
143+
project: 123,
144+
default_enabled: i % 2 === 0,
145+
}));
146+
147+
renderWithBackstage(<FlagsmithOverviewCard />, {
148+
fetchResponses: {
149+
...defaultFetchResponses,
150+
'/projects/123/features/': { results: manyFeatures },
151+
},
152+
});
153+
154+
await waitFor(() => {
155+
expect(screen.getByText('feature-1')).toBeInTheDocument();
156+
});
157+
158+
// Should show pagination
159+
expect(screen.getByRole('navigation', { name: /pagination/i })).toBeInTheDocument();
160+
});
161+
162+
it('navigates pages when pagination is clicked', async () => {
163+
const manyFeatures = Array.from({ length: 10 }, (_, i) => ({
164+
id: i + 1,
165+
name: `feature-${i + 1}`,
166+
description: `Feature ${i + 1}`,
167+
created_date: '2024-01-01T00:00:00Z',
168+
project: 123,
169+
default_enabled: true,
170+
}));
171+
172+
renderWithBackstage(<FlagsmithOverviewCard />, {
173+
fetchResponses: {
174+
...defaultFetchResponses,
175+
'/projects/123/features/': { results: manyFeatures },
176+
},
177+
});
178+
179+
await waitFor(() => {
180+
expect(screen.getByText('feature-1')).toBeInTheDocument();
181+
});
182+
183+
// Click next page
184+
const nextButton = screen.getByRole('button', { name: /next page/i });
185+
fireEvent.click(nextButton);
186+
187+
// Should show next page features
188+
await waitFor(() => {
189+
expect(screen.getByText('feature-6')).toBeInTheDocument();
190+
});
191+
});
192+
193+
it('handles empty features list', async () => {
194+
renderWithBackstage(<FlagsmithOverviewCard />, {
195+
fetchResponses: {
196+
...defaultFetchResponses,
197+
'/projects/123/features/': { results: [] },
198+
},
199+
});
200+
201+
await waitFor(() => {
202+
expect(screen.queryByText('Loading Flagsmith data...')).not.toBeInTheDocument();
203+
});
204+
205+
// Should show 0 total flags
206+
expect(screen.getByText('0')).toBeInTheDocument();
207+
});
208+
});

0 commit comments

Comments
 (0)