Files
tts-workshop/apps/proxy/src/routes/trace.test.ts
T
hypercross 9094ad58e0 feat(proxy): add offset param to inset or outset traced shapes
Trace results can now be inset (negative) or outset (positive) by a
pixel amount. Offset the outline and holes in opposite directions with
clipper-lib's miter joins, then recombine with a boolean difference so
holes grow on inset and shrink on outset. Collapsed shapes return an
empty outline; a split outline keeps the largest ring. Document the
parameter and the new dependency.
2026-08-08 15:37:27 +08:00

147 lines
4.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { deserialize } from 'bson';
import sharp from 'sharp';
import trace from './trace.js';
afterEach(() => {
vi.unstubAllGlobals();
});
/** Build a small PNG with an opaque circle in the center on a transparent bg. */
async function makePng(size = 20): Promise<Buffer> {
const rgba = Buffer.alloc(size * size * 4);
const cx = size / 2;
const cy = size / 2;
const r = size / 3;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
if (Math.hypot(x - cx, y - cy) <= r) {
const i = (y * size + x) * 4;
rgba[i] = 255;
rgba[i + 1] = 255;
rgba[i + 2] = 255;
rgba[i + 3] = 255;
}
}
}
return sharp(rgba, { raw: { width: size, height: size, channels: 4 } })
.png()
.toBuffer();
}
function stubFetch(body: Buffer) {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(new Uint8Array(body), {
headers: { 'content-type': 'image/png' },
}),
),
);
}
describe('trace route', () => {
it('rejects a missing url', async () => {
const res = await trace.request('/');
expect(res.status).toBe(400);
expect(await res.json()).toEqual({ error: 'Required' });
});
it('rejects a non-http url', async () => {
const res = await trace.request('/?url=file%3A%2F%2F%2Fetc%2Fpasswd');
expect(res.status).toBe(400);
});
it('returns 502 when the upstream fetch fails', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(new Response('error', { status: 500 })),
);
const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
expect(res.status).toBe(502);
});
it('returns a BSON shape by default', async () => {
stubFetch(await makePng());
const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe('application/octet-stream');
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.format).toBe('shape');
expect(result.mode).toBe('alpha');
expect(result.width).toBe(20);
expect(result.height).toBe(20);
expect(typeof result.svg).toBe('string');
expect(result.svg).toContain('<svg');
expect(result.shape).toBeDefined();
expect(result.shape.outline.length).toBeGreaterThan(3);
});
it('returns a raw SVG when format=svg', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&format=svg',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.format).toBe('svg');
expect(result.shape).toBeUndefined();
expect(typeof result.svg).toBe('string');
});
it('supports bw mode', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&mode=bw',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.mode).toBe('bw');
expect(result.shape.outline.length).toBeGreaterThan(3);
});
it('applies an inset offset to the shape', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=-2',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBe(-2);
expect(result.shape.outline.length).toBeGreaterThan(3);
// An inset of 2px on a 20px circle must be visibly smaller.
const extent = (pts: number[][]) => [
Math.min(...pts.map((p) => p[0]!)),
Math.max(...pts.map((p) => p[0]!)),
];
const [minX, maxX] = extent(result.shape.outline);
expect((maxX ?? 0) - (minX ?? 0)).toBeLessThan(20);
});
it('applies an outset offset to the shape', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=2',
);
expect(res.status).toBe(200);
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBe(2);
expect(result.shape.outline.length).toBeGreaterThan(3);
});
it('rejects a non-numeric offset', async () => {
stubFetch(await makePng());
const res = await trace.request(
'/?url=https%3A%2F%2Fexample.com%2Fa.png&offset=abc',
);
expect(res.status).toBe(400);
});
it('omits offset when not requested', async () => {
stubFetch(await makePng());
const res = await trace.request('/?url=https%3A%2F%2Fexample.com%2Fa.png');
const result = deserialize(new Uint8Array(await res.arrayBuffer()));
expect(result.offset).toBeUndefined();
});
});