diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 150 |
1 files changed, 149 insertions, 1 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs index d196377..316816e 100644 --- a/tests/gallery-probes/probe.mjs +++ b/tests/gallery-probes/probe.mjs @@ -68,7 +68,7 @@ try { // 1. defaults: size=2 (M), card count ok('default size 2', await evl(`document.body.dataset.size`) === '2'); const cards = await evl(`document.querySelectorAll('.card').length`); - ok('109 cards', cards === 109, `got ${cards}`); + ok('110 cards', cards === 110, `got ${cards}`); // 2. zoom actually scales: card visual width at 3x vs 1x await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`); @@ -240,6 +240,154 @@ try { })()`); ok('changing an axis clears the preset selection', diverged === '1->0', diverged); + // 10. R57 ABC keypad — fills the taxonomy's text x alphanumeric empty cell. + // ABC order is the whole point: it is what industrial keypads do wherever + // 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')] + .map(k => k.dataset.k).filter(k => /^[A-Z]$/.test(k)); + const want = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ']; + return JSON.stringify(letters) === JSON.stringify(want) + ? 'ok' : 'got ' + letters.length + ': ' + letters.join(''); + })()`); + ok('R57 carries A-Z in alphabetical order', abcOrder === 'ok', abcOrder); + + const abcDigits = await evl(`(()=>{ + const d = [...document.querySelectorAll('#card-R57 .kp-key')] + .map(k => k.dataset.k).filter(k => /^[0-9]$/.test(k)); + return d.length === 10 ? 'ok' : 'got ' + d.length + ': ' + d.join(''); + })()`); + ok('R57 carries a full 0-9 block', abcDigits === 'ok', abcDigits); + + // 10b. LAYOUT, geometrically. The A-Z check above reads DOM order, which the + // builder controls by push order — it would pass with every key rendered + // in the wrong place. This reads actual x positions instead: letters own + // the left columns, digits the right, and the alphabet column-aligns with + // itself all the way down (the discontinuity Craig caught: A-L used to + // 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); + return g ? Math.round(g.querySelector('rect').getBBox().x) : null; + }; + const colStarts = ['A','D','G','J','M','S','Y'].map(x); + if (new Set(colStarts).size !== 1) return 'alphabet not column-aligned: ' + JSON.stringify(colStarts); + if (!(x('A') < x('1'))) return 'letters not left of digits: A=' + x('A') + ' 1=' + x('1'); + if (!(x('DEL') > x('J'))) return 'DEL not in the block beside the digits: DEL=' + x('DEL') + ' J=' + x('J'); + return 'ok'; + })()`); + ok('R57 letters left, digits right, alphabet column-aligned', layout === 'ok', layout); + + // 10c. DEL sits where the hand already is and CLR is exiled to the corner. + // Frequency and blast radius pull the same way: DEL is constant and costs + // one character, CLR is rare and costs the entry. Pinned because it is a + // 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); + return g ? g.querySelector('rect').getBBox() : null; + }; + const del = box('DEL'), clr = box('CLR'), ent = box('ENT'); + if (!del || !clr || !ent) return 'missing key'; + if (!(del.y < clr.y)) return 'DEL should sit above CLR: DEL.y=' + del.y + ' CLR.y=' + clr.y; + if (!(Math.abs(del.y - ent.y) < 1)) return 'DEL should share the ENT row'; + return 'ok'; + })()`); + ok('R57 DEL is in reach, CLR is in the corner', reach === 'ok', reach); + + // 10d. The three function keys are a cost ladder — DEL takes one character + // back, CLR throws the entry away, ENT commits — so each must read as a + // different key before the legend is read. Checks they are mutually + // distinct and all differ from a plain cap, rather than naming a gradient: + // 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); + 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') }; + const fn = [f.DEL, f.CLR, f.ENT]; + if (new Set(fn).size !== 3) return 'function keys not mutually distinct: ' + JSON.stringify(f); + if (fn.includes(f.plain) || fn.includes(f.digit)) return 'a function key wears a plain cap: ' + JSON.stringify(f); + return 'ok'; + })()`); + ok('R57 DEL/CLR/ENT each read as their own key', ladder === 'ok', ladder); + + // 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); + 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; + })()`); + ok('R57 accumulates typed characters in order', typed.includes('WIFI7'), typed); + + // 11b. DEL takes back ONE character. Without it the only way out of a typo is + // 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 click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true})); + click('CLR'); + for (const c of ['C','A','B','S']) click(c); + click('DEL'); + const back = document.getElementById('rd-R57').textContent; + for (let i = 0; i < 6; i++) click('DEL'); // past empty: must not throw or wrap + const floor = document.getElementById('rd-R57').textContent; + return back + ' | ' + floor; + })()`); + // Both halves are asserted: the earlier version computed the past-empty + // state and then never looked at it, so "stops at empty" was a promise in + // the name only — a DEL that wrapped the buffer would have passed. + ok('R57 DEL takes back one character, and stops at empty', + del.split(' | ')[0] === 'CAB' && del.split(' | ')[1] === 'empty', del); + + // 11c. A space must be VISIBLE in the window. The buffer is honest either way, + // but SVG collapses trailing whitespace, so a space rendered as a space is + // a keypress with no feedback: the operator presses SPACE, sees nothing, + // presses again, and now carries two spaces they cannot see in a + // 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 click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true})); + const win = () => document.querySelector('#card-R57 text[font-size="14"]').textContent; + click('CLR'); + for (let i = 0; i < 13; i++) click('A'); + const before = win(); + click('SPC'); + const after = win(); + if (before === after) return 'space produced no visible change: ' + JSON.stringify(after); + if (/ $/.test(after)) return 'space rendered as a raw trailing space (invisible): ' + JSON.stringify(after); + click('CLR'); + return 'ok'; + })()`); + ok('R57 a typed space is visible in the window', space === 'ok', space); + + // 12. the two committing keys do different things: ENTER commits the buffer, + // CLEAR empties it. Types its own buffer rather than inheriting one from + // 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 click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true})); + click('CLR'); + for (const c of ['N','E','T','5']) click(c); + click('ENT'); + const after = document.getElementById('rd-R57').textContent; + click('CLR'); + return after + ' | ' + document.getElementById('rd-R57').textContent; + })()`); + // Asserts the COMMIT SIGNAL ('ENTER · ' + buf), not merely that the buffer + // is still readable: typing the last character already put NET5 in the + // readout, so /NET5/ was true before ENT was ever pressed. That check + // passed with the ENT branch deleted (the key falls through to buf += 'ENT' + // and NET5 still matches) — it could not fail. + ok('R57 ENTER commits and CLEAR empties', + /^ENTER · NET5$/.test(committed.split(' | ')[0]) && !/NET5/.test(committed.split(' | ')[1]), committed); + // late exceptions from interactions const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown'); ok('no exceptions after interaction', errs2.length === 0); |
