diff --git a/CHANGELOG.md b/CHANGELOG.md
index a389c0c5..4dfc5baf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to
## Changed
- 📝(frontend) Update documentation
+- ✅(frontend) Improve tests coverage
## [3.2.1] - 2025-05-06
diff --git a/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts b/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts
new file mode 100644
index 00000000..395a5460
--- /dev/null
+++ b/src/frontend/apps/impress/src/api/__tests__/APIError.test.ts
@@ -0,0 +1,36 @@
+import { APIError, isAPIError } from '@/api';
+
+describe('APIError', () => {
+ it('should correctly instantiate with required fields', () => {
+ const error = new APIError('Something went wrong', { status: 500 });
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(APIError);
+ expect(error.message).toBe('Something went wrong');
+ expect(error.status).toBe(500);
+ expect(error.cause).toBeUndefined();
+ expect(error.data).toBeUndefined();
+ });
+
+ it('should correctly instantiate with all fields', () => {
+ const details = { field: 'email' };
+ const error = new APIError('Validation failed', {
+ status: 400,
+ cause: ['Invalid email format'],
+ data: details,
+ });
+
+ expect(error.name).toBe('APIError');
+ expect(error.status).toBe(400);
+ expect(error.cause).toEqual(['Invalid email format']);
+ expect(error.data).toEqual(details);
+ });
+
+ it('should be detected by isAPIError type guard', () => {
+ const error = new APIError('Unauthorized', { status: 401 });
+ const notAnError = { message: 'Fake error' };
+
+ expect(isAPIError(error)).toBe(true);
+ expect(isAPIError(notAnError)).toBe(false);
+ });
+});
diff --git a/src/frontend/apps/impress/src/api/__tests__/config.test.ts b/src/frontend/apps/impress/src/api/__tests__/config.test.ts
new file mode 100644
index 00000000..cb9bf268
--- /dev/null
+++ b/src/frontend/apps/impress/src/api/__tests__/config.test.ts
@@ -0,0 +1,16 @@
+import { baseApiUrl } from '@/api';
+
+describe('config', () => {
+ it('constructs URL with default version', () => {
+ expect(baseApiUrl()).toBe('http://test.jest/api/v1.0/');
+ });
+
+ it('constructs URL with custom version', () => {
+ expect(baseApiUrl('2.0')).toBe('http://test.jest/api/v2.0/');
+ });
+
+ it('uses env origin if available', () => {
+ process.env.NEXT_PUBLIC_API_ORIGIN = 'https://env.example.com';
+ expect(baseApiUrl('3.0')).toBe('https://env.example.com/api/v3.0/');
+ });
+});
diff --git a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx
index 05dc0b13..9b9d6a9a 100644
--- a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx
+++ b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx
@@ -36,4 +36,13 @@ describe('fetchAPI', () => {
expect(fetchMock.lastUrl()).toEqual('http://test.jest/api/v2.0/some/url');
});
+
+ it('removes Content-Type header when withoutContentType is true', async () => {
+ fetchMock.mock('http://test.jest/api/v1.0/some/url', 200);
+
+ await fetchAPI('some/url', { withoutContentType: true });
+
+ const options = fetchMock.lastOptions();
+ expect(options?.headers).not.toHaveProperty('Content-Type');
+ });
});
diff --git a/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx b/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx
new file mode 100644
index 00000000..e4706367
--- /dev/null
+++ b/src/frontend/apps/impress/src/api/__tests__/helpers.test.tsx
@@ -0,0 +1,59 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { renderHook, waitFor } from '@testing-library/react';
+
+import { useAPIInfiniteQuery } from '@/api';
+
+interface DummyItem {
+ id: number;
+}
+
+interface DummyResponse {
+ results: DummyItem[];
+ next?: string;
+}
+
+const createWrapper = () => {
+ const queryClient = new QueryClient();
+ return ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+};
+
+describe('helpers', () => {
+ it('fetches and paginates correctly', async () => {
+ const mockAPI = jest
+ .fn, [{ page: number; query: string }]>()
+ .mockResolvedValueOnce({
+ results: [{ id: 1 }],
+ next: 'url?page=2',
+ })
+ .mockResolvedValueOnce({
+ results: [{ id: 2 }],
+ next: undefined,
+ });
+
+ const { result } = renderHook(
+ () => useAPIInfiniteQuery('test-key', mockAPI, { query: 'test' }),
+ { wrapper: createWrapper() },
+ );
+
+ // Wait for first page
+ await waitFor(() => {
+ expect(result.current.data?.pages[0].results[0].id).toBe(1);
+ });
+
+ // Fetch next page
+ await result.current.fetchNextPage();
+
+ await waitFor(() => {
+ expect(result.current.data?.pages.length).toBe(2);
+ });
+
+ await waitFor(() => {
+ expect(result.current.data?.pages[1].results[0].id).toBe(2);
+ });
+
+ expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 1 });
+ expect(mockAPI).toHaveBeenCalledWith({ query: 'test', page: 2 });
+ });
+});
diff --git a/src/frontend/apps/impress/src/api/__tests__/utils.test.ts b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts
new file mode 100644
index 00000000..86433188
--- /dev/null
+++ b/src/frontend/apps/impress/src/api/__tests__/utils.test.ts
@@ -0,0 +1,57 @@
+import { errorCauses, getCSRFToken } from '@/api';
+
+describe('utils', () => {
+ describe('errorCauses', () => {
+ const createMockResponse = (jsonData: any, status = 400): Response => {
+ return {
+ status,
+ json: () => jsonData,
+ } as unknown as Response;
+ };
+
+ it('parses multiple string causes from error body', async () => {
+ const mockResponse = createMockResponse(
+ {
+ field: ['error message 1', 'error message 2'],
+ },
+ 400,
+ );
+
+ const result = await errorCauses(mockResponse, { context: 'login' });
+
+ expect(result.status).toBe(400);
+ expect(result.cause).toEqual(['error message 1', 'error message 2']);
+ expect(result.data).toEqual({ context: 'login' });
+ });
+
+ it('returns undefined causes if no JSON body', async () => {
+ const mockResponse = createMockResponse(null, 500);
+
+ const result = await errorCauses(mockResponse);
+
+ expect(result.status).toBe(500);
+ expect(result.cause).toBeUndefined();
+ expect(result.data).toBeUndefined();
+ });
+ });
+
+ describe('getCSRFToken', () => {
+ it('extracts csrftoken from document.cookie', () => {
+ Object.defineProperty(document, 'cookie', {
+ writable: true,
+ value: 'sessionid=xyz; csrftoken=abc123; theme=dark',
+ });
+
+ expect(getCSRFToken()).toBe('abc123');
+ });
+
+ it('returns undefined if csrftoken is not present', () => {
+ Object.defineProperty(document, 'cookie', {
+ writable: true,
+ value: 'sessionid=xyz; theme=dark',
+ });
+
+ expect(getCSRFToken()).toBeUndefined();
+ });
+ });
+});