128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
DASHCAM_COORDINATES,
|
|
POSTCARD_HEIGHT,
|
|
POSTCARD_WIDTH,
|
|
coverCrop,
|
|
drawPostcard,
|
|
postcardLayout,
|
|
type DashcamTelemetry,
|
|
type PostcardContext,
|
|
} from '@/lib/dashcam-postcard';
|
|
|
|
const telemetry: DashcamTelemetry = {
|
|
sectionId: 'infrastructure',
|
|
sectionLabel: 'Infrastructure',
|
|
chapter: 'pre-dawn',
|
|
exit: '04',
|
|
routePercent: 47,
|
|
clock: '03:51',
|
|
name: 'Luca Pîslaru',
|
|
city: 'Bucharest',
|
|
};
|
|
|
|
interface TextCall { text: string; x: number; y: number; align: CanvasTextAlign; font: string }
|
|
|
|
function stubContext() {
|
|
const texts: TextCall[] = [];
|
|
const images: number[][] = [];
|
|
const ctx = {
|
|
fillStyle: '' as unknown,
|
|
strokeStyle: '' as unknown,
|
|
lineWidth: 1,
|
|
font: '',
|
|
textAlign: 'start' as CanvasTextAlign,
|
|
textBaseline: 'alphabetic' as CanvasTextBaseline,
|
|
drawImage: vi.fn((_source: unknown, ...rest: number[]) => { images.push(rest); }),
|
|
fillRect: vi.fn(),
|
|
fillText: vi.fn((text: string, x: number, y: number) => {
|
|
texts.push({ text, x, y, align: ctx.textAlign, font: ctx.font });
|
|
}),
|
|
beginPath: vi.fn(),
|
|
moveTo: vi.fn(),
|
|
lineTo: vi.fn(),
|
|
stroke: vi.fn(),
|
|
arc: vi.fn(),
|
|
fill: vi.fn(),
|
|
createLinearGradient: vi.fn(() => ({ addColorStop: vi.fn() })),
|
|
save: vi.fn(),
|
|
restore: vi.fn(),
|
|
};
|
|
return { ctx: ctx as unknown as PostcardContext & typeof ctx, texts, images };
|
|
}
|
|
|
|
const source = (width: number, height: number) => ({ width, height }) as unknown as HTMLCanvasElement;
|
|
|
|
describe('coverCrop', () => {
|
|
it('crops a wider source to the 16:9 centre', () => {
|
|
expect(coverCrop(2400, 900, 1600, 900)).toEqual({ sx: 400, sy: 0, sw: 1600, sh: 900 });
|
|
});
|
|
|
|
it('crops a taller source to the 16:9 centre', () => {
|
|
const crop = coverCrop(1440, 1080, 1600, 900);
|
|
expect(crop.sx).toBe(0);
|
|
expect(crop.sw).toBe(1440);
|
|
expect(crop.sh).toBeCloseTo(810, 6);
|
|
expect(crop.sy).toBeCloseTo(135, 6);
|
|
});
|
|
|
|
it('keeps an exact-ratio source whole and tolerates an empty one', () => {
|
|
expect(coverCrop(3200, 1800, 1600, 900)).toEqual({ sx: 0, sy: 0, sw: 3200, sh: 1800 });
|
|
expect(coverCrop(0, 0, 1600, 900)).toEqual({ sx: 0, sy: 0, sw: 0, sh: 0 });
|
|
});
|
|
});
|
|
|
|
describe('postcardLayout', () => {
|
|
it('keeps every plate inside the frame at 1600 x 900', () => {
|
|
const l = postcardLayout();
|
|
expect(l.inset).toBe(44);
|
|
expect(l.left).toBeGreaterThan(l.inset);
|
|
expect(l.right).toBeLessThan(POSTCARD_WIDTH - l.inset);
|
|
expect(l.top - l.font).toBeGreaterThan(l.inset);
|
|
expect(l.bottom - l.lineGap - l.font).toBeGreaterThan(POSTCARD_HEIGHT / 2);
|
|
expect(l.bottom).toBeLessThan(POSTCARD_HEIGHT - l.inset);
|
|
expect(l.credit).toBeGreaterThan(l.bottom);
|
|
expect(l.credit).toBeLessThan(POSTCARD_HEIGHT);
|
|
});
|
|
|
|
it('scales with the smaller axis', () => {
|
|
expect(postcardLayout(800, 450).inset).toBe(22);
|
|
expect(postcardLayout(800, 900).inset).toBe(22);
|
|
});
|
|
});
|
|
|
|
describe('drawPostcard', () => {
|
|
it('covers the frame with the scene and writes the telemetry and credit', () => {
|
|
const { ctx, texts, images } = stubContext();
|
|
drawPostcard(ctx, source(2880, 1800), telemetry);
|
|
const l = postcardLayout();
|
|
|
|
expect(images).toEqual([[0, 90, 2880, 1620, 0, 0, POSTCARD_WIDTH, POSTCARD_HEIGHT]]);
|
|
expect(texts.map((call) => call.text)).toEqual([
|
|
'REC DASHCAM · PRE DAWN · EXIT 04',
|
|
'ROUTE 47% · INFRASTRUCTURE',
|
|
'03:51',
|
|
'LUCA PÎSLARU',
|
|
DASHCAM_COORDINATES,
|
|
'NIGHT SHIFT · LUCA PÎSLARU · BUCHAREST',
|
|
]);
|
|
const byText = Object.fromEntries(texts.map((call) => [call.text, call]));
|
|
expect(byText['ROUTE 47% · INFRASTRUCTURE']).toMatchObject({ x: l.left, y: l.bottom, align: 'left' });
|
|
expect(byText['03:51']).toMatchObject({ x: l.right, y: l.top, align: 'right' });
|
|
expect(byText[DASHCAM_COORDINATES]).toMatchObject({ x: l.right, y: l.bottom - l.lineGap, align: 'right' });
|
|
expect(byText['NIGHT SHIFT · LUCA PÎSLARU · BUCHAREST']).toMatchObject({ x: 800, y: l.credit, align: 'center' });
|
|
expect(texts.every((call) => call.font.includes('Geist Mono'))).toBe(true);
|
|
// Four corner brackets.
|
|
expect(ctx.stroke).toHaveBeenCalledTimes(4);
|
|
expect(ctx.save).toHaveBeenCalledTimes(1);
|
|
expect(ctx.restore).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('still draws the chrome over an empty source', () => {
|
|
const { ctx, texts, images } = stubContext();
|
|
drawPostcard(ctx, source(0, 0), telemetry);
|
|
expect(images).toEqual([]);
|
|
expect(texts).toHaveLength(6);
|
|
});
|
|
});
|