diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 176 |
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 |
