feat: living tab title and favicon follow the drive
This commit is contained in:
115
tests/unit/tab-feed.test.ts
Normal file
115
tests/unit/tab-feed.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatTabTitle, quantiseRoute } from '@/journey/tab-feed';
|
||||
import { renderTabIcon, tabIconHref, TAB_ICON } from '@/journey/tab-icon';
|
||||
import { validateProductionInputs } from '@/content/schemas';
|
||||
import { render } from '@/entry-server';
|
||||
|
||||
const base = { routeProgress: 0.47, label: 'Infrastructure', arrived: false, parked: false, dashcam: false };
|
||||
|
||||
describe('formatTabTitle', () => {
|
||||
it('reads the route in 5% steps with an uppercase chapter', () => {
|
||||
expect(formatTabTitle(base)).toBe('ROUTE 45% · INFRASTRUCTURE');
|
||||
expect(formatTabTitle({ ...base, routeProgress: 0.48 })).toBe('ROUTE 50% · INFRASTRUCTURE');
|
||||
expect(formatTabTitle({ ...base, routeProgress: 0, label: 'Start' })).toBe('ROUTE 0% · START');
|
||||
expect(formatTabTitle({ ...base, label: ' Selected builds ' })).toBe('ROUTE 45% · SELECTED BUILDS');
|
||||
});
|
||||
|
||||
it('clamps and survives non-finite progress', () => {
|
||||
expect(formatTabTitle({ ...base, routeProgress: 1.4 })).toBe('ROUTE 100% · INFRASTRUCTURE');
|
||||
expect(formatTabTitle({ ...base, routeProgress: -1 })).toBe('ROUTE 0% · INFRASTRUCTURE');
|
||||
expect(formatTabTitle({ ...base, routeProgress: Number.NaN })).toBe('ROUTE 0% · INFRASTRUCTURE');
|
||||
});
|
||||
|
||||
it('parks, arrives, and yields to the dashcam', () => {
|
||||
expect(formatTabTitle({ ...base, parked: true })).toBe('PARKED · 45% · INFRASTRUCTURE');
|
||||
expect(formatTabTitle({ ...base, arrived: true, label: 'Contact' })).toBe('ARRIVED · CONTACT');
|
||||
expect(formatTabTitle({ ...base, arrived: true, parked: true, label: 'Contact' })).toBe('PARKED · 100% · CONTACT');
|
||||
expect(formatTabTitle({ ...base, dashcam: true })).toBeNull();
|
||||
expect(formatTabTitle({ ...base, dashcam: true, parked: true, arrived: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('quantises to stable steps', () => {
|
||||
expect(quantiseRoute(0.024)).toBe(0);
|
||||
expect(quantiseRoute(0.026)).toBe(0.05);
|
||||
expect(new Set([0.5, 0.51, 0.52, 0.49].map((r) => formatTabTitle({ ...base, routeProgress: r }))).size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
const beamLengths = (svg: string): number[] =>
|
||||
[...svg.matchAll(/<path d='M16 \d+L([\d.]+) /gu)].map((m) => Number(m[1]) - 16);
|
||||
const opacities = (svg: string): number[] => [...svg.matchAll(/fill-opacity='([\d.]+)'/gu)].map((m) => Number(m[1]));
|
||||
|
||||
describe('renderTabIcon', () => {
|
||||
const icon = { route: 0, accent: '#8be9ed', parked: false, dawn: false };
|
||||
|
||||
it('keeps the dark plate and the L glyph', () => {
|
||||
const svg = renderTabIcon(icon);
|
||||
expect(svg.startsWith("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>")).toBe(true);
|
||||
expect(svg).toContain(`fill='${TAB_ICON.plate}'`);
|
||||
expect(svg).toContain(`<path d='M11 7v18h11' fill='none' stroke='${TAB_ICON.glyph}' stroke-width='4'/>`);
|
||||
expect(svg).not.toMatch(/["\n]/u);
|
||||
});
|
||||
|
||||
it('lengthens and brightens two beams with the route, in the accent', () => {
|
||||
const at = (route: number) => renderTabIcon({ ...icon, route });
|
||||
const [l0, l50, l100] = [at(0), at(0.5), at(1)].map(beamLengths);
|
||||
expect(l0).toEqual([4, 4]);
|
||||
expect(l50).toEqual([9, 9]);
|
||||
expect(l100).toEqual([14, 14]);
|
||||
expect(opacities(at(0))).toEqual([0.55, 0.55]);
|
||||
expect(opacities(at(0.5))).toEqual([0.75, 0.75]);
|
||||
expect(opacities(at(1))).toEqual([0.95, 0.95]);
|
||||
expect(at(0.5).match(/fill='#8be9ed'/gu)).toHaveLength(2);
|
||||
expect(at(0.51)).toBe(at(0.5));
|
||||
});
|
||||
|
||||
it('drops to amber parking lights when parked', () => {
|
||||
const svg = renderTabIcon({ ...icon, route: 0.8, parked: true });
|
||||
expect(beamLengths(svg)).toEqual([]);
|
||||
expect(svg.match(new RegExp(`fill='${TAB_ICON.parking}'`, 'gu'))).toHaveLength(2);
|
||||
expect(svg).not.toContain('#8be9ed');
|
||||
});
|
||||
|
||||
it('turns gold on peach at day', () => {
|
||||
const svg = renderTabIcon({ ...icon, route: 1, dawn: true });
|
||||
expect(svg).toContain(`<rect width='32' height='32' rx='6' fill='${TAB_ICON.dawnPlate}'/>`);
|
||||
expect(svg).toContain(`stroke='${TAB_ICON.dawnGlyph}'`);
|
||||
expect(svg).not.toContain(TAB_ICON.plate);
|
||||
expect(svg).not.toContain('#8be9ed');
|
||||
});
|
||||
|
||||
it('ignores an accent that is not a hex colour', () => {
|
||||
expect(renderTabIcon({ ...icon, accent: "red'/><script>" })).not.toContain('script');
|
||||
});
|
||||
|
||||
it('encodes the href as a data:image/svg+xml URI', () => {
|
||||
const svg = renderTabIcon({ ...icon, route: 0.5 });
|
||||
const href = tabIconHref(svg);
|
||||
expect(href.startsWith('data:image/svg+xml,%3Csvg')).toBe(true);
|
||||
expect(href).not.toMatch(/[<>#" ]/u);
|
||||
expect(decodeURIComponent(href.slice('data:image/svg+xml,'.length))).toBe(svg);
|
||||
});
|
||||
});
|
||||
|
||||
describe('server HTML', () => {
|
||||
const fixture = (name: string): unknown =>
|
||||
JSON.parse(readFileSync(resolve(process.cwd(), 'tests/fixtures', name), 'utf8'));
|
||||
|
||||
it('carries no living tab state', async () => {
|
||||
const html = await render('/', validateProductionInputs(
|
||||
fixture('portfolio-content.valid.json'),
|
||||
fixture('asset-manifest.valid.json'),
|
||||
fixture('site-config.preview.json'),
|
||||
new URL('file:///prepared-assets/'),
|
||||
));
|
||||
expect(html).not.toMatch(/ROUTE \d+%|PARKED ·|ARRIVED ·|data:image\/svg\+xml/u);
|
||||
});
|
||||
|
||||
it('leaves index.html title and icon alone', () => {
|
||||
const index = readFileSync(resolve(process.cwd(), 'index.html'), 'utf8');
|
||||
expect(index).toContain('<title>Portfolio</title>');
|
||||
expect(index).toContain("%3Cpath d='M11 7v18h11' fill='none' stroke='%23ffd693' stroke-width='4'/%3E");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user