Vis testing battery: library + debug page sections
Collects every manual testing entry point into one library (src/debug/battery.js) rendered from the live registries: all 17 bank songs with app/+venue/strip links, all 70 scenes grouped by family with gate/wide links, all 3 venues, 9 curated param and scenario combos, and the graduated lab pages with their headless runners. debug.html renders it; new songs/scenes/venues join free.
This commit is contained in:
@@ -33,6 +33,22 @@
|
|||||||
pre { padding: 8px 10px; overflow-x: auto; margin: 6px 0 0; }
|
pre { padding: 8px 10px; overflow-x: auto; margin: 6px 0 0; }
|
||||||
.note { color: #6b7280; font-size: 12px; max-width: 80ch; }
|
.note { color: #6b7280; font-size: 12px; max-width: 80ch; }
|
||||||
.warn { color: #eab308; }
|
.warn { color: #eab308; }
|
||||||
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 2px; }
|
||||||
|
.chips a {
|
||||||
|
background: #080a0f; border: 1px solid #1a1e28; border-radius: 3px;
|
||||||
|
padding: 1px 7px; font-size: 12px; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.chips a:hover { border-color: #7dd3fc; text-decoration: none; }
|
||||||
|
.chips .fam { width: 100%; color: #565f70; font-size: 11px; letter-spacing: .08em;
|
||||||
|
text-transform: uppercase; margin-top: 6px; }
|
||||||
|
.chips .fam:first-child { margin-top: 0; }
|
||||||
|
.links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||||
|
.links a {
|
||||||
|
background: #080a0f; border: 1px solid #1a1e28; border-radius: 3px;
|
||||||
|
padding: 1px 7px; font-size: 12px;
|
||||||
|
}
|
||||||
|
.links a:hover { border-color: #7dd3fc; text-decoration: none; }
|
||||||
|
.links .lbl { color: #565f70; font-size: 12px; align-self: center; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -43,6 +59,48 @@
|
|||||||
many.
|
many.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2>Vis testing battery</h2>
|
||||||
|
<p class="lede">
|
||||||
|
The testing library: every bank song, every scene, every venue, the
|
||||||
|
param/scenario combos worth re-running, and the lab pages. Rendered
|
||||||
|
from the live registries (<code>src/debug/battery.js</code>), so a new
|
||||||
|
song, scene or venue joins the battery for free.
|
||||||
|
</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card fast">
|
||||||
|
<h3>songs — the bank, one click each</h3>
|
||||||
|
<p>Each song holds a corner of the feature space (<code>covers</code>).
|
||||||
|
App loads the song; +venue adds the room; strip is one frame per
|
||||||
|
30 s across the whole song.</p>
|
||||||
|
<div id="battery-songs" class="chips"><span class="note">loading…</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="card fast">
|
||||||
|
<h3>venues — every room</h3>
|
||||||
|
<p>Pinned gallery first (fastest way to review a venue change), then
|
||||||
|
the app and the filmstrip with the room on.</p>
|
||||||
|
<div id="battery-venues"><span class="note">loading…</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Scenes — every visualizer</h2>
|
||||||
|
<p class="note">
|
||||||
|
Gate is the per-scene acceptance battery (renders, animates,
|
||||||
|
deterministic, distinct, sweep, flash, traits); wide is the scene on
|
||||||
|
six songs × six seeds.
|
||||||
|
</p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card fast">
|
||||||
|
<h3>by family</h3>
|
||||||
|
<div id="battery-scenes" class="chips"><span class="note">loading…</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Scenarios — params and combos</h2>
|
||||||
|
<div class="grid" id="battery-scenarios"></div>
|
||||||
|
|
||||||
|
<h2>Lab pages + headless runners</h2>
|
||||||
|
<div class="grid" id="battery-labs"></div>
|
||||||
|
|
||||||
<h2>Looking at output</h2>
|
<h2>Looking at output</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card fast">
|
<div class="card fast">
|
||||||
@@ -212,5 +270,109 @@ npm test</pre>
|
|||||||
session over a file called <code>clicktrack.js</code> — or a dev server
|
session over a file called <code>clicktrack.js</code> — or a dev server
|
||||||
caught mid-restart.
|
caught mid-restart.
|
||||||
</p>
|
</p>
|
||||||
|
<script type="module">
|
||||||
|
import {
|
||||||
|
songNames, songCovers, families, scenesIn,
|
||||||
|
venueNames, appLinks, sceneLinks, venueLinks,
|
||||||
|
SCENARIOS, LABS,
|
||||||
|
} from '/src/debug/battery.js';
|
||||||
|
|
||||||
|
const el = (tag, cls, text) => {
|
||||||
|
const e = document.createElement(tag);
|
||||||
|
if (cls) e.className = cls;
|
||||||
|
if (text !== undefined) e.textContent = text;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
const link = (href, text) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = href;
|
||||||
|
a.textContent = text;
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Songs: one row per bank song — name, corner it holds, app / +venue / strip.
|
||||||
|
{
|
||||||
|
const host = document.getElementById('battery-songs');
|
||||||
|
host.replaceChildren();
|
||||||
|
for (const name of songNames()) {
|
||||||
|
const covers = songCovers(name);
|
||||||
|
const L = appLinks(name);
|
||||||
|
const row = el('span', 'fam', `${name} — ${covers} `);
|
||||||
|
host.appendChild(row);
|
||||||
|
host.appendChild(link(L.app, 'app'));
|
||||||
|
host.appendChild(link(L.venue, '+venue'));
|
||||||
|
host.appendChild(link(`/filmstrip.html?song=${encodeURIComponent(name)}`, 'strip'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Venues: pinned gallery + app + filmstrip per room.
|
||||||
|
{
|
||||||
|
const host = document.getElementById('battery-venues');
|
||||||
|
host.replaceChildren();
|
||||||
|
for (const name of venueNames()) {
|
||||||
|
const L = venueLinks(name);
|
||||||
|
const card = el('div', null);
|
||||||
|
card.appendChild(el('p', null, name));
|
||||||
|
const row = el('div', 'links');
|
||||||
|
row.appendChild(link(L.gallery, 'gallery'));
|
||||||
|
row.appendChild(link(L.app, 'app +venue'));
|
||||||
|
row.appendChild(link(L.filmstrip, 'strip'));
|
||||||
|
card.appendChild(row);
|
||||||
|
host.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenes: one chip per visualizer (→ gate), ◈ per scene (→ wide gallery).
|
||||||
|
{
|
||||||
|
const host = document.getElementById('battery-scenes');
|
||||||
|
host.replaceChildren();
|
||||||
|
for (const fam of families()) {
|
||||||
|
host.appendChild(el('span', 'fam', fam));
|
||||||
|
for (const name of scenesIn(fam)) {
|
||||||
|
const L = sceneLinks(name);
|
||||||
|
host.appendChild(link(L.gate, name));
|
||||||
|
const w = link(L.wide, '◈');
|
||||||
|
w.title = `${name} — wide gallery (6 songs × 6 seeds)`;
|
||||||
|
host.appendChild(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenarios: curated cards from the library.
|
||||||
|
{
|
||||||
|
const host = document.getElementById('battery-scenarios');
|
||||||
|
host.replaceChildren();
|
||||||
|
for (const s of SCENARIOS) {
|
||||||
|
const card = el('div', 'card');
|
||||||
|
card.appendChild(el('h3', null, s.title));
|
||||||
|
card.appendChild(el('p', null, s.desc));
|
||||||
|
const row = el('div', 'links');
|
||||||
|
for (const [label, href] of s.links) row.appendChild(link(href, label));
|
||||||
|
card.appendChild(row);
|
||||||
|
host.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labs: pages + the CLI that drives them.
|
||||||
|
{
|
||||||
|
const host = document.getElementById('battery-labs');
|
||||||
|
host.replaceChildren();
|
||||||
|
for (const lab of LABS) {
|
||||||
|
const card = el('div', 'card cli');
|
||||||
|
card.appendChild(el('h3', null, lab.title));
|
||||||
|
card.appendChild(el('p', null, lab.desc));
|
||||||
|
if (lab.href) {
|
||||||
|
const row = el('div', 'links');
|
||||||
|
row.appendChild(link(lab.href, 'open'));
|
||||||
|
card.appendChild(row);
|
||||||
|
}
|
||||||
|
if (lab.cli) {
|
||||||
|
const pre = el('pre', null, lab.cli);
|
||||||
|
card.appendChild(pre);
|
||||||
|
}
|
||||||
|
host.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// 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 behind it. The fastest way to review a venue change.',
|
||||||
|
links: [
|
||||||
|
['Ride + corridor', `/venue-gallery.html${q({ venueSel: 'Ride', viz: 'Synthwave Corridor', rows: 2, cols: 3 })}`],
|
||||||
|
['Chamber + swarm', `/venue-gallery.html${q({ venueSel: 'Chamber', viz: 'Swarm', rows: 2, cols: 3 })}`],
|
||||||
|
['Stage + pylons', `/venue-gallery.html${q({ venueSel: 'Stage', viz: 'Pylon Field 3D', 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user