Skip to content

Commit 33a1578

Browse files
committed
✅(frontend) Improve test coverage
Signed-off-by: Zorin95670 <[email protected]>
1 parent 17f1225 commit 33a1578

File tree

6 files changed

+178
-0
lines changed

6 files changed

+178
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to
1515
## Changed
1616

1717
- 📝(frontend) Update documentation
18+
- ✅(frontend) Improve tests coverage
1819

1920
## [3.2.1] - 2025-05-06
2021

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { APIError, isAPIError } from '@/api';
2+
3+
describe('APIError', () => {
4+
it('should correctly instantiate with required fields', () => {
5+
const error = new APIError('Something went wrong', { status: 500 });
6+
7+
expect(error).toBeInstanceOf(Error);
8+
expect(error).toBeInstanceOf(APIError);
9+
expect(error.message).toBe('Something went wrong');
10+
expect(error.status).toBe(500);
11+
expect(error.cause).toBeUndefined();
12+
expect(error.data).toBeUndefined();
13+
});
14+
15+
it('should correctly instantiate with all fields', () => {
16+
const details = { field: 'email' };
17+
const error = new APIError('Validation failed', {
18+
status: 400,
19+
cause: ['Invalid email format'],
20+
data: details,
21+
});
22+
23+
expect(error.name).toBe('APIError');
24+
expect(error.status).toBe(400);
25+
expect(error.cause).toEqual(['Invalid email format']);
26+
expect(error.data).toEqual(details);
27+
});
28+
29+
it('should be detected by isAPIError type guard', () => {
30+
const error = new APIError('Unauthorized', { status: 401 });
31+
const notAnError = { message: 'Fake error' };
32+
33+
expect(isAPIError(error)).toBe(true);
34+
expect(isAPIError(notAnError)).toBe(false);
35+
});
36+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { baseApiUrl } from '@/api';
2+
3+
describe('config', () => {
4+
it('constructs URL with default version', () => {
5+
expect(baseApiUrl()).toBe('http://test.jest/api/v1.0/');
6+
});
7+
8+
it('constructs URL with custom version', () => {
9+
expect(baseApiUrl('2.0')).toBe('http://test.jest/api/v2.0/');
10+
});
11+
12+
it('uses env origin if available', () => {
13+
process.env.NEXT_PUBLIC_API_ORIGIN = 'https://env.example.com';
14+
expect(baseApiUrl('3.0')).toBe('https://env.example.com/api/v3.0/');
15+
});
16+
});

src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ describe('fetchAPI', () => {
3636

3737
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v2.0/some/url');
3838
});
39+
40+
it('removes Content-Type header when withoutContentType is true', async () => {
41+
fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
42+
43+
await fetchAPI('some/url', { withoutContentType: true });
44+
45+
const options = fetchMock.lastOptions();
46+
expect(options?.headers).not.toHaveProperty('Content-Type');
47+
});
3948
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import { renderHook, waitFor } from '@testing-library/react';
3+
4+
import { useAPIInfiniteQuery } from '@/api';
5+
6+
interface DummyItem {
7+
id: number;
8+
}
9+
10+
interface DummyResponse {
11+
results: DummyItem[];
12+
next?: string;
13+
}
14+
15+
const createWrapper = () => {
16+
const queryClient = new QueryClient();
17+
return ({ children }: { children: React.ReactNode }) => (
18+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
19+
);
20+
};
21+
22+
describe('helpers', () => {
23+
it('fetches and paginates correctly', async () => {
24+
const mockAPI = jest
25+
.fn<Promise<DummyResponse>, [{ page: number; query: string }]>()
26+
.mockResolvedValueOnce({
27+
results: [{ id: 1 }],
28+
next: 'url?page=2',
29+
})
30+
.mockResolvedValueOnce({
31+
results: [{ id: 2 }],
32+
next: undefined,
33+
});
34+
35+
const { result } = renderHook(
36+
() => useAPIInfiniteQuery('test-key', mockAPI, { query: 'test' }),
37+
{ wrapper: createWrapper() },
38+
);
39+
40+
// Wait for first page
41+
await waitFor(() => {
42+
expect(result.current.data?.pages[0].results[0].id).toBe(1);
43+
});
44+
45+
// Fetch next page
46+
await result.current.fetchNextPage();
47+
48+
await waitFor(() => {
49+
expect(result.current.data?.pages.length).toBe(2);
50+
});
51+
52+
await waitFor(() => {
53+
expect(result.current.data?.pages[1].results[0].id).toBe(2);
54+
});
55+
56+
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 1 });
57+
expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 2 });
58+
});
59+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { errorCauses, getCSRFToken } from '@/api';
2+
3+
describe('utils', () => {
4+
describe('errorCauses', () => {
5+
const createMockResponse = (jsonData: any, status = 400): Response => {
6+
return {
7+
status,
8+
json: () => jsonData,
9+
} as unknown as Response;
10+
};
11+
12+
it('parses multiple string causes from error body', async () => {
13+
const mockResponse = createMockResponse(
14+
{
15+
field: ['error message 1', 'error message 2'],
16+
},
17+
400,
18+
);
19+
20+
const result = await errorCauses(mockResponse, { context: 'login' });
21+
22+
expect(result.status).toBe(400);
23+
expect(result.cause).toEqual(['error message 1', 'error message 2']);
24+
expect(result.data).toEqual({ context: 'login' });
25+
});
26+
27+
it('returns undefined causes if no JSON body', async () => {
28+
const mockResponse = createMockResponse(null, 500);
29+
30+
const result = await errorCauses(mockResponse);
31+
32+
expect(result.status).toBe(500);
33+
expect(result.cause).toBeUndefined();
34+
expect(result.data).toBeUndefined();
35+
});
36+
});
37+
38+
describe('getCSRFToken', () => {
39+
it('extracts csrftoken from document.cookie', () => {
40+
Object.defineProperty(document, 'cookie', {
41+
writable: true,
42+
value: 'sessionid=xyz; csrftoken=abc123; theme=dark',
43+
});
44+
45+
expect(getCSRFToken()).toBe('abc123');
46+
});
47+
48+
it('returns undefined if csrftoken is not present', () => {
49+
Object.defineProperty(document, 'cookie', {
50+
writable: true,
51+
value: 'sessionid=xyz; theme=dark',
52+
});
53+
54+
expect(getCSRFToken()).toBeUndefined();
55+
});
56+
});
57+
});

0 commit comments

Comments
 (0)