diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 219 |
1 files changed, 218 insertions, 1 deletions
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs index 6f4184a..5e95096 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('110 cards', cards === 110, `got ${cards}`); + ok('111 cards', cards === 111, `got ${cards}`); // 2. zoom actually scales: card visual width at 3x vs 1x await evl(`document.querySelector('.szbar .key[data-sz="3"]').click()`); @@ -516,6 +516,223 @@ try { })()`); ok('R57 drops keys that are not on the plate', unmapped === 'cleared', unmapped); + // 14. R58 index typewriter. THE check: selecting is not committing. Walking the + // stylus over the plate must print nothing at all — that separation IS the + // grammar, and it's the whole reason this card exists next to R57. If a cell + // 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. + 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 empty = buf(); + ['M','i','g'].forEach(c => cell(c).dispatchEvent(new MouseEvent('click', {bubbles:true}))); + const afterSelecting = buf(); + lever.dispatchEvent(new MouseEvent('click', {bubbles:true})); + return JSON.stringify([empty, afterSelecting, buf()]); + })()`); + ok('R58 selecting prints nothing; only the lever commits', grammar === '["","","g"]', grammar); + + // 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})); + 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})); + } + return document.getElementById('rd-R58').textContent; + })()`); + ok('R58 stylus + lever spells a word', spelled.includes('Mignon'), spelled); + + // 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})); + 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(); + })()`); + /* 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. */ + ok('R58 the lever repeats without re-aiming', repeat === 'ss', JSON.stringify(repeat)); + + // 14d. The plate's whole point, per Craig: it considered the characters a + // keyboard skips. Both cases with no shift key, plus accents, fractions and + // the section mark — that coverage is the idea being preserved from the + // Mignon, where the key ORDER deliberately is not. + const charset = await evl(`(()=>{ + const have = new Set([...document.querySelectorAll('#card-R58 .ix-cell')].map(c => c.dataset.c)); + const need = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz', + ...'0123456789', 'ä','ö','ü','Ä','Ö','Ü','ß','§','½','¼']; + const missing = need.filter(c => !have.has(c)); + return missing.length ? 'missing: ' + missing.join(' ') : 'ok'; + })()`); + ok('R58 plate carries both cases, digits and the extended set', charset === 'ok', charset); + + // 14e. The layout is DATA, so revising the order is a table edit rather than a + // 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; + 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 declared = L.flat().filter(Boolean).length; + return cells === declared ? 'ok' : 'drawn ' + cells + ' but declared ' + declared; + })()`); + ok('R58 layout is a declared table the plate renders', layoutData === 'ok', layoutData); + + // 14f. Nothing sits on top of the plate. The lever and CLR started life over the + // last column, burying characters and the PRINT legend, and every check + // above stayed green through it — geometry is invisible to behaviour. The + // gutter is sized off the layout table, so a wider plate must not slide the + // 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 right = Math.max(...cells.map(b => b.x + b.width)); + const hits = []; + for (const sel of ['.ix-lever', '.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)); + } + return hits.length ? hits.join('; ') : 'ok'; + })()`); + ok('R58 lever and CLR clear the plate', clear === 'ok', clear); + + // 14g. The gutter stack doesn't collide with itself. The x-only check above + // can't see this: the lever and legend are anchored to the plate's top and + // CLR to the viewBox, so a SHORTER layout table (five rows is a plausible + // edit) used to ride CLR up over the PRINT legend. Growth was always safe; + // 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 legend = [...document.querySelectorAll('#card-R58 text')].find(t => t.textContent === 'PRINT'); + if (!lever || !clr || !legend) return 'missing gutter part'; + const lg = legend.getBBox(); + const overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && + a.y < b.y + b.height && b.y < a.y + a.height; + 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; + if (clr.y + clr.height > vb.height) return 'CLR falls outside the viewBox'; + return 'ok'; + })()`); + ok('R58 gutter stack does not collide or overflow', stack === 'ok', stack); + + // 14h. The layout table has no duplicate characters. cells{} and KEYS{} are both + // keyed by character, so a repeat would silently keep only the last: two + // cells would render and both be clickable, but clicking the first would + // 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 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'; + })()`); + ok('R58 layout has no duplicate characters', dupes === 'ok', dupes); + + // 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; + h.press('CLR'); + ['F1', 'PRINT ', '', 'constructor', 'ZZ'].forEach(k => h.press(k)); + return JSON.stringify([h.get(), h.selected()]); + })()`); + ok('R58 press() filters junk from any caller', ixPress.startsWith('[""'), ixPress); + + // 15. R58's keymap comes OUT of the layout table rather than beside it, so + // 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; + if (!K) return 'no KEYS'; + const chars = L.flat().filter(Boolean); + const missing = chars.filter(c => K[c] !== c); + if (missing.length) return 'plate chars not mapped: ' + missing.join(' '); + if (K.Enter !== 'PRINT') return 'Enter -> ' + K.Enter + ', want PRINT'; + const extra = Object.keys(K).filter(k => k !== 'Enter' && !chars.includes(k)); + return extra.length ? 'maps keys not on the plate: ' + extra.join(' ') : 'ok'; + })()`); + ok('R58 keymap is derived from the layout, Enter is the lever', ixKeys === 'ok', ixKeys); + + // 15b. THE grammar again, now through the keyboard. Typing a letter must move + // the stylus and print NOTHING. If a keypress ever prints, the card has + // 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 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})); + pad.focus(); + ['M','i','g'].forEach(send); + const afterTyping = h.get(); + const resting = h.selected(); + send('Enter'); + return JSON.stringify([afterTyping, resting, h.get()]); + })()`); + ok('R58 typing selects, Enter prints', ixType === '["","g","g"]', ixType); + + // 15c. The plate holds both cases, so nothing is uppercased on the way in: + // Shift picks the case because 'a' and 'A' are different cells. R57 has to + // 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 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})); + pad.focus(); + send('a'); send('Enter'); + send('A'); send('Enter'); + return h.get(); + })()`); + ok('R58 keeps case: a and A are different cells', ixCase === 'aA', ixCase); + + // 15d. Space isn't on the plate, so it isn't ours: it must scroll the page like + // 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'); + pad.focus(); + const e = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true }); + pad.dispatchEvent(e); + return e.defaultPrevented ? 'swallowed space' : 'ok'; + })()`); + 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. + // 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})); + document.activeElement.blur(); + const before = JSON.stringify([h.get(), h.selected()]); + ['Q','Z'].forEach(k => document.body.dispatchEvent( + new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }))); + const after = JSON.stringify([h.get(), h.selected()]); + return before === after ? 'ok' : before + ' -> ' + after; + })()`); + ok('R58 ignores keys when unfocused', ixBlur === 'ok', ixBlur); + // late exceptions from interactions const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown'); ok('no exceptions after interaction', errs2.length === 0); |
