storebound/tests/no-handmade-svg.test.ts

105 lines
6.4 KiB
TypeScript

// Owner rule: no hand-written SVG. Charts come from visx, icons from Phosphor, the store marks from simple-icons, the
// journey diagram from Mermaid, the fixture art from tools/fixture-art.ts. This guard fails on inline SVG drawing in
// the desktop webview, the site and the teaser video (media/teaser), and on any SVG file that is not a known asset.
// The brand mark is a PNG (tools/brand-assets.py from brand/master/logo-mark.png): no inline brand SVG anywhere.
// Allowed: the <svg> root a visx chart renders into (files that import @visx/*), the vendored icon
// files (site/img/icons: Phosphor and simple-icons; apps/desktop/src/ui/store-marks and templates/web: simple-icons;
// media/teaser/icons: Phosphor), each byte-equal to
// its package (tools/site-icons.ts), and the Mermaid output (docs/img, site/img journey*.svg).
import { describe, expect, test } from 'bun:test';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { checkSiteIcons, ICON_DIR, MARK_DIR, SITE_ICONS, STORE_MARKS, TEASER_DIR, TEASER_ICONS, WEB_DIR } from '../tools/site-icons';
const ROOT = join(import.meta.dir, '..');
const walk = (dir: string, keep: (f: string) => boolean): string[] => readdirSync(join(ROOT, dir)).flatMap(n => {
const rel = join(dir, n);
if (n === 'node_modules' || n === 'dist') return [];
return statSync(join(ROOT, rel)).isDirectory() ? walk(rel, keep) : keep(n) ? [rel] : [];
});
const read = (f: string) => readFileSync(join(ROOT, f), 'utf8');
/** SVG drawing elements and inline SVG images. */
const SHAPES = /<(path|circle|rect|ellipse|line|polyline|polygon)[\s/>]|data:image\/svg\+xml|<svg[\s>]/g;
/** Every hand-drawn hit in a source text: "file:line <tag". */
export function svgHits(file: string, text: string, opts: { visxRoot?: boolean } = {}): string[] {
const out: string[] = [];
for (const m of text.matchAll(SHAPES)) {
if (opts.visxRoot && /^<svg/.test(m[0])) continue;
out.push(`${file}:${text.slice(0, m.index).split('\n').length} ${m[0].trim()}`);
}
return out;
}
describe('no hand-written SVG', () => {
test('desktop webview: charts through visx, icons through Phosphor, no inline SVG at all (the brand mark is a PNG)', () => {
const files = walk('apps/desktop/src', n => /\.(tsx?|css|html)$/.test(n));
expect(files.length).toBeGreaterThan(30);
const hits = files.flatMap(f => {
const s = read(f);
return svgHits(f, s, { visxRoot: /from '@visx\//.test(s) });
});
expect(hits).toEqual([]);
// icons only through Phosphor's per-icon modules (tree-shaken), never the lucide set or the whole Phosphor index
const bad = files.filter(f => /from '(lucide-react|@phosphor-icons\/react)'/.test(read(f)));
expect(bad).toEqual([]);
});
test('the guard itself catches a drawn icon and an inline SVG image', () => {
expect(svgHits('x.tsx', 'const a = <svg viewBox="0 0 24 24"><path d="M0 0h1" /></svg>;')).toHaveLength(2);
expect(svgHits('x.ts', "const u = 'data:image/svg+xml;base64,AAAA';")).toHaveLength(1);
expect(svgHits('x.tsx', '<svg width={w}><LinePath data={d} /></svg>', { visxRoot: true })).toEqual([]);
expect(svgHits('x.tsx', '<svg width={w}><circle r={2} /></svg>', { visxRoot: true })).toHaveLength(1);
});
test('site pages: icons are vendored Phosphor / simple-icons files; no inline SVG, the brand mark is a PNG', () => {
const pages = walk('site', n => n.endsWith('.html'));
expect(pages.length).toBeGreaterThanOrEqual(2);
const hits = pages.flatMap(f => svgHits(f, read(f)));
expect(hits).toEqual([]);
expect(checkSiteIcons()).toEqual([]);
for (const f of ['site/style.css', 'site/main.js']) expect(svgHits(f, read(f)), f).toEqual([]);
});
test('teaser video (media/teaser): icons and the mouse cursor are vendored Phosphor files, no inline SVG', () => {
const files = walk('media/teaser', n => /\.(html|js|ts|css)$/.test(n)).filter(f => !f.startsWith('media/teaser/out/'));
expect(files.length).toBeGreaterThanOrEqual(5);
expect(files.flatMap(f => svgHits(f, read(f)))).toEqual([]);
// every icon the teaser references is one of the vendored Phosphor files
const used = files.flatMap(f => [...read(f).matchAll(/icons\/([\w-]+\.svg)/g)].map(m => m[1]));
expect(used.length).toBeGreaterThanOrEqual(TEASER_ICONS.length);
const vendored: string[] = TEASER_ICONS.map(([w, i]) => `${i}-${w}.svg`);
for (const n of used) expect(vendored, n).toContain(n ?? '');
});
test('templates (the web init landing page): store marks are the vendored simple-icons files, no inline SVG', () => {
const files = walk('templates', n => /\.(html|css|js)$/.test(n));
expect(files).toContain('templates/web/landing.html');
expect(files.flatMap(f => svgHits(f, read(f)))).toEqual([]);
});
test('SVG files: vendored Phosphor / simple-icons files and Mermaid diagrams only', () => {
const svgs = ['site', 'docs', 'apps/desktop/src', 'media', 'templates'].flatMap(d => walk(d, n => n.endsWith('.svg'))).map(f => relative(ROOT, join(ROOT, f)));
const vendored = new Set([
...[...SITE_ICONS, ...STORE_MARKS].map(n => relative(ROOT, join(ICON_DIR, `${n}.svg`))),
...STORE_MARKS.map(n => relative(ROOT, join(MARK_DIR, `${n}.svg`))),
...STORE_MARKS.map(n => relative(ROOT, join(WEB_DIR, `${n}.svg`))),
...TEASER_ICONS.map(([w, n]) => relative(ROOT, join(TEASER_DIR, `${n}-${w}.svg`))),
]);
// byte equality with the packages is checked by checkSiteIcons (above); here: no other SVG file sneaks in
for (const f of svgs) {
if (vendored.has(f)) continue;
expect(f, `${f}: not a known asset`).toMatch(/^(docs|site)\/img\/journey(-tall)?\.svg$/);
expect(read(f), `${f}: render it with bun tools/diagrams.ts`).toContain('aria-roledescription="flowchart-v2"');
}
});
test('brand: the mark is the PNG set from tools/brand-assets.py, no SVG brand file', () => {
const all = ['site', 'apps/desktop/src', 'apps/desktop/src-tauri/icons', 'brand'].flatMap(d => walk(d, n => n.endsWith('.svg')));
expect(all.filter(f => !/^(site\/img\/icons|apps\/desktop\/src\/ui\/store-marks)\//.test(f) && !/journey(-tall)?\.svg$/.test(f))).toEqual([]);
for (const f of ['brand/master/logo-mark.png', 'site/favicon.ico', 'site/img/logo-mark.png', 'apps/desktop/src/ui/logo-mark.png', 'apps/desktop/src-tauri/icons/icon.icns']) {
expect(statSync(join(ROOT, f)).size, f).toBeGreaterThan(0);
}
});
});