Files
menu/test/support/harness.ts
T
tanshu c31b73d553 Let the owner manage categories, and give the menus a shelf
Categories were baked into config/<site>.site.json, so adding one meant a
rebuild and a redeploy. They are now data: data/sections.json, seeded from the
config the first time, and managed in the admin (add, rename, reorder, remove).
A category's web address is fixed when it is created because that is what a QR
code points at, and a category cannot be removed while it still holds menus.

The landing and the top bar were the ugliest parts. The landing was a column of
hairlines with tiny meta — a broadsheet, and the paper that makes this app worth
looking at was nowhere on it. It is now a shelf: each category is a row whose
right edge is a sliver of that menu's first page, bleeding off the screen, with
the shelf edge in brass and the name in heavy tight Archivo. The bar was a row
of teal pills that would not survive more categories: it now names the menu you
are reading and opens a sheet listing the rest, each with its own paper sliver,
and the current one is the only teal thing on screen. The sheet is a link to the
landing page, so navigating still works without the script.

Also fixes the category manager's own layout, which I shipped unstyled, and the
markup contract the route tests described.
2026-09-22 22:24:53 +05:30

67 lines
1.8 KiB
TypeScript

import { mkdir, mkdtemp } from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { SetStore, type ImagePipeline } from '../../src/storage/sets.ts';
export const SECTIONS = ['bar', 'food'] as const;
/** Deterministic stand-in for sharp, so storage tests need no image work. */
export class FakePipeline implements ImagePipeline {
calls = 0;
async process(data: Buffer, _originalName: string) {
this.calls += 1;
const hash = createHash('sha256').update(data).digest('hex').slice(0, 8);
return {
sourceExt: 'jpg' as const,
hash,
width: 3000,
height: 2000,
original: data,
webp: Buffer.from(`webp:${hash}`),
jpeg: Buffer.from(`jpeg:${hash}`),
thumb: Buffer.from(`thumb:${hash}`),
};
}
}
export function testClock(start = '2026-01-01T00:00:00.000Z') {
let time = Date.parse(start);
return {
now: () => new Date(time),
advance: (ms: number) => {
time += ms;
},
set: (iso: string) => {
time = Date.parse(iso);
},
};
}
export async function tempDir(prefix: string): Promise<string> {
const base = path.join(process.cwd(), '.tmp');
await mkdir(base, { recursive: true });
return mkdtemp(path.join(base, `${prefix}-`));
}
export function makeStore(
dataDir: string,
options: { clock?: ReturnType<typeof testClock>; maxPagesPerSet?: number } = {},
) {
const pipeline = new FakePipeline();
const clock = options.clock ?? testClock();
const store = new SetStore({
dataDir,
pipeline,
sections: { has: (slug: string) => (SECTIONS as readonly string[]).includes(slug) },
maxPagesPerSet: options.maxPagesPerSet ?? 40,
timeZone: 'Asia/Kolkata',
now: clock.now,
});
return { store, pipeline, clock };
}
export function pngBytes(label: string): Buffer {
return Buffer.from(`fake-image:${label}`);
}