Skip to content

Commit 51b218b

Browse files
talissoncostaclaude
andcommitted
test(hooks): add unit tests for custom hooks
Add tests for useFlagsmithProject and useFlagsmithUsage hooks including loading states, success flows, and error handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 5501efa commit 51b218b

File tree

2 files changed

+423
-0
lines changed

2 files changed

+423
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { renderHook, waitFor } from '@testing-library/react';
2+
import { ReactNode } from 'react';
3+
// eslint-disable-next-line @backstage/no-undeclared-imports
4+
import { TestApiProvider } from '@backstage/test-utils';
5+
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
6+
import { useFlagsmithProject } from './useFlagsmithProject';
7+
import {
8+
mockProject,
9+
mockEnvironments,
10+
mockFeatures,
11+
createMockDiscoveryApi,
12+
} from '../__tests__/fixtures';
13+
14+
describe('useFlagsmithProject', () => {
15+
const createWrapper = (fetchMock: jest.Mock) => {
16+
const discoveryApi = createMockDiscoveryApi();
17+
const fetchApi = { fetch: fetchMock };
18+
19+
return ({ children }: { children: ReactNode }) => (
20+
<TestApiProvider
21+
apis={[
22+
[discoveryApiRef, discoveryApi],
23+
[fetchApiRef, fetchApi],
24+
]}
25+
>
26+
{children}
27+
</TestApiProvider>
28+
);
29+
};
30+
31+
it('returns loading state initially', () => {
32+
const fetchMock = jest.fn().mockResolvedValue({
33+
ok: true,
34+
json: async () => mockProject,
35+
});
36+
37+
const { result } = renderHook(() => useFlagsmithProject('123'), {
38+
wrapper: createWrapper(fetchMock),
39+
});
40+
41+
expect(result.current.loading).toBe(true);
42+
expect(result.current.error).toBeNull();
43+
expect(result.current.project).toBeNull();
44+
expect(result.current.environments).toEqual([]);
45+
expect(result.current.features).toEqual([]);
46+
});
47+
48+
it('fetches project data successfully', async () => {
49+
const fetchMock = jest
50+
.fn()
51+
.mockResolvedValueOnce({
52+
ok: true,
53+
json: async () => mockProject,
54+
})
55+
.mockResolvedValueOnce({
56+
ok: true,
57+
json: async () => ({ results: mockEnvironments }),
58+
})
59+
.mockResolvedValueOnce({
60+
ok: true,
61+
json: async () => ({ results: mockFeatures }),
62+
});
63+
64+
const { result } = renderHook(() => useFlagsmithProject('123'), {
65+
wrapper: createWrapper(fetchMock),
66+
});
67+
68+
await waitFor(() => {
69+
expect(result.current.loading).toBe(false);
70+
});
71+
72+
expect(result.current.error).toBeNull();
73+
expect(result.current.project).toEqual(mockProject);
74+
expect(result.current.environments).toEqual(mockEnvironments);
75+
expect(result.current.features).toEqual(mockFeatures);
76+
});
77+
78+
it('returns error when projectId is undefined', async () => {
79+
const fetchMock = jest.fn();
80+
81+
const { result } = renderHook(() => useFlagsmithProject(undefined), {
82+
wrapper: createWrapper(fetchMock),
83+
});
84+
85+
await waitFor(() => {
86+
expect(result.current.loading).toBe(false);
87+
});
88+
89+
expect(result.current.error).toBe(
90+
'No Flagsmith project ID found in entity annotations',
91+
);
92+
expect(fetchMock).not.toHaveBeenCalled();
93+
});
94+
95+
it('returns error on API failure', async () => {
96+
const fetchMock = jest.fn().mockResolvedValue({
97+
ok: false,
98+
statusText: 'Internal Server Error',
99+
});
100+
101+
const { result } = renderHook(() => useFlagsmithProject('123'), {
102+
wrapper: createWrapper(fetchMock),
103+
});
104+
105+
await waitFor(() => {
106+
expect(result.current.loading).toBe(false);
107+
});
108+
109+
expect(result.current.error).toBe(
110+
'Failed to fetch project: Internal Server Error',
111+
);
112+
});
113+
114+
it('returns the client instance', () => {
115+
const fetchMock = jest.fn().mockResolvedValue({
116+
ok: true,
117+
json: async () => mockProject,
118+
});
119+
120+
const { result } = renderHook(() => useFlagsmithProject('123'), {
121+
wrapper: createWrapper(fetchMock),
122+
});
123+
124+
expect(result.current.client).toBeDefined();
125+
expect(typeof result.current.client.getProject).toBe('function');
126+
expect(typeof result.current.client.getFeatureDetails).toBe('function');
127+
});
128+
129+
it('memoizes client across re-renders', async () => {
130+
const fetchMock = jest
131+
.fn()
132+
.mockResolvedValueOnce({ ok: true, json: async () => mockProject })
133+
.mockResolvedValueOnce({ ok: true, json: async () => ({ results: [] }) })
134+
.mockResolvedValueOnce({ ok: true, json: async () => ({ results: [] }) });
135+
136+
const { result, rerender } = renderHook(() => useFlagsmithProject('123'), {
137+
wrapper: createWrapper(fetchMock),
138+
});
139+
140+
const firstClient = result.current.client;
141+
142+
await waitFor(() => {
143+
expect(result.current.loading).toBe(false);
144+
});
145+
146+
rerender();
147+
148+
expect(result.current.client).toBe(firstClient);
149+
});
150+
151+
it('handles network errors', async () => {
152+
const fetchMock = jest
153+
.fn()
154+
.mockRejectedValue(new Error('Network error'));
155+
156+
const { result } = renderHook(() => useFlagsmithProject('123'), {
157+
wrapper: createWrapper(fetchMock),
158+
});
159+
160+
await waitFor(() => {
161+
expect(result.current.loading).toBe(false);
162+
});
163+
164+
expect(result.current.error).toBe('Network error');
165+
});
166+
167+
it('handles non-Error exceptions', async () => {
168+
const fetchMock = jest.fn().mockRejectedValue('String error');
169+
170+
const { result } = renderHook(() => useFlagsmithProject('123'), {
171+
wrapper: createWrapper(fetchMock),
172+
});
173+
174+
await waitFor(() => {
175+
expect(result.current.loading).toBe(false);
176+
});
177+
178+
expect(result.current.error).toBe('Unknown error');
179+
});
180+
});

0 commit comments

Comments
 (0)