aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/gallery-probes/probe.mjs150
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);