diff options
Diffstat (limited to 'tests/gallery-probes/probe.mjs')
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 198 |
1 files changed, 177 insertions, 21 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs index 7793c6f..7b9ba5e 100644 --- a/tests/gallery-probes/probe.mjs +++ b/tests/gallery-probes/probe.mjs @@ -886,16 +886,17 @@ try { // Park the page's 1.5s demo ticker so it can't retarget mid-check // (IH.flap is the ticker's handle reference; the card element keeps the real one). await evl(`(() => { window.__realFlap = IH.flap; IH.flap = { next(){}, set(){}, tick(){} }; })()`); - // Let the board settle on word 0 first — the demo ticker left it anywhere, - // and set(0) itself cascades (there is deliberately no teleport path). - await evl(`document.querySelector('#card-N20').gw.set(0)`); - await sleep(4200); + // Let the board settle on a known grid first — the demo ticker left it + // anywhere, and every command cascades (there is deliberately no teleport + // path). setText is the deterministic seam; next() is random by design. + await evl(`document.querySelector('#card-N20').gw.setText('SYSTEM\\nVOLUME\\nSIGNAL')`); + await sleep(5400); // Start a cascade and sample the displayed reading every 15ms until stable. await evl(`(() => { const h = document.querySelector('#card-N20').gw; window.__n20 = { samples: [], anims: 0 }; - h.set(1); // DNS -> LINK, multi-step distances on several cells + h.setText('VOLUME\\nSIGNAL\\nRECORD'); // multi-step distances on several cells window.__n20.n = 0; window.__n20.timer = setInterval(() => { window.__n20.samples.push(h.reading()); @@ -907,20 +908,20 @@ try { window.__n20.anims++; }, 15); })()`); - await sleep(4200); + await sleep(5400); const n20 = await evl(`(() => { clearInterval(window.__n20.timer); const h = document.querySelector('#card-N20').gw; return { samples: [...new Set(window.__n20.samples)], final: h.reading(), anims: window.__n20.anims, - chars: h.chars, words: ['DNS ','LINK'] }; + chars: h.chars }; })()`); - ok('N20 cascade arrives at the target word', n20.final.trim() === 'LINK', n20.final); + ok('N20 cascade arrives at the commanded grid', n20.final === 'VOLUME\nSIGNAL\nRECORD', JSON.stringify(n20.final)); ok('N20 cascade passes through intermediate readings (never teleports)', - n20.samples.filter(s => s.trim() !== 'DNS' && s.trim() !== 'LINK').length >= 2, + n20.samples.filter(s => s !== 'SYSTEM\nVOLUME\nSIGNAL' && s !== 'VOLUME\nSIGNAL\nRECORD').length >= 2, n20.samples.length + ' distinct states'); ok('N20 flip runs WAAPI animations (a CSS class swap cannot satisfy this)', n20.anims > 0, 'anim samples ' + n20.anims); { - // Cell 0 travels D->L; every observed transition must step +1 through the charset. + // Cell 0 travels S->V; every observed transition must step +1 through the charset. const seq = [...new Set(n20.samples.map(s => s[0]))]; let stepped = seq.length >= 3 ? 'ok' : 'too few states: ' + seq.join(''); for (let i = 1; i < seq.length && stepped === 'ok'; i++) { @@ -935,31 +936,186 @@ try { const h = document.querySelector('#card-N20').gw; window.__n20r = { samples: [] }; window.__n20r.timer = setInterval(() => window.__n20r.samples.push(h.reading()), 15); - h.set(2); setTimeout(() => h.set(3), 180); + h.setText('SIGNAL\\nRECORD\\nONLINE'); setTimeout(() => h.setText('RECORD\\nONLINE\\nSTEREO'), 180); })()`); - await sleep(4600); + await sleep(5800); const n20b = await evl(`(() => { clearInterval(window.__n20r.timer); const h = document.querySelector('#card-N20').gw; - return { final: h.reading(), target: 'WIFI', - sawSync: window.__n20r.samples.some(s => s.trim() === 'SYNC'), + return { final: h.reading().split('\\n')[0], target: 'RECORD', + sawSync: window.__n20r.samples.some(s => s.split('\\n')[0] === 'SIGNAL'), anims: document.querySelector('#card-N20').getAnimations({ subtree: true }).length }; })()`); - ok('N20 retarget mid-cascade lands on the new target', n20b.final.trim() === n20b.target, n20b.final); - ok('N20 retarget re-aims rather than queueing (SYNC never fully forms)', !n20b.sawSync, n20b.sawSync ? 'saw SYNC' : 'ok'); + ok('N20 retarget mid-cascade lands on the new target', n20b.final === n20b.target, n20b.final); + ok('N20 retarget re-aims rather than queueing (SIGNAL never fully forms)', !n20b.sawSync, n20b.sawSync ? 'saw SIGNAL' : 'ok'); ok('N20 no cascade left running after arrival', n20b.anims === 0, 'live anims ' + n20b.anims); + // The demo scramble: next() sends each row to a random pool word — every + // row changes, rows stay mutually distinct, all drawn from the pool. + const n20s = await evl(`(() => { + const el = document.createElement('div'); document.body.appendChild(el); + const pool = ['SYSTEM','VOLUME','SIGNAL','RECORD','ONLINE','STEREO','FILTER','OUTPUT']; + const h = GW.splitFlap(el, { rows: 3, cells: 6, words: pool, animate: false }); + const grids = [h.reading().split('\\n')]; + h.next(); grids.push(h.reading().split('\\n')); + h.next(); grids.push(h.reading().split('\\n')); + el.remove(); + for (const g of grids) { + if (new Set(g).size !== 3) return 'rows not distinct: ' + g.join(','); + if (!g.every(w => pool.includes(w.trim()))) return 'word outside pool: ' + g.join(','); + } + for (let i = 1; i < grids.length; i++) + for (let r = 0; r < 3; r++) + if (grids[i][r] === grids[i - 1][r]) return 'row ' + r + ' kept its word: ' + grids[i][r]; + return 'ok'; + })()`); + ok('N20 scramble sends every row to a different pool word', n20s === 'ok', n20s); + // animate:false (the reduced-motion path) jumps straight to the target. const n20c = await evl(`(() => { const el = document.createElement('div'); document.body.appendChild(el); - const h = GW.splitFlap(el, { animate: false }); + const settles = []; + const h = GW.splitFlap(el, { animate: false, onSettle: r => settles.push(r) }); h.set(1); - const r = h.reading(); el.remove(); return r; + const r = h.reading(); el.remove(); + return { r, settles }; + })()`); + ok('N20 animate:false jumps straight to the target', n20c.r.trim() === 'LINK', n20c.r); + ok('N20 settle announces once per command', n20c.settles.length === 1 && n20c.settles[0].trim() === 'LINK', + JSON.stringify(n20c.settles)); + + // The card is a three-row six-letter board; the demo draws random pool words. + const n20d = await evl(`(() => { + const h = document.querySelector('#card-N20').gw; + return { cells: document.querySelectorAll('#card-N20 .flapd').length, + lines: document.querySelectorAll('#card-N20 .flapline').length, + readLines: h.reading().split('\\n').length }; })()`); - ok('N20 animate:false jumps straight to the target', n20c.trim() === 'LINK', n20c); + ok('N20 board is three rows of six cells', n20d.cells === 18 && n20d.lines === 3 && n20d.readLines === 3, + JSON.stringify(n20d)); - // Unpark the ticker and restore the demo state it expects. - await evl(`(() => { IH.flap = window.__realFlap; IH.flap.set(0); })()`); + // Skin axis: dark (cream on black) and the inverse light (black on cream), + // switched by the chip rig via setStyle, computed ink actually inverting. + const n20e = await evl(`(() => { + const h = document.querySelector('#card-N20').gw; + if (typeof h.setStyle !== 'function' || !GW.splitFlap.STYLES || !GW.splitFlap.STYLES.skin) return 'no skin axis'; + const skins = Object.keys(GW.splitFlap.STYLES.skin); + if (!['dark','white','light','paper'].every(k => skins.includes(k))) return 'skins are: ' + skins.join(','); + const ink = () => getComputedStyle(document.querySelector('#card-N20 .fh')).color; + const lum = c => { const m = c.match(/[0-9]+/g).map(Number); return m[0] + m[1] + m[2]; }; + const dark = ink(); + h.setStyle('skin', 'white'); + const white = ink(); + h.setStyle('skin', 'light'); + const light = ink(); + const gotClass = document.querySelector('#card-N20 .flap').classList.contains('flap-light'); + h.setStyle('skin', 'paper'); + const paper = ink(); + const paperClass = document.querySelector('#card-N20 .flap').classList.contains('flap-paper'); + h.setStyle('skin', 'dark'); + const back = ink(); + if (!gotClass) return 'light skin class missing'; + if (!paperClass) return 'paper skin class missing'; + if (white !== 'rgb(255, 255, 255)') return 'white skin ink is ' + white; + if (!(lum(dark) > 400 && lum(dark) < 760 && lum(light) < 300 && lum(paper) < 300)) return 'inks wrong: ' + dark + ' / ' + light + ' / ' + paper; + if (back !== dark) return 'did not restore dark: ' + back; + return 'ok'; + })()`); + ok('N20 skins: cream dark, white ink, ivory board, white board — all switch and restore', n20e === 'ok', n20e); + + const n20fnt = await evl(`(() => { + const h = document.querySelector('#card-N20').gw; + if (!GW.splitFlap.STYLES.font) return 'no font axis'; + const fam = () => getComputedStyle(document.querySelector('#card-N20 .fh')).fontFamily; + const mono = fam(); + h.setStyle('font', 'helv'); + const helv = fam(); + h.setStyle('font', 'mono'); + const back = fam(); + if (!/helvetica/i.test(helv)) return 'helv did not apply: ' + helv; + if (!/berkeley|mono/i.test(mono) || back !== mono) return 'mono wrong or not restored: ' + mono + ' / ' + back; + return 'ok'; + })()`); + ok('N20 font switches between Berkeley Mono and Helvetica', n20fnt === 'ok', n20fnt); + const n20f = await evl(`(() => { + const chips = [...document.querySelectorAll('#card-N20 .famchips .fc, #card-N20 .famchips [title]')].map(c => c.title || c.textContent); + return chips.length ? chips.join(',') : 'no chips'; + })()`); + ok('N20 card offers skin and font chips', + ['dark','white','light','paper','mono','helv'].every(t => n20f.includes(t)), n20f); + const n20h = await evl(`(() => { + const rd = document.querySelector('#card-N20 .wrd'); + const slider = document.querySelector('#card-N20 .famchips input[type=range]'); + if (rd && getComputedStyle(rd).display !== 'none') return 'readout still visible'; + if (!slider) return 'slider not in the chips row'; + return 'ok'; + })()`); + ok('N20 board is the only display: readout hidden, slider rides with the chips', n20h === 'ok', n20h); + + // Flap speed: the handle takes setFlapMs and the card's slider drives it. + const n20v = await evl(`(() => { + const h = document.querySelector('#card-N20').gw; + if (typeof h.setFlapMs !== 'function' || typeof h.flapMs !== 'function') return 'no speed api'; + const before = h.flapMs(); + h.setFlapMs(120); + if (h.flapMs() !== 120) return 'setFlapMs did not take: ' + h.flapMs(); + const slider = document.querySelector('#card-N20 input[type=range]'); + if (!slider) { h.setFlapMs(before); return 'no slider on the card'; } + slider.value = '90'; slider.dispatchEvent(new Event('input', { bubbles: true })); + const viaSlider = h.flapMs(); + h.setFlapMs(before); slider.value = String(before); + return viaSlider === 90 ? 'ok' : 'slider did not drive setFlapMs: ' + viaSlider; + })()`); + ok('N20 flap speed: setFlapMs api + card slider drive the rate', n20v === 'ok', n20v); + + // Unpark the ticker, restore the demo state, and measure the dwell: the + // board must settle, hold the reading ~2s, and only then take the next update. + await evl(`(() => { + IH.flap = window.__realFlap; + const h = document.querySelector('#card-N20').gw; + window.__n20dwell = { settleAt: 0, changeAt: 0, settled: '' }; + h.onSettle(r => { if (!window.__n20dwell.settleAt) { window.__n20dwell.settleAt = performance.now(); window.__n20dwell.settled = r; } }); + window.__n20dwell.timer = setInterval(() => { + const d = window.__n20dwell; + if (d.settleAt && !d.changeAt && h.reading() !== d.settled) { d.changeAt = performance.now(); clearInterval(d.timer); } + }, 50); + h.set(0); + })()`); + await sleep(9000); + const dwell = await evl(`(() => { + const d = window.__n20dwell; clearInterval(d.timer); + return { settleAt: d.settleAt, delta: d.changeAt ? d.changeAt - d.settleAt : -1 }; + })()`); + ok('N20 settles, then dwells ~2s before the next update', + dwell.settleAt > 0 && dwell.delta >= 1800 && dwell.delta <= 4000, 'delta ' + Math.round(dwell.delta)); + + // Skin chips carry visible swatch dots (the rig paints STYLES[..].dot; + // a string-valued STYLES entry leaves both chips transparent and identical). + const n20g = await evl(`(() => { + const chips = [...document.querySelectorAll('#card-N20 .famchips .fc')]; + if (chips.length < 6) return 'chips missing: ' + chips.length; + // each chip is a miniature flap cell: board colour + a letter in the ink + const pair = c => getComputedStyle(c).backgroundColor + '/' + getComputedStyle(c, '::after').color; + const skins = chips.slice(0, 4).map(pair); + if (new Set(skins).size !== 4) return 'skin previews not distinct: ' + skins.join(' '); + if (chips.slice(0, 4).some(c => getComputedStyle(c, '::after').content === 'none')) return 'skin chip has no ink letter'; + const fams = chips.slice(4).map(c => getComputedStyle(c, '::after').fontFamily); + if (!/helvetica/i.test(fams[1]) || fams[0] === fams[1]) return 'font previews wrong: ' + fams.join(' vs '); + const size = parseFloat(getComputedStyle(chips[0]).width); + return size >= 15 ? 'ok' : 'chips too small to read: ' + size; + })()`); + ok('N20 chips are miniature flap cells: board colour, ink letter, real face', n20g === 'ok', n20g); + + // Under prefers-reduced-motion the board paints once and RESTS — the dwell + // chain must stay gated off, or the panel self-advances every 2s forever. + await send('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-reduced-motion', value: 'reduce' }] }); + await send('Page.reload'); + await sleep(2500); + const rm1 = await evl(`document.querySelector('#card-N20').gw.reading()`); + await sleep(5200); + const rm2 = await evl(`(() => ({ r: document.querySelector('#card-N20').gw.reading(), anims: document.getAnimations().length }))()`); + ok('N20 rests under reduced motion (no self-advancing dwell chain)', + rm1 === rm2.r && rm2.anims === 0, JSON.stringify({ before: rm1, after: rm2.r, anims: rm2.anims })); // late exceptions from interactions const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown'); |
