Spaces:
Running
Running
| // Unit tests for frontend/src/api/client.ts URL composition + error handling. | |
| // Runs under node:test with a synthetic fetch mock so no backend is needed. | |
| import { test, mock } from 'node:test'; | |
| import assert from 'node:assert/strict'; | |
| // bun/node strip .ts extension when type='module' is set in package.json; | |
| // without that we load via bun's loader by requesting the .ts path. | |
| const clientPath = new URL('../../frontend/src/api/client.ts', import.meta.url).pathname; | |
| const { API, apiUrl, apiFetch, apiJson, apiPost, ApiError } = await import(clientPath); | |
| test('apiUrl falls back to API root on empty input', () => { | |
| assert.equal(apiUrl(), API); | |
| assert.equal(apiUrl(''), API); | |
| }); | |
| test('apiUrl prepends slash when missing', () => { | |
| assert.equal(apiUrl('engines'), `${API}/engines`); | |
| assert.equal(apiUrl('/engines'), `${API}/engines`); | |
| }); | |
| test('apiUrl passes absolute URLs through untouched', () => { | |
| assert.equal(apiUrl('https://example.com/foo'), 'https://example.com/foo'); | |
| assert.equal(apiUrl('http://localhost:9000/bar'), 'http://localhost:9000/bar'); | |
| }); | |
| test('ApiError carries status + detail', () => { | |
| const err = new ApiError('boom', { status: 503, detail: { code: 'x' } }); | |
| assert.equal(err.name, 'ApiError'); | |
| assert.equal(err.message, 'boom'); | |
| assert.equal(err.status, 503); | |
| assert.deepEqual(err.detail, { code: 'x' }); | |
| }); | |
| test('apiFetch resolves on 2xx', async () => { | |
| const originalFetch = globalThis.fetch; | |
| globalThis.fetch = mock.fn(async () => new Response('ok', { status: 200 })); | |
| try { | |
| const res = await apiFetch('/ping'); | |
| assert.equal(res.status, 200); | |
| } finally { | |
| globalThis.fetch = originalFetch; | |
| } | |
| }); | |
| test('apiFetch throws ApiError with JSON detail on non-2xx', async () => { | |
| const originalFetch = globalThis.fetch; | |
| globalThis.fetch = mock.fn(async () => | |
| new Response(JSON.stringify({ detail: 'Job not found' }), { | |
| status: 404, statusText: 'Not Found', | |
| headers: { 'Content-Type': 'application/json' }, | |
| }), | |
| ); | |
| try { | |
| await assert.rejects( | |
| () => apiFetch('/dub/x'), | |
| (err) => { | |
| assert.ok(err instanceof ApiError); | |
| assert.equal(err.status, 404); | |
| assert.equal(err.detail, 'Job not found'); | |
| assert.match(err.message, /404/); | |
| return true; | |
| }, | |
| ); | |
| } finally { | |
| globalThis.fetch = originalFetch; | |
| } | |
| }); | |
| test('apiJson parses 2xx body', async () => { | |
| const originalFetch = globalThis.fetch; | |
| globalThis.fetch = mock.fn(async () => | |
| new Response(JSON.stringify({ ok: true, n: 42 }), { | |
| status: 200, headers: { 'Content-Type': 'application/json' }, | |
| }), | |
| ); | |
| try { | |
| const body = await apiJson('/ping'); | |
| assert.deepEqual(body, { ok: true, n: 42 }); | |
| } finally { | |
| globalThis.fetch = originalFetch; | |
| } | |
| }); | |
| test('apiPost json body sets Content-Type + stringified body', async () => { | |
| const originalFetch = globalThis.fetch; | |
| const calls = []; | |
| globalThis.fetch = mock.fn(async (url, init) => { | |
| calls.push({ url, init }); | |
| return new Response(JSON.stringify({ received: true }), { | |
| status: 200, headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| }); | |
| try { | |
| await apiPost('/models/install', { repo_id: 'k2-fsa/OmniVoice' }); | |
| assert.equal(calls.length, 1); | |
| const { init } = calls[0]; | |
| assert.equal(init.method, 'POST'); | |
| assert.equal(init.headers['Content-Type'], 'application/json'); | |
| assert.equal(init.body, JSON.stringify({ repo_id: 'k2-fsa/OmniVoice' })); | |
| } finally { | |
| globalThis.fetch = originalFetch; | |
| } | |
| }); | |
| test('apiPost passes FormData without stringify + no Content-Type override', async () => { | |
| const originalFetch = globalThis.fetch; | |
| const calls = []; | |
| globalThis.fetch = mock.fn(async (url, init) => { | |
| calls.push({ url, init }); | |
| return new Response(JSON.stringify({}), { status: 200 }); | |
| }); | |
| try { | |
| const fd = new FormData(); | |
| fd.append('text', 'hello'); | |
| await apiPost('/generate', fd); | |
| assert.equal(calls[0].init.body, fd); | |
| // Browser sets multipart boundary; we must NOT force a JSON header. | |
| assert.equal(calls[0].init.headers, undefined); | |
| } finally { | |
| globalThis.fetch = originalFetch; | |
| } | |
| }); | |