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.mjs198
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');