// The vis testing battery as a library. // // Every manual testing entry point this project has — bank songs, scenes by // family, venues, param/scenario combos, the throwaway lab pages and the // headless CLI runners — collected in one place so debug.html (and anything // headless later) renders them from the live registries instead of a stale // hardcoded list. Adding a song, scene or venue updates the battery for free. // // Pure URL builders + data: no DOM, no WebGL, no wall-clock. Safe to import // anywhere, including node (nothing here touches metadata.json, which needs // Vite's JSON handling — so scene *coverage* is deliberately not imported). import { SONGS } from '../audio/songbank.js'; import { scenes } from '../scenes/registry.js'; import { venues } from '../venues/registry.js'; const q = (params) => { const p = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v === null || v === undefined || v === '') continue; p.set(k, String(v)); } const s = p.toString(); return s ? `?${s}` : ''; }; export const songNames = () => SONGS.map((s) => s.name); export const songCovers = (name) => (SONGS.find((s) => s.name === name) || {}).covers || ''; export const sceneNames = () => scenes.map((s) => s.name); export const venueNames = () => venues.map((v) => v.name); export const families = () => { const out = []; for (const s of scenes) if (!out.includes(s.family)) out.push(s.family); return out; }; export const scenesIn = (family) => scenes.filter((s) => s.family === family).map((s) => s.name); /** App links for a bank song: plain, with venue, pinned seeds. */ export const appLinks = (song) => ({ app: `/${q({ song })}`, venue: `/${q({ song, venue: 1 })}`, seed1: `/${q({ song, seed: 1 })}`, seed2: `/${q({ song, seed: 2 })}`, }); /** Per-scene links: acceptance battery + wide single-scene gallery. */ export const sceneLinks = (name) => ({ gate: `/checks.html${q({ scene: name })}`, wide: `/gallery.html${q({ scene: name, songs: 6, seeds: 6 })}`, }); /** Per-venue links: gallery pinned to the venue, app + filmstrip with venue. */ export const venueLinks = (name) => ({ gallery: `/venue-gallery.html${q({ venueSel: name })}`, app: `/${q({ song: 'centre', venue: 1 })}`, filmstrip: `/filmstrip.html${q({ song: 'centre,ember', venue: 1, venueSel: name })}`, }); /** * Curated param/scenario combos: each is one question with the links that * answer it. Songs are corners of the feature space (see songbank.js * `covers`); pairs differ in exactly one control where possible. */ export const SCENARIOS = [ { title: 'venue A/B, same song', desc: 'Does the room earn its place? Same seed both sides — only the venue differs.', links: [ ['no venue', `/${q({ song: 'centre', seed: 7 })}`], ['venue', `/${q({ song: 'centre', seed: 7, venue: 1 })}`], ], }, { title: 'seed reroll, same song', desc: 'Same music, two draws. Should read as two videos, not two grades of one.', links: [ ['seed 1', `/${q({ song: 'ember', seed: 1 })}`], ['seed 2', `/${q({ song: 'ember', seed: 2 })}`], ], }, { title: 'tempo ends', desc: '62bpm beatless vs 174bpm broken. Motion should follow the song, not the scene.', links: [ ['drone 62', `/${q({ song: 'drone' })}`], ['runner 174', `/${q({ song: 'runner' })}`], ], }, { title: 'degenerate case', desc: 'Two sections. Must not break — still is the regression test for it.', links: [['still', `/${q({ song: 'still' })}`]], }, { title: 'hostile cases', desc: 'Limitered, noisiest, brightest: the corners that collapse palettes and directors.', links: [ ['slab (flat)', `/${q({ song: 'slab' })}`], ['grit (noise)', `/${q({ song: 'grit' })}`], ['flare (bright)', `/${q({ song: 'flare' })}`], ], }, { title: 'most sections', desc: 'Lattice rotates rosters the hardest. Watch for A/B ping-ponging and repeats.', links: [['lattice', `/${q({ song: 'lattice' })}`]], }, { title: 'venue × viz pins', desc: 'One room, chosen viz in it: fragment viz play on the room\u2019s screen, model viz stand in the shared world. Combos picked so the viz actually reads — a dark or sparse viz on a small screen is invisible, which is how a dead pin hides. (Ride is excluded: its viz lives on the dash radio, too small to review here.)', links: [ ['Chamber + Kaleido', `/venue-gallery.html${q({ venueSel: 'Chamber', viz: 'Kaleido Tunnel', rows: 2, cols: 3 })}`], ['Stage + Assembly', `/venue-gallery.html${q({ venueSel: 'Stage', viz: 'Assembly', rows: 2, cols: 3 })}`], ], }, { title: 'filmstrip with venue', desc: 'Does the room hold for two minutes, or does it drift, flash, or empty out?', links: [ ['centre + ember', `/filmstrip.html${q({ song: 'centre,ember', venue: 1 })}`], ], }, { title: 'fresh scene gates', desc: 'The per-scene battery for the scenes most recently touched.', links: [ ['Swarm', `/checks.html${q({ scene: 'Swarm' })}`], ['Assembly', `/checks.html${q({ scene: 'Assembly' })}`], ['Pylon Field 3D', `/checks.html${q({ scene: 'Pylon Field 3D' })}`], ], }, ]; /** * The graduated throwaways: single-purpose lab pages plus the headless * runners that drive them. Linked, not duplicated — the pages stay where * the runners expect them. */ export const LABS = [ { title: 'ride lab', href: '/ride-lab.html', desc: 'One Ride, four inspection cameras. Settles which way the car faces.', cli: 'node ride-lab-run.mjs', }, { title: 'passerby lab', href: '/passerby-lab.html', desc: 'Hunts a seed with a passerby cue on elegy, renders the shot head/mid/tail.', cli: null, }, { title: 'ride closeup', href: '/venue-gallery.html?rows=1&cols=1&venueSel=Ride', desc: 'Wheels + beams at 2×. Same framing the closeup runner screenshots.', cli: 'node ride-closeup.mjs', }, { title: 'venue gate loop', href: null, desc: 'Headless Chromium against vite dev: phases, filmstrip, gallery, variety. Screenshots to logs/venue/.', cli: 'node tools/venue-check.js --quick', }, ];