Skip to content

test(main): pin the keydown intercepts behaviourally, not by source text - #201

Merged
eneskirca merged 1 commit into
mainfrom
test/keydown-intercept-behaviour
Aug 15, 2026
Merged

test(main): pin the keydown intercepts behaviourally, not by source text#201
eneskirca merged 1 commit into
mainfrom
test/keydown-intercept-behaviour

Conversation

@eneskirca

Copy link
Copy Markdown
Owner

The story

PR #193 added a main-process before-input-event intercept so ⌘0 reaches the renderer instead of Electron's default View ▸ Actual Size (resetZoom), and shipped it with src/main/menu-accelerator-intercepts.test.ts — a test that asserts on the source text of src/main/index.ts (expect(MAIN_SRC).toContain(...)). PR #149 rewrites the shared guard at the top of that handler from if (input.type !== 'keyDown' || !(input.meta || input.control)) return to if (input.type !== 'keyDown') return, pushing the modifier decision into each branch — correct for its own configurable branches, but #193's Digit0 branch has no modifier test of its own, so on the merged tree the bare 0 key is swallowed app-wide and the canvas snaps to 100% (and src/main/index.ts auto-merges silently, so there is no conflict to notice).

The part that made this a task rather than a note: on that merged tree the existing test is green, and on the correct fix it goes red. It is exactly inverted. The strings it matched were all still present; what had changed was the guard that made them safe, and a test that reads the code cannot see the code being wrong, only being absent.

This is our own test debt from #193, not #149's problem, so it lands on main where it protects every branch.

The extraction

src/main/keydown-intercept.ts — new, Electron-free, alongside main-window.ts:

  • keydownIntercept(input)pure: the input's key-state shape in, a claimed action or null out. null means "leave the key completely alone"; a returned decision means "swallow it", with action separately nullable so a held ⌘0 keeps being swallowed (the menu is still listening) while forwarding nothing.
  • keydownInterceptChannel(action) — action → IPC channel.
  • installKeydownIntercepts(win) — the four-line wiring, against a structural win so a test can register on a fake.

index.ts loses 24 lines and gains one call. Shaped to read as a pair with src/renderer/lib/zoomShortcut.ts, the sibling #193 already created for the renderer half of the same chord — same input-shape-only interface, same action-or-null return, same "the refusals are the point" header.

Behaviour is byte-for-byte what shipped. In particular the primary-modifier requirement stays in the shared guard and the Digit0 branch gets no modifier test of its own — adding one would be a sensible fix, but it would also make the mutation below survive, and this PR is the test, not the fix. #149's own one-line fix is unchanged by this.

Kept in src/main rather than moved to src/core (which the conventions would normally prefer): this module exists solely to fight a native application menu, and the Server Edition's browser shell has no menu to steal a chord back from.

What the new test pins

src/main/keydown-intercept.test.ts, 20 tests. It installs the handler on a fake window and presses keys; every assertion is on an outcome — was preventDefault called, and which channel was sent. No source text anywhere.

The load-bearing half is the refusals, because every chord here is built on a character people type:

  • a bare 0, m, w is not intercepted and not prevented — the regression above;
  • Shift+0 ()), Alt+0 (AltGr), Shift+M, Alt+W all reach the page;
  • an unclaimed chord (⌘K) reaches the page; keyUp is never intercepted, even for a claimed chord.

And the claims themselves: ⌘0 and Ctrl+0 forward app:zoom-actual-size; ⌘0 is matched on code, so it survives a layout where the zero key prints something else, and Numpad0 is not claimed; a held ⌘0 is swallowed but not forwarded (both halves matter — forwarding restarts the 200ms tween, letting it through hands the repeat to resetZoom); ⌘⇧0 / ⌘⌥0 are not claimed. ⌘M and ⌘W forward their channels under both Meta and Control; ⌘⇧W is left to the menu's Close All Windows. ⌘⇧M and ⌘⌥M are claimed — that is the shipped breadth of the ⌘M branch, pinned as-is with a comment, so narrowing it turns this red on purpose rather than slipping through.

The old test

Deleted. The behavioural test subsumes every claim it made, and the one thing it could still have offered — a reminder of why the main process gets a say at all (we never call Menu.setApplicationMenu, so Electron's default menu owns all three accelerators, and a menu accelerator is handled before the page sees the key) — is better as the module's header comment, next to the code it explains. Rewriting it to assert something a string cannot lie about would just have been the new test, in a worse file. CONTRIBUTING.md's testing section gets a short entry so the next person does not reach for toContain on a source file.

Mutation result

Applied #149's guard rewrite locally — if (input.type !== 'keyDown') return null — and re-ran:

× a bare 0 reaches the page
× a bare m reaches the page
× a bare w reaches the page
× Shift+M reaches the page
× Alt+W reaches the page
× returns null for anything unclaimed and an action for a claimed chord

Tests  6 failed | 14 passed (20)

Reverted, back to 20/20. The old test was green throughout both runs.

Gates

  • npm run typecheck — clean.
  • npx vitest run src/main src/renderer — 2973 passed, 4 failed.
  • npx vitest run (full) — 5806 passed, 12 skipped, 4 failed.

The 4 are environmental in this clone and identical before and after the change: 3 × node-pty-patch (unpatched shared node_modules, exactly the case CLAUDE.md describes) and 1 × webgl-addon-pair (a genuine @xterm/addon-webgl@xterm/xterm version skew in the same shared node_modules, worth a look on its own but unrelated to this).

Not verified: nothing here was exercised in a running app. The extraction is mechanical and the suite covers the decision, but the single wiring call in createWindow is only proven by inspection — a Mac run pressing ⌘0 / ⌘M / ⌘W is still worth doing.

🤖 Generated with Claude Code

The main window's `before-input-event` handler steals ⌘M / ⌘W / ⌘0 back from
Electron's default application menu. Its test asserted on the SOURCE TEXT of
`src/main/index.ts` (`expect(MAIN_SRC).toContain(...)`), which meant it was
green on a tree where the shared `meta || control` guard had been rewritten out
from under the `Digit0` branch and the bare `0` key was swallowed app-wide —
green on the break, red on the fix.

Extract the decision into `src/main/keydown-intercept.ts`: `keydownIntercept()`
(pure, input-shape → action-or-null) plus `installKeydownIntercepts(win)` for
the four-line wiring, mirroring the renderer's `zoomShortcut.ts`. Behaviour is
byte-for-byte what shipped — in particular the modifier requirement stays in the
shared guard, because moving it per-branch is exactly the change the new test
exists to catch.

`keydown-intercept.test.ts` presses keys and asserts outcomes (prevented? which
channel?): every claimed chord bare, with Shift, with Alt, on keyUp, and under
auto-repeat. `menu-accelerator-intercepts.test.ts` is deleted — subsumed, and
its reasoning moved into the module header.

Mutation: rewriting the guard to `if (input.type !== 'keyDown') return null`
turns 6 of the 20 tests red, including the bare `0`, `m` and `w` presses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@eneskirca
eneskirca merged commit 5411aaa into main Aug 15, 2026
4 checks passed
@eneskirca
eneskirca deleted the test/keydown-intercept-behaviour branch August 15, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant