diff --git a/frontend/README.md b/frontend/README.md index 3ce37bd098..c529053d80 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -68,6 +68,8 @@ npm run preview ### Running Tests +> **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. + #### All Tests To run both unit and integration tests together: diff --git a/frontend/src/lib/api/api.test.ts b/frontend/src/lib/api/api.test.ts new file mode 100644 index 0000000000..f5dc4d4323 --- /dev/null +++ b/frontend/src/lib/api/api.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { get } from './api'; +import { error } from '@sveltejs/kit'; + +// Mock the fetch function +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +// Mock the error function from @sveltejs/kit +vi.mock('@sveltejs/kit', () => ({ + error: vi.fn() +})); + +describe('API module', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('get function', () => { + it('should make a GET request and return parsed JSON data', async () => { + const mockResponse = { data: 'test data' }; + mockFetch.mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(JSON.stringify(mockResponse)) + }); + + const result = await get('test-path'); + + expect(mockFetch).toHaveBeenCalledWith( + `${import.meta.env.VITE_API_BASE_URL}/api/v1/test-path`, + { method: 'GET', headers: {} } + ); + expect(result).toEqual(mockResponse); + }); + + it('should return an empty object if the response is empty', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve('') + }); + + const result = await get('empty-path'); + + expect(result).toEqual({}); + }); + + it('should throw an error if the response is not ok', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404 + }); + + await get('error-path'); + + expect(error).toHaveBeenCalledWith(404); + }); + }); +}); diff --git a/frontend/src/lib/types/Model/Model.test.ts b/frontend/src/lib/types/Model/Model.test.ts new file mode 100644 index 0000000000..8e93d6bd31 --- /dev/null +++ b/frontend/src/lib/types/Model/Model.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import * as api from '$lib/api/api'; +import type { ModelsResponse, TimeSeriesResponse, Model } from '$lib/types/Model/Model'; + +describe('Model types', () => { + it('should match ModelsResponse type', async () => { + const result = (await api.get('models')) as ModelsResponse; + + const expectedKeys = ['offset', 'items']; + expect(Object.keys(result)).toEqual(expect.arrayContaining(expectedKeys)); + + // Log a warning if there are additional fields + const additionalKeys = Object.keys(result).filter((key) => !expectedKeys.includes(key)); + if (additionalKeys.length > 0) { + console.warn(`Additional fields found in ModelsResponse: ${additionalKeys.join(', ')}`); + } + + expect(Array.isArray(result.items)).toBe(true); + + if (result.items.length > 0) { + const model = result.items[0]; + const expectedModelKeys: (keyof Model)[] = [ + 'name', + 'id', + 'online', + 'production', + 'team', + 'modelType', + 'createTime', + 'lastUpdated' + ]; + expect(Object.keys(model)).toEqual(expect.arrayContaining(expectedModelKeys)); + + // Log a warning if there are additional fields + const additionalModelKeys = Object.keys(model).filter( + (key) => !expectedModelKeys.includes(key as keyof Model) + ); + if (additionalModelKeys.length > 0) { + console.warn(`Additional fields found in Model: ${additionalModelKeys.join(', ')}`); + } + } + }); + + it('should match TimeSeriesResponse type', async () => { + const modelId = '0'; + const result = (await api.get( + `model/${modelId}/timeseries?startTs=1725926400000&endTs=1726106400000&offset=10h&algorithm=psi` + )) as TimeSeriesResponse; + + const expectedKeys = ['id', 'items']; + expect(Object.keys(result)).toEqual(expect.arrayContaining(expectedKeys)); + + // Log a warning if there are additional fields + const additionalKeys = Object.keys(result).filter((key) => !expectedKeys.includes(key)); + if (additionalKeys.length > 0) { + console.warn(`Additional fields found in TimeSeriesResponse: ${additionalKeys.join(', ')}`); + } + + expect(Array.isArray(result.items)).toBe(true); + + if (result.items.length > 0) { + const item = result.items[0]; + const expectedItemKeys = ['value', 'ts', 'label']; + expect(Object.keys(item)).toEqual(expect.arrayContaining(expectedItemKeys)); + + // Log a warning if there are additional fields + const additionalItemKeys = Object.keys(item).filter((key) => !expectedItemKeys.includes(key)); + if (additionalItemKeys.length > 0) { + console.warn( + `Additional fields found in TimeSeriesResponse item: ${additionalItemKeys.join(', ')}` + ); + } + } + }); +}); diff --git a/frontend/src/lib/types/Model.ts b/frontend/src/lib/types/Model/Model.ts similarity index 100% rename from frontend/src/lib/types/Model.ts rename to frontend/src/lib/types/Model/Model.ts diff --git a/frontend/src/routes/models/+page.server.ts b/frontend/src/routes/models/+page.server.ts index c686a4a9db..7740a05d27 100644 --- a/frontend/src/routes/models/+page.server.ts +++ b/frontend/src/routes/models/+page.server.ts @@ -1,5 +1,5 @@ import type { PageServerLoad } from './$types'; -import type { ModelsResponse } from '$lib/types/Model'; +import type { ModelsResponse } from '$lib/types/Model/Model'; import * as api from '$lib/api/api'; export const load: PageServerLoad = async (): Promise<{ models: ModelsResponse }> => { diff --git a/frontend/src/routes/models/+page.svelte b/frontend/src/routes/models/+page.svelte index e4a8142b5d..d9b701d120 100644 --- a/frontend/src/routes/models/+page.svelte +++ b/frontend/src/routes/models/+page.svelte @@ -1,5 +1,5 @@