Skip to content

Commit d998f8d

Browse files
Frontend tests (#32)
## Summary Added unit tests to ensure that our core API implementation is working, and to ensure type consistency with the backend. If the backend changes/removes any existing fields, then the frontend tests will fail. If the backend adds an additional field, the tests will succeed but show a warning in the console. First, start the backend: `sbt "project hub" run` Next, run your unit tests: `npm run test:unit` They will automatically re-run if you change the testing code. When we need CI/CD we can start the backend before running tests. ## Checklist - [x] Added Unit Tests - [x] Covered by existing CI - [ ] Integration tested - [x] Documentation update <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Added a "Running Tests" section in the README to guide users on executing tests, including backend startup requirements. - Introduced unit tests for API functions and model response structures to ensure data integrity. - **Bug Fixes** - Updated import paths for type definitions in various files to improve organization without affecting functionality. - **Chores** - Removed an obsolete unit test file. - Modified the test configuration to include all test files across the source directory for better test coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Piyush Narang <[email protected]>
1 parent d165191 commit d998f8d

File tree

9 files changed

+143
-11
lines changed

9 files changed

+143
-11
lines changed

frontend/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ npm run preview
6868

6969
### Running Tests
7070

71+
> **Important:** You must start the backend before running tests. In the future, we can add scripts to automatically start the backend before any tests are run.
72+
7173
#### All Tests
7274

7375
To run both unit and integration tests together:

frontend/src/lib/api/api.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { get } from './api';
3+
import { error } from '@sveltejs/kit';
4+
5+
// Mock the fetch function
6+
const mockFetch = vi.fn();
7+
global.fetch = mockFetch;
8+
9+
// Mock the error function from @sveltejs/kit
10+
vi.mock('@sveltejs/kit', () => ({
11+
error: vi.fn()
12+
}));
13+
14+
describe('API module', () => {
15+
beforeEach(() => {
16+
vi.resetAllMocks();
17+
});
18+
19+
afterEach(() => {
20+
vi.clearAllMocks();
21+
});
22+
23+
describe('get function', () => {
24+
it('should make a GET request and return parsed JSON data', async () => {
25+
const mockResponse = { data: 'test data' };
26+
mockFetch.mockResolvedValueOnce({
27+
ok: true,
28+
text: () => Promise.resolve(JSON.stringify(mockResponse))
29+
});
30+
31+
const result = await get('test-path');
32+
33+
expect(mockFetch).toHaveBeenCalledWith(
34+
`${import.meta.env.VITE_API_BASE_URL}/api/v1/test-path`,
35+
{ method: 'GET', headers: {} }
36+
);
37+
expect(result).toEqual(mockResponse);
38+
});
39+
40+
it('should return an empty object if the response is empty', async () => {
41+
mockFetch.mockResolvedValueOnce({
42+
ok: true,
43+
text: () => Promise.resolve('')
44+
});
45+
46+
const result = await get('empty-path');
47+
48+
expect(result).toEqual({});
49+
});
50+
51+
it('should throw an error if the response is not ok', async () => {
52+
mockFetch.mockResolvedValueOnce({
53+
ok: false,
54+
status: 404
55+
});
56+
57+
await get('error-path');
58+
59+
expect(error).toHaveBeenCalledWith(404);
60+
});
61+
});
62+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, it, expect } from 'vitest';
2+
import * as api from '$lib/api/api';
3+
import type { ModelsResponse, TimeSeriesResponse, Model } from '$lib/types/Model/Model';
4+
5+
describe('Model types', () => {
6+
it('should match ModelsResponse type', async () => {
7+
const result = (await api.get('models')) as ModelsResponse;
8+
9+
const expectedKeys = ['offset', 'items'];
10+
expect(Object.keys(result)).toEqual(expect.arrayContaining(expectedKeys));
11+
12+
// Log a warning if there are additional fields
13+
const additionalKeys = Object.keys(result).filter((key) => !expectedKeys.includes(key));
14+
if (additionalKeys.length > 0) {
15+
console.warn(`Additional fields found in ModelsResponse: ${additionalKeys.join(', ')}`);
16+
}
17+
18+
expect(Array.isArray(result.items)).toBe(true);
19+
20+
if (result.items.length > 0) {
21+
const model = result.items[0];
22+
const expectedModelKeys: (keyof Model)[] = [
23+
'name',
24+
'id',
25+
'online',
26+
'production',
27+
'team',
28+
'modelType',
29+
'createTime',
30+
'lastUpdated'
31+
];
32+
expect(Object.keys(model)).toEqual(expect.arrayContaining(expectedModelKeys));
33+
34+
// Log a warning if there are additional fields
35+
const additionalModelKeys = Object.keys(model).filter(
36+
(key) => !expectedModelKeys.includes(key as keyof Model)
37+
);
38+
if (additionalModelKeys.length > 0) {
39+
console.warn(`Additional fields found in Model: ${additionalModelKeys.join(', ')}`);
40+
}
41+
}
42+
});
43+
44+
it('should match TimeSeriesResponse type', async () => {
45+
const modelId = '0';
46+
const result = (await api.get(
47+
`model/${modelId}/timeseries?startTs=1725926400000&endTs=1726106400000&offset=10h&algorithm=psi`
48+
)) as TimeSeriesResponse;
49+
50+
const expectedKeys = ['id', 'items'];
51+
expect(Object.keys(result)).toEqual(expect.arrayContaining(expectedKeys));
52+
53+
// Log a warning if there are additional fields
54+
const additionalKeys = Object.keys(result).filter((key) => !expectedKeys.includes(key));
55+
if (additionalKeys.length > 0) {
56+
console.warn(`Additional fields found in TimeSeriesResponse: ${additionalKeys.join(', ')}`);
57+
}
58+
59+
expect(Array.isArray(result.items)).toBe(true);
60+
61+
if (result.items.length > 0) {
62+
const item = result.items[0];
63+
const expectedItemKeys = ['value', 'ts', 'label'];
64+
expect(Object.keys(item)).toEqual(expect.arrayContaining(expectedItemKeys));
65+
66+
// Log a warning if there are additional fields
67+
const additionalItemKeys = Object.keys(item).filter((key) => !expectedItemKeys.includes(key));
68+
if (additionalItemKeys.length > 0) {
69+
console.warn(
70+
`Additional fields found in TimeSeriesResponse item: ${additionalItemKeys.join(', ')}`
71+
);
72+
}
73+
}
74+
});
75+
});

frontend/src/routes/models/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { PageServerLoad } from './$types';
2-
import type { ModelsResponse } from '$lib/types/Model';
2+
import type { ModelsResponse } from '$lib/types/Model/Model';
33
import * as api from '$lib/api/api';
44

55
export const load: PageServerLoad = async (): Promise<{ models: ModelsResponse }> => {

frontend/src/routes/models/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import type { Model } from '$lib/types/Model.js';
2+
import type { Model } from '$lib/types/Model/Model';
33
import {
44
Table,
55
TableBody,

frontend/src/routes/models/[slug]/observability/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PageServerLoad } from './$types';
22
import * as api from '$lib/api/api';
3-
import type { TimeSeriesResponse } from '$lib/types/Model';
3+
import type { TimeSeriesResponse } from '$lib/types/Model/Model';
44

55
export const load: PageServerLoad = async ({
66
params

frontend/src/test/unit/index.test.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

frontend/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { defineConfig } from 'vitest/config';
44
export default defineConfig({
55
plugins: [sveltekit()],
66
test: {
7-
include: ['./src/test/unit/**/*.{test,spec}.{js,ts}']
7+
include: ['./src/**/*.{test,spec}.{js,ts}']
88
}
99
});

0 commit comments

Comments
 (0)