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.mjs664
1 files changed, 538 insertions, 126 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 051d575..3db09a5 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -1,4 +1,4 @@
-// CDP probe for the widget gallery — no playwright, Node global WebSocket/fetch.
+// CDP probe for the instrument gallery — no playwright, Node global WebSocket/fetch.
// Usage: node probe.mjs [--shot out.png] [--size N]
import { spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
@@ -65,7 +65,7 @@ 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. card count (the widgets/row default is checked in 1h)
+ // 1. card count (the instruments/row default is checked in 1h)
const cards = await evl(`document.querySelectorAll('.card').length`);
ok('111 cards', cards === 111, `got ${cards}`);
@@ -89,7 +89,7 @@ try {
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'));
+ const built = Object.keys(DUPRE.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);
@@ -97,7 +97,7 @@ try {
// 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 chip = document.querySelector('#card-07 .dupre-chip');
const lit = () => getComputedStyle(chip).color;
const before = lit();
document.querySelector('#card-07 .fc[title="vfd"]').click();
@@ -107,7 +107,7 @@ try {
})()`);
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
+ // 1e. The colour-policy backbone. Every instrument'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
@@ -116,10 +116,10 @@ try {
// 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;
+ const KINDS = DUPRE.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 fns = Object.entries(DUPRE).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)
@@ -177,27 +177,27 @@ try {
})()`);
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
+ // 1h. instruments/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()`);
+ ok('default is 3 instruments/row', await evl(`document.body.dataset.cols`) === '3');
+ await evl(`document.querySelector('.szbar .dupre-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()`);
+ await evl(`document.querySelector('.szbar .dupre-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');
+ ok('instruments/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.querySelector('.szbar .dupre-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];})()`);
+ const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.dupre-fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
const before = await evl(`document.getElementById('rd-03').textContent`);
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);
@@ -218,21 +218,21 @@ try {
// 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
// the generic engaged look. Craig's call, 2026-07-16.
- const order = await evl(`[...document.querySelectorAll('#card-02 .key')].map(b => b.textContent)`);
+ const order = await evl(`[...document.querySelectorAll('#card-02 .dupre-key')].map(b => b.textContent)`);
ok('card 02 keys read SCAN, LIVE, MUTED',
JSON.stringify(order) === JSON.stringify(['SCAN', 'LIVE', 'MUTED']), JSON.stringify(order));
const engaged = await evl(`(()=>{
- const lit = [...document.querySelectorAll('#card-02 .key')].filter(b => /\\b(on|green|red)\\b/.test(b.className));
- return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('key ','') : 'lit=' + lit.length;
+ const lit = [...document.querySelectorAll('#card-02 .dupre-key')].filter(b => /\\bdupre-(on|green|red)\\b/.test(b.className));
+ return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('dupre-key dupre-','') : 'lit=' + lit.length;
})()`);
ok('card 02 engages LIVE in green by default', engaged === 'LIVE:green', engaged);
const muted = await evl(`(()=>{
- const keys = [...document.querySelectorAll('#card-02 .key')];
+ const keys = [...document.querySelectorAll('#card-02 .dupre-key')];
keys.find(b => b.textContent === 'MUTED').click();
- const lit = keys.filter(b => /\\b(on|green|red)\\b/.test(b.className));
- return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('key ','') : 'lit=' + lit.length;
+ const lit = keys.filter(b => /\\bdupre-(on|green|red)\\b/.test(b.className));
+ return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('dupre-key dupre-','') : 'lit=' + lit.length;
})()`);
ok('card 02 MUTED engages red and releases LIVE', muted === 'MUTED:red', muted);
@@ -245,7 +245,7 @@ try {
// disengaged state instead. dim offText lets an off toggle recede in a dense
// panel. Craig's call, 2026-07-16.
const atoms = await evl(`(()=>{
- const S = GW.slideToggle.STYLES;
+ const S = DUPRE.slideToggle.STYLES;
const want = { on: ['amber','green','dark','red','warn'], onText: ['panel','cream','green'],
off: ['dark','red'], offText: ['white','red','black','dim'],
thumb: ['light','dark','chrome','brass'] };
@@ -260,9 +260,9 @@ try {
// must resolve in STYLES — a typo here would silently fall through to the
// stylesheet default and look "nearly right", which is the worst outcome.
const presets = await evl(`(()=>{
- const P = GW.slideToggle.PRESETS, S = GW.slideToggle.STYLES;
+ const P = DUPRE.slideToggle.PRESETS, S = DUPRE.slideToggle.STYLES;
if (!P) return 'no PRESETS';
- const AX = GW.slideToggle.AXIS_ORDER;
+ const AX = DUPRE.slideToggle.AXIS_ORDER;
const bad = [];
for (const [name, p] of Object.entries(P)) {
for (const ax of AX) {
@@ -279,7 +279,7 @@ try {
// the only thing carrying state. Both backgrounds must stay dark while the
// inks diverge — if either pill lights, the preset has lost its point.
const dark = await evl(`(()=>{
- const P = GW.slideToggle.PRESETS.dark, S = GW.slideToggle.STYLES;
+ const P = DUPRE.slideToggle.PRESETS.dark, S = DUPRE.slideToggle.STYLES;
const onBg = S.on[P.on].vars['--sw-on-bg'], offBg = S.off[P.off].vars['--sw-off-bg'];
const onInk = S.onText[P.onText].vars['--sw-on-ink'], offInk = S.offText[P.offText].vars['--sw-off-ink'];
if (onBg !== offBg) return 'pills differ: on=' + onBg + ' off=' + offBg;
@@ -288,7 +288,7 @@ try {
})()`);
ok('dark preset keeps both pills dark and the inks distinct', dark === 'ok', dark);
- // 7b. the card claims the preset it is actually in. The widget defaults to
+ // 7b. the card claims the preset it is actually in. The instrument defaults to
// `panel`, so a preset group with nothing lit would assert "no preset
// active" — false, and exactly the kind of quiet mislabel this walk exists
// to catch. Nothing above this point touches card 01's chips (checks 2-4
@@ -304,8 +304,8 @@ try {
})()`);
ok('card 01 defaults to the panel preset', defaultPreset === 'panel', defaultPreset);
- // 8. a preset chip drives the widget AND re-syncs the axis chips, so the card
- // never shows a combination the widget isn't in.
+ // 8. a preset chip drives the instrument AND re-syncs the axis chips, so the card
+ // never shows a combination the instrument isn't in.
const applied = await evl(`(()=>{
const card = document.getElementById('card-01');
const groups = [...card.querySelectorAll('.fgroup')];
@@ -319,7 +319,7 @@ try {
const brd = card.querySelector('.switch').style.getPropertyValue('--sw-on-brd');
return (lit.length === 1 ? lit[0].title : 'lit=' + lit.length) + '|' + brd;
})()`);
- ok('armed preset drives widget and syncs the on chip', applied === 'red|var(--fail)', applied);
+ ok('armed preset drives instrument and syncs the on chip', applied === 'red|var(--fail)', applied);
// 8b. axes are not independent: onText overrides the ink `on` sets, so changing
// `on` after picking an onText must not silently revert the legend while its
@@ -360,7 +360,7 @@ try {
// the operator can't be assumed to touch-type, and it is what Craig's
// reference photos show. A QWERTY drift here would silently lose the idiom.
const abcOrder = await evl(`(()=>{
- const letters = [...document.querySelectorAll('#card-R57 .kp-key')]
+ const letters = [...document.querySelectorAll('#card-R57 .dupre-kp-key')]
.map(k => k.dataset.k).filter(k => /^[A-Z]$/.test(k));
const want = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];
return JSON.stringify(letters) === JSON.stringify(want)
@@ -369,7 +369,7 @@ try {
ok('R57 carries A-Z in alphabetical order', abcOrder === 'ok', abcOrder);
const abcDigits = await evl(`(()=>{
- const d = [...document.querySelectorAll('#card-R57 .kp-key')]
+ const d = [...document.querySelectorAll('#card-R57 .dupre-kp-key')]
.map(k => k.dataset.k).filter(k => /^[0-9]$/.test(k));
return d.length === 10 ? 'ok' : 'got ' + d.length + ': ' + d.join('');
})()`);
@@ -383,7 +383,7 @@ try {
// start at column 3 while M-X started at column 0).
const layout = await evl(`(()=>{
const x = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? Math.round(g.querySelector('rect').getBBox().x) : null;
};
const colStarts = ['A','D','G','J','M','S','Y'].map(x);
@@ -400,7 +400,7 @@ try {
// deliberate inversion of where they started, easy to "tidy" back.
const reach = await evl(`(()=>{
const box = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? g.querySelector('rect').getBBox() : null;
};
const del = box('DEL'), clr = box('CLR'), ent = box('ENT');
@@ -418,7 +418,7 @@ try {
// the palette may be retuned, the distinction may not collapse.
const ladder = await evl(`(()=>{
const fill = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? g.querySelector('rect').getAttribute('fill') : null;
};
const f = { DEL: fill('DEL'), CLR: fill('CLR'), ENT: fill('ENT'), plain: fill('A'), digit: fill('1') };
@@ -432,7 +432,7 @@ try {
// 11. typing accumulates, in order. A keypad that registers presses but drops
// or reorders them is the failure that matters for a password field.
const typed = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
for (const c of ['W','I','F','I','7']) key(c).dispatchEvent(new MouseEvent('click', {bubbles:true}));
return document.getElementById('rd-R57').textContent;
@@ -443,7 +443,7 @@ try {
// wiping the whole entry, which on a 20-character passphrase means
// starting over — so the check that matters is that DEL is not CLR.
const del = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
click('CLR');
for (const c of ['C','A','B','S']) click(c);
@@ -466,7 +466,7 @@ try {
// passphrase they can't read back. Checked past the 13-char truncation
// boundary, where there are no pad dots left for a space to displace.
const space = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
const win = () => document.querySelector('#card-R57 text[font-size="14"]').textContent;
click('CLR');
@@ -486,7 +486,7 @@ try {
// the checks above — they mutate it, so a check that assumed their leftovers
// would pass or fail on their behaviour instead of its own.
const committed = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
click('CLR');
for (const c of ['N','E','T','5']) click(c);
@@ -505,10 +505,10 @@ try {
// 13. KEYS is a declarative TABLE, not a function over a DOM event. The Emacs
// port installs this same table into a keymap — it never sees a keydown —
- // so a function here would force it to re-derive the widget's intent and
+ // so a function here would force it to re-derive the instrument's intent and
// the two bindings would drift. (README, keyboard contract.)
const keysTable = await evl(`(()=>{
- const K = GW.abcKeypad.KEYS;
+ const K = DUPRE.abcKeypad.KEYS;
if (!K) return 'no KEYS';
if (typeof K !== 'object' || Array.isArray(K)) return 'KEYS is not a plain table: ' + typeof K;
const missing = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'].filter(c => K[c] !== c);
@@ -519,11 +519,11 @@ try {
})()`);
ok('R57 KEYS is a declarative table covering the plate', keysTable === 'ok', keysTable);
- // 13b. THE contract's first rule: no document-level listener. A widget that
+ // 13b. THE contract's first rule: no document-level listener. An instrument that
// binds globally types into itself from anywhere on a 110-card page.
// Typing at the body with the card unfocused must do nothing at all.
const unfocused = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
document.activeElement.blur();
for (const c of ['A','B','C']) document.body.dispatchEvent(
@@ -535,8 +535,8 @@ try {
// 13c. Focused, the same keys land — and land through press(), so click and key
// cannot drift apart.
const typedByKey = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
@@ -554,18 +554,18 @@ try {
// identical buffer and readout. Duplicated logic in the handler would
// pass every check above this one and fail here.
const drift = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const rd = () => document.getElementById('rd-R57').textContent;
const seq = ['A','B','SPC','7'];
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
seq.forEach(k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true})));
- const byClick = rd() + '/' + document.getElementById('card-R57').gw.get();
+ const byClick = rd() + '/' + document.getElementById('card-R57').dupre.get();
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
['A','B',' ','7'].forEach(k =>
pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true })));
- const byKey = rd() + '/' + document.getElementById('card-R57').gw.get();
+ const byKey = rd() + '/' + document.getElementById('card-R57').dupre.get();
return byClick === byKey ? 'ok' : 'drift: click=' + byClick + ' key=' + byKey;
})()`);
ok('R57 click and key land in the same place', drift === 'ok', drift);
@@ -576,9 +576,9 @@ try {
// caller is a hole in exactly the target the table exists for.
const pressGuard = await evl(`(()=>{
const card = document.getElementById('card-R57');
- const h = card.gw;
+ const h = card.dupre;
if (!h || !h.press) return 'no handle';
- const key = k => [...card.querySelectorAll('.kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...card.querySelectorAll('.dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
h.press('F1'); h.press('ArrowLeft'); h.press('');
const junk = h.get();
@@ -590,7 +590,7 @@ try {
// 13c-4. Every key the table maps must be a real plate action, or the Emacs
// port installs a binding that silently does nothing.
const keysSubset = await evl(`(()=>{
- const bad = Object.entries(GW.abcKeypad.KEYS).filter(([,v]) => !GW.abcKeypad.ACTIONS.has(v));
+ const bad = Object.entries(DUPRE.abcKeypad.KEYS).filter(([,v]) => !DUPRE.abcKeypad.ACTIONS.has(v));
return bad.length ? 'KEYS maps to non-actions: ' + JSON.stringify(bad) : 'ok';
})()`);
ok('R57 every KEYS value is a real plate action', keysSubset === 'ok', keysSubset);
@@ -598,9 +598,9 @@ try {
// 13d. preventDefault is spent only where there is a default worth killing.
// Space scrolls and Backspace navigates back, so those are claimed; Tab is
// how the page is navigable and Escape belongs to the audit stepper, so a
- // widget that swallows either breaks something it cannot see.
+ // instrument that swallows either breaks something it cannot see.
const defaults = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
pad.focus();
const fired = k => {
const e = new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true });
@@ -621,8 +621,8 @@ try {
// 13e. press() is an allowlist, not a mailbox: an unmapped key must not append.
const unmapped = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
['F1','ArrowLeft','Home','é','!'].forEach(k =>
@@ -637,12 +637,12 @@ try {
// click ever prints, the card has silently become a keypad with extra steps.
// Reads the BUFFER, not the card readout: the readout is supposed to change
// while hunting ("stylus over g"), and asserting on it would fail a correct
- // widget for showing the operator where the pointer is.
+ // instrument for showing the operator where the pointer is.
const grammar = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
- const buf = () => document.getElementById('card-R58').gw.get();
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
+ const buf = () => document.getElementById('card-R58').dupre.get();
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
const empty = buf();
['M','i','g'].forEach(c => cell(c).dispatchEvent(new MouseEvent('click', {bubbles:true})));
const afterSelecting = buf();
@@ -654,9 +654,9 @@ try {
// 14b. The lever prints whatever the stylus is resting on, once per pull — the
// operator's two hands are two separate acts.
const spelled = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
for (const c of ['M','i','g','n','o','n']) {
cell(c).dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
@@ -668,13 +668,13 @@ try {
// 14c. Pulling the lever twice prints the character twice: the selection stays
// put, which is what lets you type 'ss' without re-aiming.
const repeat = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
cell('s').dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
- return document.getElementById('card-R58').gw.get();
+ return document.getElementById('card-R58').dupre.get();
})()`);
/* Exact, on the buffer. /ss$/ on the readout also matched 'sss', so it passed
even if selecting printed — unable to fail on the one bug this card is about. */
@@ -690,7 +690,7 @@ try {
// 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 have = new Set([...document.querySelectorAll('#card-R58 .dupre-ix-cell')].map(c => c.dataset.c));
const need = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz',
...'23456789', 'ä','ö','ü','§','½','¼','¾'];
const missing = need.filter(c => !have.has(c));
@@ -707,7 +707,7 @@ try {
// 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);
+ const g = [...document.querySelectorAll('#card-R58 .dupre-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('&');
@@ -722,10 +722,10 @@ try {
// redraw. Craig has already said the keys will change; a layout welded into
// the drawing is one that never does.
const layoutData = await evl(`(()=>{
- const L = GW.indexPlate && GW.indexPlate.LAYOUT;
+ const L = DUPRE.indexPlate && DUPRE.indexPlate.LAYOUT;
if (!L) return 'no LAYOUT';
if (!Array.isArray(L) || !L.every(Array.isArray)) return 'LAYOUT is not a grid';
- const cells = document.querySelectorAll('#card-R58 .ix-cell').length;
+ const cells = document.querySelectorAll('#card-R58 .dupre-ix-cell').length;
const declared = L.flat().filter(Boolean).length;
return cells === declared ? 'ok' : 'drawn ' + cells + ' but declared ' + declared;
})()`);
@@ -738,10 +738,10 @@ try {
// controls back onto the characters.
const clear = await evl(`(()=>{
const box = sel => { const e = document.querySelector('#card-R58 ' + sel); return e ? e.getBBox() : null; };
- const cells = [...document.querySelectorAll('#card-R58 .ix-cell')].map(c => c.getBBox());
+ const cells = [...document.querySelectorAll('#card-R58 .dupre-ix-cell')].map(c => c.getBBox());
const right = Math.max(...cells.map(b => b.x + b.width));
const hits = [];
- for (const sel of ['.ix-lever', '.ix-clear']) {
+ for (const sel of ['.dupre-ix-lever', '.dupre-ix-clear']) {
const b = box(sel);
if (!b) { hits.push(sel + ' missing'); continue; }
if (b.x < right) hits.push(sel + ' starts at ' + Math.round(b.x) + ', left of the plate edge ' + Math.round(right));
@@ -757,7 +757,7 @@ try {
// shrink was the trap, which is why VH now takes a floor.
const stack = await evl(`(()=>{
const bb = sel => { const e = document.querySelector('#card-R58 ' + sel); return e ? e.getBBox() : null; };
- const lever = bb('.ix-lever'), clr = bb('.ix-clear');
+ const lever = bb('.dupre-ix-lever'), clr = bb('.dupre-ix-clear');
const legend = [...document.querySelectorAll('#card-R58 text')].find(t => t.textContent === 'PRINT');
if (!lever || !clr || !legend) return 'missing gutter part';
const lg = legend.getBBox();
@@ -766,7 +766,7 @@ try {
if (overlaps(lever, clr)) return 'lever overlaps CLR';
if (overlaps(lg, clr)) return 'PRINT legend overlaps CLR';
if (overlaps(lg, lever)) return 'PRINT legend overlaps the lever';
- const vb = document.querySelector('#card-R58 .ix-pad').viewBox.baseVal;
+ const vb = document.querySelector('#card-R58 .dupre-ix-pad').viewBox.baseVal;
if (clr.y + clr.height > vb.height) return 'CLR falls outside the viewBox';
return 'ok';
})()`);
@@ -778,7 +778,7 @@ try {
// jump the stylus across the plate to the second. 14e can't see it — a
// duplicate inflates the drawn count and the declared count equally.
const dupes = await evl(`(()=>{
- const flat = GW.indexPlate.LAYOUT.flat().filter(Boolean);
+ const flat = DUPRE.indexPlate.LAYOUT.flat().filter(Boolean);
const seen = new Set(), dup = new Set();
for (const c of flat) (seen.has(c) ? dup : seen).add(c);
return dup.size ? 'duplicated on the plate: ' + [...dup].join(' ') : 'ok';
@@ -788,7 +788,7 @@ try {
// 14i. press() is the allowlist for R58 too. R57 has this check; without it,
// press('F1') reaching select() unfiltered would ship green.
const ixPress = await evl(`(()=>{
- const h = document.getElementById('card-R58').gw;
+ const h = document.getElementById('card-R58').dupre;
h.press('CLR');
['F1', 'PRINT ', '', 'constructor', 'ZZ'].forEach(k => h.press(k));
return JSON.stringify([h.get(), h.selected()]);
@@ -799,7 +799,7 @@ try {
// relaying the plate can't leave a keybinding pointing at a character the
// plate no longer carries. Enter is the lever.
const ixKeys = await evl(`(()=>{
- const K = GW.indexPlate.KEYS, L = GW.indexPlate.LAYOUT;
+ const K = DUPRE.indexPlate.KEYS, L = DUPRE.indexPlate.LAYOUT;
if (!K) return 'no KEYS';
const chars = L.flat().filter(Boolean);
const missing = chars.filter(c => K[c] !== c);
@@ -815,10 +815,10 @@ try {
// quietly become R57 with a nicer plate, and the one thing it exists to
// demonstrate is gone.
const ixType = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
- const h = document.getElementById('card-R58').gw;
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
+ const h = document.getElementById('card-R58').dupre;
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
['M','i','g'].forEach(send);
const afterTyping = h.get();
@@ -833,10 +833,10 @@ try {
// uppercase; this one must not, and that difference is the plate's whole
// argument for having no shift key.
const ixCase = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
- const h = document.getElementById('card-R58').gw;
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
+ const h = document.getElementById('card-R58').dupre;
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
send('a'); send('Enter');
send('A'); send('Enter');
@@ -848,7 +848,7 @@ try {
// always. (The missing space cell is a known gap — the real Mignon has a
// separate space key.)
const ixSpace = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
pad.focus();
const e = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true });
pad.dispatchEvent(e);
@@ -857,13 +857,13 @@ try {
ok('R58 leaves Space alone (not on the plate)', ixSpace === 'ok', ixSpace);
// 15e. Unfocused, it hears nothing — the contract's first rule, on the second
- // widget to take keys.
+ // instrument to take keys.
// Asserts nothing CHANGED, rather than expecting a cleared selection: CLR
// is fresh paper, and fresh paper doesn't move the operator's hand, so the
// stylus legitimately stays where the previous check left it.
const ixBlur = await evl(`(()=>{
- const h = document.getElementById('card-R58').gw;
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const h = document.getElementById('card-R58').dupre;
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
document.activeElement.blur();
const before = JSON.stringify([h.get(), h.selected()]);
['Q','Z'].forEach(k => document.body.dispatchEvent(
@@ -877,7 +877,7 @@ try {
// through the declared charset one flap at a time, retarget cleanly, and
// animate:false jumps straight to the target.
const flapApi = await evl(`(() => {
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
return { reading: typeof h.reading, chars: typeof h.chars, set: typeof h.set };
})()`);
ok('N20 handle exposes reading() and chars', flapApi.reading === 'function' && flapApi.chars === 'string',
@@ -889,12 +889,12 @@ try {
// 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);
+ await evl(`document.querySelector('#card-N20').dupre.setText('SYSTEM\\nVOLUME\\nSIGNAL')`);
+ await sleep(6500);
// Start a cascade and sample the displayed reading every 15ms until stable.
await evl(`(() => {
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
window.__n20 = { samples: [], anims: 0 };
h.setText('VOLUME\\nSIGNAL\\nRECORD'); // multi-step distances on several cells
window.__n20.n = 0;
@@ -908,10 +908,10 @@ try {
window.__n20.anims++;
}, 15);
})()`);
- await sleep(5400);
+ await sleep(6500);
const n20 = await evl(`(() => {
clearInterval(window.__n20.timer);
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
return { samples: [...new Set(window.__n20.samples)], final: h.reading(), anims: window.__n20.anims,
chars: h.chars };
})()`);
@@ -933,15 +933,15 @@ try {
// Retarget mid-cascade: aim at word 2, then word 3 while still spinning; must land on 3.
await evl(`(() => {
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
window.__n20r = { samples: [] };
window.__n20r.timer = setInterval(() => window.__n20r.samples.push(h.reading()), 15);
h.setText('SIGNAL\\nRECORD\\nONLINE'); setTimeout(() => h.setText('RECORD\\nONLINE\\nSTEREO'), 180);
})()`);
- await sleep(5800);
+ await sleep(7000);
const n20b = await evl(`(() => {
clearInterval(window.__n20r.timer);
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
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 };
@@ -955,7 +955,7 @@ try {
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 h = DUPRE.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'));
@@ -975,7 +975,7 @@ try {
const n20c = await evl(`(() => {
const el = document.createElement('div'); document.body.appendChild(el);
const settles = [];
- const h = GW.splitFlap(el, { animate: false, onSettle: r => settles.push(r) });
+ const h = DUPRE.splitFlap(el, { animate: false, onSettle: r => settles.push(r) });
h.set(1);
const r = h.reading(); el.remove();
return { r, settles };
@@ -986,7 +986,7 @@ try {
// 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;
+ const h = document.querySelector('#card-N20').dupre;
return { cells: document.querySelectorAll('#card-N20 .flapd').length,
lines: document.querySelectorAll('#card-N20 .flapline').length,
readLines: h.reading().split('\\n').length };
@@ -997,47 +997,48 @@ try {
// 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 (skins[0] !== 'white') return 'white must list first, got: ' + skins.join(',');
+ const h = document.querySelector('#card-N20').dupre;
+ if (typeof h.setStyle !== 'function' || !DUPRE.splitFlap.STYLES || !DUPRE.splitFlap.STYLES.skin) return 'no skin axis';
+ const skins = Object.keys(DUPRE.splitFlap.STYLES.skin);
+ if (skins[0] !== 'paper') return 'paper must list first, got: ' + skins.join(',');
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 white = ink(); // the card's default skin is white ink
- if (white !== 'rgb(255, 255, 255)') return 'default ink is not white: ' + white;
+ const paperDef = ink(); // the card's default skin is the white card, dark letters
+ if (lum(paperDef) >= 300) return 'default ink is not dark-on-paper: ' + paperDef;
+ if (!document.querySelector('#card-N20 .flap').classList.contains('flap-paper')) return 'default skin class is not paper';
+ h.setStyle('skin', 'white');
+ const white = ink();
+ if (white !== 'rgb(255, 255, 255)') return 'white skin ink is ' + white;
h.setStyle('skin', 'dark');
const cream = 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', 'white');
const back = ink();
if (!gotClass) return 'light skin class missing';
- if (!paperClass) return 'paper skin class missing';
- if (!(lum(cream) > 400 && lum(cream) < 760 && lum(light) < 300 && lum(paper) < 300)) return 'inks wrong: ' + cream + ' / ' + light + ' / ' + paper;
- if (back !== white) return 'did not restore the white default: ' + back;
+ if (!(lum(cream) > 400 && lum(cream) < 760 && lum(light) < 300)) return 'inks wrong: ' + cream + ' / ' + light;
+ if (back !== paperDef) return 'did not restore the paper default: ' + back;
return 'ok';
})()`);
- ok('N20 skins: white default first, cream, ivory board, white board — all switch and restore', n20e === 'ok', n20e);
+ ok('N20 skins: paper default first, white ink, cream, ivory — 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 h = document.querySelector('#card-N20').dupre;
+ if (!DUPRE.splitFlap.STYLES.font) return 'no font axis';
const fam = () => getComputedStyle(document.querySelector('#card-N20 .fh')).fontFamily;
+ const helv = fam(); // the card's default face is Helvetica
+ if (!/helvetica/i.test(helv)) return 'default face is not helvetica: ' + helv;
+ h.setStyle('font', 'mono');
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;
+ if (!/berkeley|mono/i.test(mono) || /helvetica/i.test(mono)) return 'mono did not apply: ' + mono;
+ if (back !== helv) return 'did not restore the helv default: ' + back;
return 'ok';
})()`);
- ok('N20 font switches between Berkeley Mono and Helvetica', n20fnt === 'ok', n20fnt);
+ ok('N20 face: Helvetica default, Berkeley Mono one click away', 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';
@@ -1055,10 +1056,10 @@ try {
// Flap speed: the handle takes setFlapMs and the card's slider drives it.
const n20v = await evl(`(() => {
- const h = document.querySelector('#card-N20').gw;
+ const h = document.querySelector('#card-N20').dupre;
if (typeof h.setFlapMs !== 'function' || typeof h.flapMs !== 'function') return 'no speed api';
const before = h.flapMs();
- if (before !== 85) return 'default flap rate is ' + before + ', want 85';
+ if (before !== 100) return 'default flap rate is ' + before + ', want 100';
h.setFlapMs(120);
if (h.flapMs() !== 120) return 'setFlapMs did not take: ' + h.flapMs();
const slider = document.querySelector('#card-N20 input[type=range]');
@@ -1074,7 +1075,7 @@ try {
// 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;
+ const h = document.querySelector('#card-N20').dupre;
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(() => {
@@ -1083,7 +1084,7 @@ try {
}, 50);
h.set(0);
})()`);
- await sleep(9000);
+ await sleep(10500);
const dwell = await evl(`(() => {
const d = window.__n20dwell; clearInterval(d.timer);
return { settleAt: d.settleAt, delta: d.changeAt ? d.changeAt - d.settleAt : -1 };
@@ -1102,20 +1103,39 @@ try {
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 ');
+ if (!fams.some(f => /helvetica/i.test(f)) || 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);
+ // N24 jewels — the handle reads and drives lamp state. Waybar-bound: a
+ // consumer must be able to set a jewel without synthesizing clicks.
+ const jw = await evl(`(() => {
+ const h = document.getElementById('card-N24').dupre;
+ if (!h.get || !h.set) return 'no get/set on handle';
+ const init = h.get().join(',');
+ if (init !== '0,1,2,-1') return 'initial ' + init;
+ h.set(3, 1);
+ if (h.get()[3] !== 1) return 'set(3,1) not reflected: ' + h.get().join(',');
+ const el = document.querySelectorAll('#card-N24 .dupre-jewel')[3];
+ if (!el) return 'no .dupre-jewel elements';
+ if (el.classList.contains('dupre-dim')) return 'jewel 3 still dark after set(3,1)';
+ h.set(3, -1);
+ if (!el.classList.contains('dupre-dim')) return 'set(3,-1) did not dark';
+ if (h.get()[3] !== -1) return 'get after dark: ' + h.get().join(',');
+ return 'ok';
+ })()`);
+ ok('N24 jewels handle exposes get/set', jw === 'ok', jw);
+
// 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()`);
+ const rm1 = await evl(`document.querySelector('#card-N20').dupre.reading()`);
await sleep(5200);
- const rm2 = await evl(`(() => ({ r: document.querySelector('#card-N20').gw.reading(), anims: document.getAnimations().length }))()`);
+ const rm2 = await evl(`(() => ({ r: document.querySelector('#card-N20').dupre.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 }));
@@ -1131,6 +1151,398 @@ try {
})()`);
ok('every card ID badge links to its own anchor', anchors === 'ok', anchors);
+ // Batch-3 set() hardening: out-of-range input cannot wedge an instrument.
+ // presetBank clamps; dualKnob clamps both spindles. Built on detached hosts
+ // so the gallery's own cards stay untouched.
+ const clamps = await evl(`(() => {
+ const pb = DUPRE.presetBank(document.createElement('div'));
+ pb.set(99); const hi = pb.get();
+ pb.set(-5); const lo = pb.get();
+ const dk = DUPRE.dualKnob(document.createElement('div'));
+ dk.set(500, -500); const [o, i] = dk.get();
+ return JSON.stringify({ hi, lo, o, i });
+ })()`);
+ ok('presetBank and dualKnob set() clamp out-of-range input',
+ clamps === JSON.stringify({ hi: 3, lo: 0, o: 100, i: 0 }), clamps);
+
+ // Batch-4 set() hardening, same detached-host shape: vernierDial and
+ // flutedKnob clamp 0-100; filterBank clamps the band index (an out-of-range
+ // band lands on the last, it doesn't throw); multiBandDial clamps the band
+ // to its rings and keeps the current band when set() omits it.
+ const clamps4 = await evl(`(() => {
+ const vd = DUPRE.vernierDial(document.createElement('div'));
+ vd.set(500); const vHi = vd.get();
+ vd.set(-5); const vLo = vd.get();
+ const fk = DUPRE.flutedKnob(document.createElement('div'));
+ fk.set(500); const fHi = fk.get();
+ const fb = DUPRE.filterBank(document.createElement('div'));
+ fb.set(99, 30); const fbLast = fb.get()[11];
+ const mb = DUPRE.multiBandDial(document.createElement('div'));
+ mb.set(50, 99); const [, bHi] = mb.get();
+ mb.set(70); const [v2, bKeep] = mb.get();
+ return JSON.stringify({ vHi, vLo, fHi, fbLast, bHi, v2, bKeep });
+ })()`);
+ ok('batch-4 set() clamps: vernier/fluted 0-100, filterBank + multiBand band index',
+ clamps4 === JSON.stringify({ vHi: 100, vLo: 0, fHi: 100, fbLast: 30, bHi: 3, v2: 70, bKeep: 3 }), clamps4);
+
+ // The batch-4 rename holds end to end: a detached dipBank round-trips its
+ // word through the dupre- prefixed switch classes, and jogWheel's cached
+ // inner disc actually rotates on set.
+ const b4dom = await evl(`(() => {
+ const db = DUPRE.dipBank(document.createElement('div'));
+ db.set('101');
+ const jw = DUPRE.jogWheel(document.createElement('div'));
+ jw.set(10);
+ const inner = jw.el.querySelector('.dupre-inner');
+ return JSON.stringify({ word: db.get(), sw: db.el.querySelectorAll('.dupre-dipsw').length,
+ rot: inner ? inner.style.transform : 'missing' });
+ })()`);
+ ok('dipBank word round-trips and jogWheel rotates via dupre- classes',
+ b4dom === JSON.stringify({ word: '101000', sw: 6, rot: 'rotate(40deg)' }), b4dom);
+
+ // Batch-5 set() hardening, same detached-host shape: ledRow, pillSlide and
+ // discSwitch clamp their index (discSwitch used to throw); waveRegion keeps
+ // E at least 4 above S however set() is called, not only on the drag path.
+ // ledRow's default index is 5 only for the default program list — a caller's
+ // own items start at 0 (the consoleKeys default-active rule).
+ const clamps5 = await evl(`(() => {
+ const lr = DUPRE.ledRow(document.createElement('div'));
+ lr.set(99); const lrHi = lr.get();
+ lr.set(-3); const lrLo = lr.get();
+ const lrDef = DUPRE.ledRow(document.createElement('div')).get();
+ const lrOwn = DUPRE.ledRow(document.createElement('div'), { items: ['A', 'B', 'C'] }).get();
+ const ps = DUPRE.pillSlide(document.createElement('div'));
+ ps.set(9); const psHi = ps.get();
+ ps.set(-1); const psLo = ps.get();
+ let dsHi;
+ try { const ds = DUPRE.discSwitch(document.createElement('div')); ds.set(9); dsHi = ds.get(); }
+ catch (e) { dsHi = 'threw'; }
+ const wr = DUPRE.waveRegion(document.createElement('div'));
+ wr.set(80, 20); const { s: wrS, e: wrE } = wr.get();
+ wr.set(-10, 200); const { s: wrS2, e: wrE2 } = wr.get();
+ return JSON.stringify({ lrHi, lrLo, lrDef, lrOwn, psHi, psLo, dsHi, wrS, wrE, wrS2, wrE2 });
+ })()`);
+ ok('batch-5 set() clamps: ledRow/pillSlide/discSwitch index, waveRegion S/E order',
+ clamps5 === JSON.stringify({ lrHi: 7, lrLo: 0, lrDef: 5, lrOwn: 0, psHi: 2, psLo: 0, dsHi: 2, wrS: 80, wrE: 84, wrS2: 0, wrE2: 100 }), clamps5);
+
+ // entryKeypad buffer round-trip on a detached host: digits accumulate, the
+ // buffer caps at 6, keys outside the contract's set are ignored, ✗ clears,
+ // ✓ commits and clears.
+ const b5dom = await evl(`(() => {
+ const kp = DUPRE.entryKeypad(document.createElement('div'));
+ ['1', '2', '3'].forEach(k => kp.press(k)); const buf3 = kp.get();
+ kp.press('X'); const stray = kp.get();
+ ['4', '5', '6', '7'].forEach(k => kp.press(k)); const cap = kp.get();
+ kp.press('✗'); const clr = kp.get();
+ kp.press('1'); kp.press('✓'); const commit = kp.get();
+ return JSON.stringify({ buf3, stray, cap, clr, commit });
+ })()`);
+ ok('entryKeypad buffers digits only, caps at 6, clears and commits',
+ b5dom === JSON.stringify({ buf3: '123', stray: '123', cap: '123456', clr: '', commit: '' }), b5dom);
+
+ // batch-6 domain gates on detached hosts: fourWayToggle falls back to C on a
+ // bad initial position and ignores invalid quadrants in set(), camTimer
+ // clamps set() to the ring (12 steps → 0..11, fractions floor), pinMatrix
+ // drops initial pins that name no intersection.
+ const clamps6 = await evl(`(() => {
+ const fw = DUPRE.fourWayToggle(document.createElement('div'), { position: 'Z' });
+ const fwDef = fw.get(); fw.set('B'); const fwB = fw.get(); fw.set('Q'); const fwQ = fw.get();
+ fw.set('toString'); const fwProto = fw.get();
+ const ct = DUPRE.camTimer(document.createElement('div'), { position: 99 });
+ const ctHi = ct.get(); ct.set(-5); const ctLo = ct.get(); ct.set(2.7); const ctInt = ct.get();
+ const pm = DUPRE.pinMatrix(document.createElement('div'), { pins: ['OSC1>VCF', 'BAD>KEY', 'LFO>NOPE', 'OSC2>VCA>X'] });
+ return JSON.stringify({ fwDef, fwB, fwQ, fwProto, ctHi, ctLo, ctInt, pmN: pm.get().length });
+ })()`);
+ ok('batch-6 domain gates: fourWayToggle quadrant, camTimer step clamp, pinMatrix pin filter',
+ clamps6 === JSON.stringify({ fwDef: 'C', fwB: 'B', fwQ: 'B', fwProto: 'B', ctHi: 11, ctLo: 0, ctInt: 2, pmN: 1 }), clamps6);
+
+ // dsky verb/noun grammar through the prefixed DOM (catches rename stragglers):
+ // VERB arms exactly one hot window, V16 N36 commits PROG 16 and disarms,
+ // V35 lights all six status lamps.
+ const b6dom = await evl(`(() => {
+ const d = DUPRE.dsky(document.createElement('div'));
+ const K = {};
+ d.el.querySelectorAll('.dupre-dsky-pad .dupre-key').forEach(b => K[b.textContent] = b);
+ K.VERB.click();
+ const hot1 = d.el.querySelectorAll('.dupre-dsky-win.dupre-hot').length;
+ K['1'].click(); K['6'].click(); K.NOUN.click(); K['3'].click(); K['6'].click(); K.ENTR.click();
+ const prog = d.get().prog;
+ const hot0 = d.el.querySelectorAll('.dupre-dsky-win.dupre-hot').length;
+ K.VERB.click(); K['3'].click(); K['5'].click(); K.ENTR.click();
+ const lit = d.el.querySelectorAll('.dupre-dsky-sl.dupre-on').length;
+ return JSON.stringify({ hot1, prog, hot0, lit });
+ })()`);
+ ok('dsky grammar drives prefixed DOM: hot window, V16 N36 sets PROG, V35 lights all lamps',
+ b6dom === JSON.stringify({ hot1: 1, prog: '16', hot0: 0, lit: 6 }), b6dom);
+
+ // batch-7 domain gates on detached hosts: decadeBox coerces and clamps its
+ // digits, twoHandSafety ignores sides outside L/R, the meters clamp their
+ // set()/push() domains, sparkline floors its history at two points, scope
+ // clears the trace below two samples, eqBars reads missing bands as 0.
+ const clamps7 = await evl(`(() => {
+ const db = DUPRE.decadeBox(document.createElement('div'), { digits: [12, -3, undefined, 7] });
+ const dbTot = db.get();
+ const thsLog = [];
+ const ths = DUPRE.twoHandSafety(document.createElement('div'), { onChange: st => thsLog.push(st) });
+ ths.press('X'); ths.press('L'); ths.press('R');
+ const ms = DUPRE.miniSig(document.createElement('div'));
+ ms.set(2); const msHi = ms.get(); ms.set(-1); const msLo = ms.get();
+ const fb = DUPRE.fuelBar(document.createElement('div'));
+ fb.set(150); const fbHi = fb.get();
+ const rr = DUPRE.radialRing(document.createElement('div'));
+ rr.set(150); const rrHi = rr.get();
+ const sp = DUPRE.sparkline(document.createElement('div'), { samples: 1 });
+ sp.push(5); const spHi = sp.get();
+ const spPts = sp.el.querySelector('polyline').getAttribute('points');
+ const ws = DUPRE.waveStrip(document.createElement('div'));
+ ws.set([0, 5, -5], 0.5);
+ const wsD = ws.el.querySelector('path').getAttribute('d');
+ const sc = DUPRE.scope(document.createElement('div'));
+ sc.set([0.5], 1);
+ const scEmpty = sc.el.querySelector('polyline').getAttribute('points');
+ sc.set([5, -5], 1);
+ const scPts = sc.el.querySelector('polyline').getAttribute('points');
+ let eqTxt; const eq = DUPRE.eqBars(document.createElement('div'), { bands: 3, cells: 4, onChange: (v, t) => eqTxt = t });
+ eq.set([2, -1]);
+ const eqB0 = eq.el.children[0].querySelectorAll('.dupre-on,.dupre-hot,.dupre-clip').length;
+ const eqRest = eq.el.children[1].querySelectorAll('.dupre-on,.dupre-hot,.dupre-clip').length
+ + eq.el.children[2].querySelectorAll('.dupre-on,.dupre-hot,.dupre-clip').length;
+ return JSON.stringify({ dbTot, ths: thsLog.join('|'), msHi, msLo, fbHi, rrHi, spHi,
+ spOk: !spPts.includes('NaN'), wsOk: !wsD.includes('NaN') && wsD.includes(' 33.0') && wsD.includes(' 5.0'),
+ scEmpty, scPts, eqTxt, eqB0, eqRest });
+ })()`);
+ ok('batch-7 domain gates: decadeBox digits, twoHandSafety side, meter clamps, trace guards',
+ clamps7 === JSON.stringify({ dbTot: 9007, ths: 'ready|armed|running', msHi: 1, msLo: 0, fbHi: 100,
+ rrHi: 100, spHi: 1, spOk: true, wsOk: true, scEmpty: '', scPts: '0.0,61.0 176.0,17.0',
+ eqTxt: 'peak 100%', eqB0: 4, eqRest: 0 }), clamps7);
+
+ // voiceLoop grammar through the prefixed DOM (catches rename stragglers):
+ // the default set opens FD/GNC/A-G engaged with GNC talking, click cycles
+ // idle → monitored → talking → idle with talk exclusive, and a caller's own
+ // loop set starts all-idle (the demo default is the stock set's alone).
+ const b7dom = await evl(`(() => {
+ const vl = DUPRE.voiceLoop(document.createElement('div'));
+ const ks = vl.el.querySelectorAll('.dupre-vk');
+ const nKeys = ks.length;
+ const mon0 = vl.el.querySelectorAll('.dupre-mon').length;
+ const tlk0 = vl.el.querySelectorAll('.dupre-tlk').length;
+ ks[2].click();
+ const mon1 = vl.el.querySelectorAll('.dupre-mon').length;
+ ks[0].click();
+ const tlk1 = vl.el.querySelectorAll('.dupre-tlk').length;
+ const fdTalks = ks[0].classList.contains('dupre-tlk') && !ks[1].classList.contains('dupre-tlk');
+ ks[0].click();
+ const fdIdle = vl.get()[0] === '0' && !ks[0].classList.contains('dupre-mon');
+ const own = DUPRE.voiceLoop(document.createElement('div'), { loops: ['A', 'B', 'C'] });
+ const ownIdle = own.get().join('') === '000' && own.el.querySelectorAll('.dupre-mon,.dupre-tlk').length === 0;
+ return JSON.stringify({ nKeys, mon0, tlk0, mon1, tlk1, fdTalks, fdIdle, ownIdle });
+ })()`);
+ ok('voiceLoop cycles through prefixed DOM: demo default, exclusive talk, own set idle',
+ b7dom === JSON.stringify({ nKeys: 8, mon0: 3, tlk0: 1, mon1: 4, tlk1: 1, fdTalks: true, fdIdle: true, ownIdle: true }), b7dom);
+
+ // batch-8 domain gates on detached hosts: the needle meters clamp set() to
+ // their documented 0-100, stripChart floors its history at two samples and
+ // survives a non-array set(), battCells floors its cell count at one.
+ const clamps8 = await evl(`(() => {
+ const xn = DUPRE.crossNeedle(document.createElement('div'));
+ xn.set(150); const xnHi = xn.get(); xn.set(-10); const xnLo = xn.get();
+ const th = DUPRE.thermometer(document.createElement('div'));
+ th.set(-5); const thLo = th.get();
+ const bd = DUPRE.bourdon(document.createElement('div'));
+ bd.set(200); const bdHi = bd.get();
+ const st = DUPRE.stripChart(document.createElement('div'), { samples: 1 });
+ st.set(null, 0.5);
+ const stPts = st.el.querySelector('polyline').getAttribute('points');
+ let cmTxt; const cm = DUPRE.corrMeter(document.createElement('div'), { onChange: (v, t) => cmTxt = t });
+ cm.set(200); const cmHi = cm.get();
+ const bc = DUPRE.battCells(document.createElement('div'), { cells: -3 });
+ const bcCells = bc.el.querySelectorAll('.dupre-cell').length;
+ bc.set(-10); const bcLo = bc.get();
+ const vu = DUPRE.mcVu(document.createElement('div'), { value: 5 });
+ const vuInit = vu.get();
+ const vuNdl = vu.el.querySelector('line[stroke-width="1.6"]').getAttribute('transform');
+ return JSON.stringify({ xnHi, xnLo, thLo, bdHi, stOk: !stPts.includes('NaN'), cmHi, cmTxt, bcCells, bcLo, vuInit, vuNdl });
+ })()`);
+ ok('batch-8 domain gates: needle-meter clamps, stripChart array/floor guards, battCells cell floor, mcVu init',
+ clamps8 === JSON.stringify({ xnHi: 100, xnLo: 0, thLo: 0, bdHi: 100, stOk: true,
+ cmHi: 100, cmTxt: '+1.00', bcCells: 1, bcLo: 0, vuInit: 1.02, vuNdl: 'rotate(43,75,112)' }), clamps8);
+
+ // batch-8 state through the prefixed DOM (catches rename stragglers): the
+ // battery lights dupre-on cells and tints dupre-warn under the threshold,
+ // the crossed needles and mercury column track set() through their renamed
+ // internals.
+ const b8dom = await evl(`(() => {
+ const bc = DUPRE.battCells(document.createElement('div'));
+ const nCells = bc.el.querySelectorAll('.dupre-cell').length;
+ const on62 = bc.el.querySelectorAll('.dupre-cell.dupre-on').length;
+ const warn62 = bc.el.querySelectorAll('.dupre-warn').length;
+ bc.set(20);
+ const on20 = bc.el.querySelectorAll('.dupre-cell.dupre-on').length;
+ const warn20 = bc.el.querySelectorAll('.dupre-cell.dupre-warn.dupre-on').length;
+ const xn = DUPRE.crossNeedle(document.createElement('div'));
+ xn.set(0);
+ const fwdT = xn.el.querySelector('.dupre-fwd').style.transform;
+ const rflT = xn.el.querySelector('.dupre-rfl').style.transform;
+ const th = DUPRE.thermometer(document.createElement('div'));
+ th.set(75);
+ const thH = th.el.querySelector('.dupre-thermo-fill').style.height;
+ const bd = DUPRE.bourdon(document.createElement('div'));
+ bd.set(50);
+ const bdT = bd.el.querySelector('.dupre-bourdon-ndl').style.transform;
+ const cm = DUPRE.corrMeter(document.createElement('div'));
+ cm.set(50);
+ const cmT = cm.el.querySelector('.dupre-corr-ndl').style.transform;
+ const stc = DUPRE.stripChart(document.createElement('div'));
+ stc.push(1);
+ const penT = stc.el.querySelector('.dupre-pen').style.top;
+ return JSON.stringify({ nCells, on62, warn62, on20, warn20, fwdT, rflT, thH, bdT, cmT, penT });
+ })()`);
+ ok('batch-8 renamed DOM tracks state: battery on/warn cells, needles and pen move',
+ b8dom === JSON.stringify({ nCells: 8, on62: 5, warn62: 0, on20: 2, warn20: 2,
+ fwdT: 'rotate(-42deg)', rflT: 'rotate(42deg)', thH: '75%', bdT: 'rotate(0deg)',
+ cmT: 'rotate(0deg)', penT: '3px' }), b8dom);
+
+ // batch-9 domain gates on detached hosts: the flight instruments clamp (or
+ // normalize) both set() and their build opts, badges coerces junk variants,
+ // the readout wraps its clock.
+ const clamps9 = await evl(`(() => {
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'), { bank: -999, pitch: 5 });
+ const aiB = ai.get().bank;
+ ai.set(999, 999); const aiHi = ai.get();
+ const hb = DUPRE.headingBug(document.createElement('div'), { value: 450 });
+ const hbB = hb.get().cmd;
+ const hbNdl = hb.el.querySelector('g polygon').parentNode.getAttribute('transform');
+ hb.set(-30); const hbSet = hb.get().cmd;
+ const vt = DUPRE.verticalTape(document.createElement('div'), { value: 99 });
+ const vtB = vt.get(); vt.set(0); const vtLo = vt.get();
+ const vtT = vt.el.querySelector('g g').getAttribute('transform');
+ const tn = DUPRE.twinNeedle(document.createElement('div'), { fuel: 99, oil: -5 });
+ const tnB = tn.get();
+ let cmTxt; const cm = DUPRE.comfortMeter(document.createElement('div'),
+ { temp: 999, humidity: -40, onChange: (v, t) => cmTxt = t });
+ const cmB = cm.get();
+ const bg = DUPRE.badges(document.createElement('div'), { items: [['X', 7], ['Y', 'junk']] });
+ const bgVars = bg.get();
+ const tr = DUPRE.tabularReadout(document.createElement('div'), { secs: -10 });
+ const trB = tr.get();
+ return JSON.stringify({ aiB, aiHi, hbB, hbNdl, hbSet, vtB, vtLo, vtT, tnB, cmB, cmTxt, bgVars, trB });
+ })()`);
+ ok('batch-9 domain gates: flight-instrument clamps at build and set, badges variant coercion, readout wrap',
+ clamps9 === JSON.stringify({ aiB: -60, aiHi: { bank: 60, pitch: 20 }, hbB: 90,
+ hbNdl: 'rotate(90.0,65,65)', hbSet: 330, vtB: 35, vtLo: 5, vtT: 'translate(0,92.5)',
+ tnB: [4, 0], cmB: [100, 0], cmTxt: '100F · 0% RH · TOO WARM', bgVars: [1, 0], trB: 3590 }), clamps9);
+
+ // batch-9 state through the renamed DOM (catches rename stragglers): badges
+ // swap variant classes, the readout counts down through dupre-readout and
+ // carries a styled dupre-unit (the old .readout .u rule was dead — .u is a
+ // sibling, so the caption rendered unstyled), the engraved label counts
+ // through dupre-cnt, and the SVG flight instruments track set().
+ const b9dom = await evl(`(() => {
+ const bg = DUPRE.badges(document.createElement('div'));
+ const nBadge = bg.el.querySelectorAll('.dupre-badge').length;
+ const red0 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const ghost0 = bg.el.querySelectorAll('.dupre-badge.dupre-ghost').length;
+ bg.set(0, 1);
+ const red1 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const trHost = document.createElement('div'); document.body.appendChild(trHost);
+ const tr = DUPRE.tabularReadout(trHost);
+ const trTxt = tr.el.querySelector('.dupre-readout').textContent;
+ const unit = tr.el.querySelector('.dupre-unit');
+ const unitTxt = unit.textContent;
+ const unitDisp = getComputedStyle(unit).display;
+ tr.tick(); const trTick = tr.el.querySelector('.dupre-readout').textContent;
+ tr.el.querySelector('.dupre-readout').click(); tr.tick();
+ const trPaused = tr.el.querySelector('.dupre-readout').textContent;
+ trHost.remove();
+ const el = DUPRE.engravedLabel(document.createElement('div'));
+ const cnt0 = el.el.querySelector('.dupre-cnt').textContent;
+ el.set(7); const cnt7 = el.el.querySelector('.dupre-cnt').textContent;
+ el.el.click(); const cnt8 = el.el.querySelector('.dupre-cnt').textContent;
+ const tn = DUPRE.twinNeedle(document.createElement('div'));
+ tn.set(0, 4);
+ const tnF = tn.el.querySelector('polygon[fill="#e0523a"]').parentNode.getAttribute('transform');
+ const tnO = tn.el.querySelector('polygon[fill="#bfc4d0"]').parentNode.getAttribute('transform');
+ const cm = DUPRE.comfortMeter(document.createElement('div'));
+ cm.set(70, 80);
+ const humid = [...cm.el.querySelectorAll('text')].find(t => t.textContent === 'TOO HUMID').getAttribute('fill');
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'));
+ ai.set(30, -10);
+ const aiT = ai.el.querySelector('g g').getAttribute('transform');
+ const rc = DUPRE.roundCrt(document.createElement('div'));
+ const p1 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ rc.tick();
+ const p2 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ const cr = DUPRE.chartRecorder(document.createElement('div'));
+ cr.reset();
+ const crH = cr.get();
+ const crD = cr.el.querySelector('path[stroke="#8f2416"]').getAttribute('d');
+ return JSON.stringify({ nBadge, red0, ghost0, red1, trTxt, unitTxt, unitDisp, trTick, trPaused,
+ cnt0, cnt7, cnt8, tnF, tnO, humid, aiT, crtMoves: p1 !== p2 && !p2.includes('NaN'), crH, crD });
+ })()`);
+ ok('batch-9 renamed DOM tracks state: badge variants, readout clock + styled unit, engraved count, instruments follow set()',
+ b9dom === JSON.stringify({ nBadge: 3, red0: 1, ghost0: 1, red1: 2, trTxt: '24:10',
+ unitTxt: 'timer', unitDisp: 'block', trTick: '24:09', trPaused: '24:09',
+ cnt0: '· 3', cnt7: '· 7', cnt8: '· 8', tnF: 'rotate(-170.0,60,62)', tnO: 'rotate(10.0,60,62)',
+ humid: '#c23a28', aiT: 'rotate(-30.0,65,65) translate(0,-16.0)',
+ crtMoves: true, crH: 0, crD: '' }), b9dom);
+
+ // batch-10 domain gate: the annunciator documents a 0..2 state per cell, so
+ // an out-of-range initial state or set() must clamp rather than paint an
+ // undefined class (the mcVu build-enforcement precedent). Without the clamp,
+ // CLS[9] is undefined and get() reads back 0 — this fails RED on that.
+ const clamps10 = await evl(`(() => {
+ const an = DUPRE.annunciator(document.createElement('div'), { cells: [['A', 9], ['B', -3], ['C', 1]] });
+ const anB = an.get();
+ an.set(0, 9); an.set(1, -5);
+ const anSet = an.get();
+ return JSON.stringify({ anB, anSet });
+ })()`);
+ ok('batch-10 domain gate: annunciator state clamps to 0..2 at build and set',
+ clamps10 === JSON.stringify({ anB: [2, 0, 1], anSet: [2, 0, 1] }), clamps10);
+
+ // batch-10 state through the renamed DOM (catches rename stragglers): the six
+ // extracted DOM builders each expose their handle and paint the dupre- classes
+ // — a JS/CSS name mismatch would render unstyled without throwing, so assert
+ // the class names directly. patchBay lights hot jacks and freqScale carries
+ // the compound dupre-fs-band unit label (dupre-band is the EQ's).
+ const b10dom = await evl(`(() => {
+ const ow = DUPRE.outputWell(document.createElement('div'));
+ const owRoot = ow.el.className, owSeed = ow.el.querySelectorAll('.dupre-ostep').length;
+ ow.push(['red', 'Err', 'boom']);
+ const owN = ow.el.querySelectorAll('.dupre-ostep').length;
+ const owEv = ow.el.querySelector('.dupre-ostep:last-child .dupre-ev').textContent;
+ const owRed = ow.el.querySelector('.dupre-ostep:last-child .dupre-lamp').classList.contains('dupre-red');
+ const to = DUPRE.toast(document.createElement('div'));
+ const toRoot = to.el.className; to.fire('ping'); const toTxt = to.el.textContent;
+ const tc = DUPRE.tapeCounter(document.createElement('div'));
+ const tcRoot = tc.el.className;
+ const tcWheels = tc.el.querySelectorAll('.dupre-cwheel').length;
+ const tcRed = tc.el.querySelectorAll('.dupre-redw').length;
+ tc.set(120); const tcV = tc.get();
+ const fq = DUPRE.freqScale(document.createElement('div'));
+ const fqBand = fq.el.querySelector('.dupre-fs-band').textContent;
+ const fqTicks = fq.el.querySelectorAll('.dupre-tick').length > 0;
+ const fqPtr = !!fq.el.querySelector('.dupre-fptr');
+ fq.set(50); const fqV = fq.get();
+ const pb = DUPRE.patchBay(document.createElement('div'));
+ const pbJacks = pb.el.querySelectorAll('.dupre-jack').length;
+ const pbHot = pb.el.querySelectorAll('.dupre-jack.dupre-hot').length;
+ const pbConns = pb.get();
+ const an = DUPRE.annunciator(document.createElement('div'));
+ const anCells = an.el.querySelectorAll('.dupre-acell').length;
+ const anWarn = an.el.querySelectorAll('.dupre-warn').length;
+ const anFault = an.el.querySelectorAll('.dupre-fault').length;
+ const anMcOn = an.el.querySelector('.dupre-mc').classList.contains('dupre-on');
+ return JSON.stringify({ owRoot, owSeed, owN, owEv, owRed, toRoot, toTxt,
+ tcRoot, tcWheels, tcRed, tcV, fqBand, fqTicks, fqPtr, fqV,
+ pbJacks, pbHot, pbConns, anCells, anWarn, anFault, anMcOn });
+ })()`);
+ ok('batch-10 renamed DOM tracks state: well/toast/counter/freqscale/patchbay/annunciator handles + dupre- classes',
+ b10dom === JSON.stringify({ owRoot: 'dupre-owell', owSeed: 2, owN: 3, owEv: 'boom', owRed: true,
+ toRoot: 'dupre-toastw', toTxt: 'ping', tcRoot: 'dupre-counter', tcWheels: 6, tcRed: 2, tcV: 120,
+ fqBand: 'MHz', fqTicks: true, fqPtr: true, fqV: 50, pbJacks: 8, pbHot: 4, pbConns: ['0-5', '2-7'],
+ anCells: 6, anWarn: 1, anFault: 1, anMcOn: true }), b10dom);
+
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);