- "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
149 lines
5 KiB
TypeScript
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);
|
|
});
|
|
});
|