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