aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes/probe.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/gallery-probes/probe.mjs')
-rw-r--r--tests/gallery-probes/probe.mjs176
1 files changed, 158 insertions, 18 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 5e95096..905d758 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -65,21 +65,136 @@ try {
const errs = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions on load', errs.length === 0, errs.map(e => e.params.exceptionDetails?.exception?.description).join('; ').slice(0, 200));
- // 1. defaults: size=2 (M), card count
- ok('default size 2', await evl(`document.body.dataset.size`) === '2');
+ // 1. card count (the widgets/row default is checked in 1h)
const cards = await evl(`document.querySelectorAll('.card').length`);
ok('111 cards', cards === 111, `got ${cards}`);
- // 2. zoom actually scales: card visual width at 3x vs 1x
- await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`);
- const w3 = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
- await evl(`document.querySelector('.szbar .key[data-sz="1"]').click()`);
- const w1 = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
- ok('3x wider than 1x', w3 > w1 * 1.8, `w3=${Math.round(w3)} w1=${Math.round(w1)}`);
- ok('size chip flips state', await evl(`document.body.dataset.size`) === '1');
-
- // 3. behavioral at 3x: fader drag on card 03 (horizontal fader) changes readout
- await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`);
+ // 1b. Every card carries a spec sheet. R58 shipped without one and nothing
+ // noticed — the sheet is the card's actual specification, so a card without
+ // it is a drawing with no contract. Cheap to forget, cheap to check.
+ const sheets = await evl(`(()=>{
+ const missing = [...document.querySelectorAll('.card')]
+ .filter(c => !c.querySelector('.winfo'))
+ .map(c => c.id.slice(5));
+ return missing.length ? 'no spec sheet: ' + missing.join(' ') : 'ok';
+ })()`);
+ ok('every card has a spec sheet', sheets === 'ok', sheets);
+
+ // 1c. Card 07's lit colour comes from the shared accent family, not a hardcode.
+ // The chip said exactly one thing (gold) when "on" is good in one panel, a
+ // warning in the next and a fault in the one after. Checks the family is
+ // shared rather than copied: a second consumer must get the same five.
+ const accents = await evl(`(()=>{
+ const want = ['amber','green','red','white','vfd'];
+ const chips = [...document.querySelectorAll('#card-07 .fc')].map(c => c.title);
+ const missing = want.filter(w => !chips.includes(w));
+ if (missing.length) return 'card 07 missing accents: ' + missing.join(' ');
+ const built = Object.keys(GW.accentStyles('--x'));
+ return JSON.stringify(built) === JSON.stringify(want) ? 'ok' : 'family is ' + JSON.stringify(built);
+ })()`);
+ ok('card 07 offers the shared accent family', accents === 'ok', accents);
+
+ // 1d. The chip actually recolours, and its default is untouched until asked —
+ // the same fallback discipline the screen families use.
+ const chipInk = await evl(`(()=>{
+ const chip = document.querySelector('#card-07 .chip');
+ const lit = () => getComputedStyle(chip).color;
+ const before = lit();
+ document.querySelector('#card-07 .fc[title="vfd"]').click();
+ const after = lit();
+ return before === 'rgb(226, 160, 56)' && after === 'rgb(99, 230, 200)'
+ ? 'ok' : before + ' -> ' + after;
+ })()`);
+ ok('card 07 defaults to gold and recolours on click', chipInk === 'ok', chipInk);
+
+ // 1e. The colour-policy backbone. Every widget's colour is either free (the
+ // consumer picks) or locked for a stated reason, and the colour pass kept
+ // finding cards where "is this one a standard?" was answered from memory and
+ // wrong. So policy is declared on the builder and checked here. The invariant
+ // that matters: the accent recolour mechanism and the 'accent' declaration
+ // imply each other, so a locked card can't silently sprout accent chips and
+ // an accent card can't forget to say so. Coverage grows card by card; this
+ // round classifies the touched set, and the count is reported not gated.
+ const policy = await evl(`(()=>{
+ const KINDS = GW.POLICIES;
+ if (!KINDS) return 'no POLICIES vocabulary';
+ const valid = new Set(Object.keys(KINDS));
+ const fns = Object.entries(GW).filter(([,v]) => typeof v === 'function');
+ const declared = fns.filter(([,v]) => v.POLICY);
+ // each POLICY record is complete: a valid kind, a why, an authentic range
+ const shape = declared.filter(([,v]) => !valid.has(v.POLICY.kind) || !v.POLICY.why || !v.POLICY.authentic)
+ .map(([k,v]) => k + '(' + v.POLICY.kind + ')');
+ if (shape.length) return 'malformed policy record: ' + shape.join(' ');
+ // A recolour mechanism and an 'accent' kind imply each other, BOTH directions,
+ // and both directions must recognise the SAME mechanisms — or a locked card
+ // carrying a multi-colour on-family slips the reverse check. A recolour
+ // mechanism is a STYLES.accent axis or a STYLES.on family (slideToggle's shape).
+ const hasMech = v => !!(v.STYLES && (v.STYLES.accent || v.STYLES.on));
+ const mechNotAccent = fns.filter(([,v]) => hasMech(v) && (!v.POLICY || v.POLICY.kind !== 'accent')).map(([k]) => k);
+ if (mechNotAccent.length) return 'recolour mechanism but not declared accent: ' + mechNotAccent.join(' ');
+ const accentNoMech = declared.filter(([,v]) => v.POLICY.kind === 'accent' && !hasMech(v)).map(([k]) => k);
+ if (accentNoMech.length) return 'declared accent but no recolour mechanism: ' + accentNoMech.join(' ');
+ // a screen kind must actually read the screen vars
+ const screenNoVars = declared.filter(([,v]) => v.POLICY.kind === 'screen' && !v.toString().includes('--scr-')).map(([k]) => k);
+ if (screenNoVars.length) return 'declared screen but reads no --scr- vars: ' + screenNoVars.join(' ');
+ return 'ok (' + declared.length + ' classified)';
+ })()`);
+ ok('policy records valid, complete and mechanism-matched', policy.startsWith('ok'), policy);
+
+ // 1f. The policy is on the PAGE, not just in code and this probe. Each card
+ // shows a badge derived from its builder's COLOR (single source, so the
+ // page can't drift from the declaration), and the badge marks free vs
+ // locked vs unclassified. Checks a known classified card renders its policy
+ // and a locked/unclassified one renders honestly.
+ const shown = await evl(`(()=>{
+ const badge = no => document.querySelector('#card-' + no + ' .cpol');
+ const b07 = badge('07'), b10 = badge('R10');
+ if (!b07 || !b10) return 'no policy badge on card';
+ if (b07.textContent !== 'accent' || b07.dataset.free !== 'true') return 'card 07 badge: ' + b07.textContent + '/' + b07.dataset.free;
+ if (b10.textContent !== 'screen') return 'card R10 badge: ' + b10.textContent;
+ // an unclassified card shows the dash, not a stale or invented policy
+ const un = [...document.querySelectorAll('.cpol')].find(b => b.dataset.pol === 'none');
+ if (un && un.textContent !== '—') return 'unclassified badge should read dash: ' + un.textContent;
+ // the badge text always matches the builder's actual COLOR, never a copy
+ const drift = [...document.querySelectorAll('.card')].map(c => {
+ const b = c.querySelector('.cpol'); const want = c.dataset.cpol;
+ return (want === 'none' ? '—' : want) === b.textContent ? null : c.id;
+ }).filter(Boolean);
+ return drift.length ? 'badge drifts from data-cpol: ' + drift.slice(0,3).join(' ') : 'ok';
+ })()`);
+ ok('colour policy is shown on each card, derived not copied', shown === 'ok', shown);
+
+ // 1g. The index tally counts coverage the way the validation tally counts the
+ // walk, so "how far has the colour pass reached" is a glance, not a grep.
+ // free + locked + unclassified must sum to the card total.
+ const ptally = await evl(`(()=>{
+ const rows = [...document.querySelectorAll('#ptally .vrow[data-g] .vn')].map(v => +v.textContent);
+ const total = +document.querySelector('#ptally .vtot .vn').textContent;
+ const cards = document.querySelectorAll('.card').length;
+ if (total !== cards) return 'tally total ' + total + ' != ' + cards + ' cards';
+ if (rows.reduce((a,b)=>a+b,0) !== total) return 'free+locked+unclassified ' + rows + ' != ' + total;
+ return 'ok free=' + rows[0] + ' locked=' + rows[1] + ' unclassified=' + rows[2];
+ })()`);
+ ok('colour policy tally sums to the card total', ptally.startsWith('ok'), ptally);
+
+ // 1h. widgets/row: the control sets an explicit column count and the cards
+ // resize to match. One-per-row cards must be much wider than four-per-row,
+ // and the count actually reaches the grid.
+ ok('default is 3 widgets/row', await evl(`document.body.dataset.cols`) === '3');
+ await evl(`document.querySelector('.szbar .key[data-cols="1"]').click()`);
+ const w1col = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
+ const cols1 = await evl(`getComputedStyle(document.querySelector('.grid')).gridTemplateColumns.split(' ').length`);
+ await evl(`document.querySelector('.szbar .key[data-cols="4"]').click()`);
+ const w4col = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
+ const cols4 = await evl(`getComputedStyle(document.querySelector('.grid')).gridTemplateColumns.split(' ').length`);
+ ok('1/row is much wider than 4/row', w1col > w4col * 2.5, `w1=${Math.round(w1col)} w4=${Math.round(w4col)}`);
+ ok('the grid renders the chosen column count', cols1 === 1 && cols4 === 4, `1->${cols1} 4->${cols4}`);
+ ok('widgets/row chip flips state', await evl(`document.body.dataset.cols`) === '4');
+
+ // 3. behavioral, zoomed: fader drag on card 03 changes readout. Run at 2/row,
+ // not 1/row: at one-per-row the card is wide enough that its controls fall
+ // outside the 1600px probe window and the dispatched mouse events miss.
+ await evl(`document.querySelector('.szbar .key[data-cols="2"]').click()`);
await evl(`document.querySelectorAll('.card')[2].scrollIntoView({block:'center'}); ''`);
await sleep(200);
const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
@@ -87,9 +202,9 @@ try {
await drag(fr[0] + fr[2] * 0.2, fr[1] + fr[3] / 2, fr[0] + fr[2] * 0.9, fr[1] + fr[3] / 2);
await sleep(150);
const after = await evl(`document.getElementById('rd-03').textContent`);
- ok('fader drag tracks at 3x', before !== after && after !== '—', `'${before}' -> '${after}'`);
+ ok('fader drag tracks at 2/row', before !== after && after !== '—', `'${before}' -> '${after}'`);
- // 4. behavioral at 3x: toggle click on card 01
+ // 4. behavioral, zoomed: toggle click on card 01
await evl(`document.querySelectorAll('.card')[0].scrollIntoView({block:'center'}); ''`);
await sleep(200);
const sw = await evl(`(()=>{const c=document.querySelectorAll('.card')[0];const s=c.querySelector('.switch')||c.querySelector('.stagew > *');const r=s.getBoundingClientRect();return [r.left+r.width/2,r.top+r.height/2];})()`);
@@ -98,7 +213,7 @@ try {
await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: sw[0], y: sw[1], button: 'left', clickCount: 1 });
await sleep(150);
const t1 = await evl(`document.getElementById('rd-01').textContent`);
- ok('toggle click responds at 3x', t0 !== t1, `'${t0}' -> '${t1}'`);
+ ok('toggle click responds at 2/row', t0 !== t1, `'${t0}' -> '${t1}'`);
// 5. card 02 console keys: reading order and per-key tone. LIVE is green
// because --pass is what the kit means by live everywhere else; gold stays
@@ -569,14 +684,39 @@ try {
// keyboard skips. Both cases with no shift key, plus accents, fractions and
// the section mark — that coverage is the idea being preserved from the
// Mignon, where the key ORDER deliberately is not.
+ // The plate is the Mignon's own, so the set is its set: both full cases,
+ // the accents and fractions a keyboard skips — and deliberately NO 1 and NO
+ // 0, because the machine has you type them with lowercase l and capital O.
+ // That economy is an old value worth keeping, so it's asserted rather than
+ // tolerated: adding a 1 key would be a silent departure from the reference.
const charset = await evl(`(()=>{
const have = new Set([...document.querySelectorAll('#card-R58 .ix-cell')].map(c => c.dataset.c));
const need = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz',
- ...'0123456789', 'ä','ö','ü','Ä','Ö','Ü','ß','§','½','¼'];
+ ...'23456789', 'ä','ö','ü','§','½','¼','¾'];
const missing = need.filter(c => !have.has(c));
- return missing.length ? 'missing: ' + missing.join(' ') : 'ok';
+ if (missing.length) return 'missing: ' + missing.join(' ');
+ const shouldNotExist = ['1','0'].filter(c => have.has(c));
+ if (shouldNotExist.length) return 'plate grew a key the Mignon economises away: ' + shouldNotExist.join(' ');
+ return 'ok';
+ })()`);
+ ok('R58 carries the Mignon set: both cases, accents, no 1 or 0', charset === 'ok', charset);
+
+ // 14j. The inversion is ZONAL, not per-case. The capitals block is dark discs on
+ // a light panel; the lowercase block is its photographic negative. But J is
+ // a capital sitting out on the ring, un-inverted — so a rule of "capitals
+ // are dark" is wrong, and only position decides.
+ const zones = await evl(`(()=>{
+ const disc = c => {
+ const g = [...document.querySelectorAll('#card-R58 .ix-cell')].find(e => e.dataset.c === c);
+ return g ? g.querySelector('circle').getAttribute('fill') : null;
+ };
+ const caps = disc('P'), lower = disc('p'), ringCap = disc('J'), ringPunct = disc('&');
+ if (caps === lower) return 'capitals and lowercase discs are the same: ' + caps;
+ if (ringCap !== ringPunct) return 'J should wear the ring look like punctuation: J=' + ringCap + ' &=' + ringPunct;
+ if (ringCap === caps) return 'J is inverted, but it sits on the ring, not in the block';
+ return 'ok';
})()`);
- ok('R58 plate carries both cases, digits and the extended set', charset === 'ok', charset);
+ ok('R58 inversion is zonal: J is a capital but not inverted', zones === 'ok', zones);
// 14e. The layout is DATA, so revising the order is a table edit rather than a
// redraw. Craig has already said the keys will change; a layout welded into