kerf-engine/app/src/api/__tests__/engine.test.ts
jlightner 9be90a4494 test: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with…
- "engine/main.py"
- "app/vite.config.ts"
- "app/src/types/engine.ts"
- "app/src/api/engine.ts"
- "app/src/api/__tests__/engine.test.ts"
- "app/src/App.tsx"
- "app/src/test-setup.ts"
- "app/tsconfig.app.json"

GSD-Task: S01/T01
2026-03-26 05:05:31 +00:00

149 lines
5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getPresets, traceImage, simplifyVector } from '../engine';
// ---------- helpers ----------
function mockFetchOk(body: unknown) {
return vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(body),
text: () => Promise.resolve(JSON.stringify(body)),
});
}
function mockFetchFail(status: number, statusText: string) {
return vi.fn().mockResolvedValue({
ok: false,
status,
statusText,
text: () => Promise.resolve(statusText),
});
}
// ---------- setup ----------
const originalFetch = globalThis.fetch;
beforeEach(() => {
// reset to a no-op fetch so tests that forget to mock still fail clearly
globalThis.fetch = vi.fn().mockRejectedValue(new Error('fetch not mocked'));
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
// ---------- getPresets ----------
describe('getPresets', () => {
it('calls GET /engine/presets and returns parsed JSON', async () => {
const payload = { presets: { sign: { name: 'sign' } } };
globalThis.fetch = mockFetchOk(payload);
const result = await getPresets();
expect(globalThis.fetch).toHaveBeenCalledOnce();
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toBe('/engine/presets');
expect(opts?.signal).toBeUndefined();
expect(result).toEqual(payload);
});
it('passes AbortSignal when provided', async () => {
globalThis.fetch = mockFetchOk({ presets: {} });
const controller = new AbortController();
await getPresets(controller.signal);
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(opts.signal).toBe(controller.signal);
});
it('throws on non-ok response', async () => {
globalThis.fetch = mockFetchFail(500, 'Internal Server Error');
await expect(getPresets()).rejects.toThrow(/failed.*500/i);
});
});
// ---------- traceImage ----------
describe('traceImage', () => {
it('sends FormData with file, preset, output_format, and JSON params', async () => {
globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });
const file = new File(['pixels'], 'test.png', { type: 'image/png' });
const params = { epsilon: 2.5, turdsize: 10 };
await traceImage(file, 'sign', params);
expect(globalThis.fetch).toHaveBeenCalledOnce();
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toBe('/engine/trace');
expect(opts.method).toBe('POST');
const body: FormData = opts.body;
expect(body).toBeInstanceOf(FormData);
expect(body.get('file')).toBeInstanceOf(File);
expect(body.get('preset')).toBe('sign');
expect(body.get('output_format')).toBe('svg');
expect(body.get('params')).toBe(JSON.stringify(params));
});
it('passes AbortSignal when provided', async () => {
globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
const controller = new AbortController();
const file = new File(['px'], 'img.png', { type: 'image/png' });
await traceImage(file, 'sign', {}, controller.signal);
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(opts.signal).toBe(controller.signal);
});
it('throws with detail on non-ok response', async () => {
globalThis.fetch = mockFetchFail(422, 'Unprocessable Entity');
const file = new File(['px'], 'bad.png', { type: 'image/png' });
await expect(traceImage(file, 'sign', {})).rejects.toThrow(/failed.*422/i);
});
});
// ---------- simplifyVector ----------
describe('simplifyVector', () => {
it('sends FormData with file, epsilon, and output_format', async () => {
globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
await simplifyVector(file, 3.0);
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(url).toBe('/engine/simplify');
expect(opts.method).toBe('POST');
const body: FormData = opts.body;
expect(body.get('file')).toBeInstanceOf(File);
expect(body.get('epsilon')).toBe('3');
expect(body.get('output_format')).toBe('svg');
});
it('passes AbortSignal when provided', async () => {
globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
const controller = new AbortController();
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
await simplifyVector(file, 1.0, controller.signal);
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
expect(opts.signal).toBe(controller.signal);
});
it('throws on non-ok response', async () => {
globalThis.fetch = mockFetchFail(400, 'Bad Request');
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
await expect(simplifyVector(file, 1.0)).rejects.toThrow(/failed.*400/i);
});
});