aboutsummaryrefslogtreecommitdiff
path: root/tests/gallery-probes
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-16 16:43:29 -0500
committerCraig Jennings <c@cjennings.net>2026-07-16 16:43:29 -0500
commit6bcd0da82c505a2ec22268adfced0004401b74a9 (patch)
tree9c6cd8374795f042e14b0a8fbef8d166aacefeea /tests/gallery-probes
parentd3180917944bcedc21e77dff2e3967eb4db142c0 (diff)
downloadarchsetup-6bcd0da82c505a2ec22268adfced0004401b74a9.tar.gz
archsetup-6bcd0da82c505a2ec22268adfced0004401b74a9.zip
feat(gallery): add the index typewriterHEADmain
The oldest way to type without a keyboard, and the first card where selecting and committing are separate acts. One hand walks a stylus over a printed plate, the other pulls a lever, and only the lever prints. You can hunt across every character and nothing happens until the second hand moves. Every other control in the kit fires on contact. The keyboard follows the same rule: typing selects, Enter is the lever. A keypress that printed would make this the ABC keypad with a nicer plate, which is the one thing it must not be. It also does no case folding, where the keypad must. This plate carries both cases as distinct cells, so Shift does exactly the work a shift key would on a machine that hasn't got one. KEYS and ACTIONS are derived from the layout table rather than kept beside it, so rewriting the table relays the plate, the keymap and the allowlist together. A binding aimed at a character the plate no longer carries isn't expressible. After the AEG Mignon Model 4, keeping the two things worth keeping: the extended set a keyboard skips, and both cases with no shift key. Its frequency order goes. Reading the plate is the interaction, and P U G Q / V I N A B isn't something you can read your way around, so ours is alphabetical with capitals beside lowercase at the same column offset. I expect to revisit the keys, so the layout is a table the geometry computes itself from. I built it at four, five and nine rows to check the gutter survives a rewrite in either direction. There's no space cell yet, so Space isn't ours and still scrolls the page. The real machine has a separate space key.
Diffstat (limited to 'tests/gallery-probes')
-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);