test(main): pin the keydown intercepts behaviourally, not by source text - #201
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The story
PR #193 added a main-process
before-input-eventintercept so ⌘0 reaches the renderer instead of Electron's default View ▸ Actual Size (resetZoom), and shipped it withsrc/main/menu-accelerator-intercepts.test.ts— a test that asserts on the source text ofsrc/main/index.ts(expect(MAIN_SRC).toContain(...)). PR #149 rewrites the shared guard at the top of that handler fromif (input.type !== 'keyDown' || !(input.meta || input.control)) returntoif (input.type !== 'keyDown') return, pushing the modifier decision into each branch — correct for its own configurable branches, but #193'sDigit0branch has no modifier test of its own, so on the merged tree the bare0key is swallowed app-wide and the canvas snaps to 100% (andsrc/main/index.tsauto-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
mainwhere it protects every branch.The extraction
src/main/keydown-intercept.ts— new, Electron-free, alongsidemain-window.ts:keydownIntercept(input)— pure: the input's key-state shape in, a claimed action ornullout.nullmeans "leave the key completely alone"; a returned decision means "swallow it", withactionseparately 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 structuralwinso a test can register on a fake.index.tsloses 24 lines and gains one call. Shaped to read as a pair withsrc/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
Digit0branch 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/mainrather than moved tosrc/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 — waspreventDefaultcalled, 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:
0,m,wis not intercepted and not prevented — the regression above;0()), Alt+0(AltGr), Shift+M, Alt+W all reach the page;keyUpis never intercepted, even for a claimed chord.And the claims themselves: ⌘0 and Ctrl+0 forward
app:zoom-actual-size; ⌘0 is matched oncode, so it survives a layout where the zero key prints something else, andNumpad0is not claimed; a held ⌘0 is swallowed but not forwarded (both halves matter — forwarding restarts the 200ms tween, letting it through hands the repeat toresetZoom); ⌘⇧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⌘Mbranch, 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 fortoContainon a source file.Mutation result
Applied #149's guard rewrite locally —
if (input.type !== 'keyDown') return null— and re-ran: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 sharednode_modules, exactly the caseCLAUDE.mddescribes) and 1 ×webgl-addon-pair(a genuine@xterm/addon-webgl↔@xterm/xtermversion skew in the same sharednode_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
createWindowis only proven by inspection — a Mac run pressing ⌘0 / ⌘M / ⌘W is still worth doing.🤖 Generated with Claude Code