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