fix(terminal): correct shortcut docs + cover copy-selection in E2E

The table JSDoc claimed ordering matters, but `chordMatches` rejects any
event with modifiers the entry doesn't require, so entries are already
mutually exclusive by construction. Rewrote the comment to reflect the
real invariant.

docs/terminal-shortcuts.md's "Reserved shell chords" list claimed
`Ctrl+K` / `Ctrl+W` always fall through, but on Linux/Windows those are
`Mod+K` / `Mod+W` and the app intercepts them. Removed them from the
never-intercepted bullet and added a platform caveat.

Also added E2E coverage for `Mod+Shift+C` (copy-selection): replaces the
`clipboard:writeText` IPC handler with a spy so the test captures the
selection without mutating the real system clipboard on headful runs.
This commit is contained in:
brennanb2025 2026-04-19 22:58:23 -07:00
parent cbf68a1534
commit 6a61e1aea4
3 changed files with 70 additions and 3 deletions

View file

@ -65,4 +65,6 @@ These fall through to xterm.js and the shell regardless of platform:
- `Ctrl+Z` — SIGTSTP
- `Ctrl+\` — SIGQUIT
- `Home` / `End` — start/end of line (xterm's terminfo sequences)
- `Ctrl+R` / `Ctrl+U` / `Ctrl+A` / `Ctrl+E` / `Ctrl+K` / `Ctrl+W` — readline
- `Ctrl+R` / `Ctrl+U` / `Ctrl+A` / `Ctrl+E` — readline
Note: on Linux/Windows, `Ctrl+K` and `Ctrl+W` ARE intercepted by the app (they are `Mod+K` / `Mod+W` — see the Windows/Linux table above). On macOS, `Mod` is Cmd, so `Ctrl+K` and `Ctrl+W` fall through to readline there.

View file

@ -9,8 +9,12 @@ import type { TerminalShortcutAction } from './terminal-shortcut-policy-types'
* - `nonMac: true` entry applies on Windows/Linux (mod = Ctrl)
* - both platform-agnostic chord
*
* Ordering matters: the first matching row wins. More specific chords (those
* requiring Shift) must come before the base variant or they'll never fire.
* Entries are mutually exclusive by construction: `chordMatches` (in
* terminal-shortcut-policy.ts) rejects any event whose modifier set isn't
* exactly what the entry requires, so a base chord (e.g. Mod+D) cannot match
* a Shift-variant event regardless of where it sits in this array. Ordering
* is not load-bearing for correctness keep entries grouped by feature for
* readability only.
*
* Chords that require dynamic runtime state (Mac Option-as-Alt composition,
* which branches on `optionKeyLocation` and the `macOptionAsAlt` setting) are

View file

@ -64,6 +64,52 @@ async function getPtyWrites(app: ElectronApplication): Promise<string[]> {
})
}
// Why: copySelection writes through the `clipboard:writeText` ipcMain.handle
// channel. Replace the handler (instead of spying on Electron's clipboard
// object) so the test both captures the call and avoids mutating the real
// system clipboard on headful runs.
async function installMainProcessClipboardSpy(app: ElectronApplication): Promise<void> {
await app.evaluate(({ ipcMain }) => {
const g = globalThis as unknown as {
__clipboardTextLog?: string[]
__clipboardSpyInstalled?: boolean
}
if (g.__clipboardSpyInstalled) {
return
}
g.__clipboardTextLog = []
g.__clipboardSpyInstalled = true
ipcMain.removeHandler('clipboard:writeText')
ipcMain.handle('clipboard:writeText', (_event, text: string) => {
g.__clipboardTextLog!.push(text)
})
})
}
async function getClipboardWrites(app: ElectronApplication): Promise<string[]> {
return app.evaluate(() => {
const g = globalThis as unknown as { __clipboardTextLog?: string[] }
return g.__clipboardTextLog ?? []
})
}
async function selectAllActiveTerminal(page: Page): Promise<void> {
await page.evaluate(() => {
const paneManagers = window.__paneManagers
if (!paneManagers) {
return
}
const state = window.__store?.getState()
const tabId = state?.activeTabId
if (!tabId) {
return
}
const manager = paneManagers.get(tabId)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0]
pane?.terminal.selectAll()
})
}
// Why: the window-level keydown handler is gated on non-editable targets; the
// xterm helper textarea is treated as non-editable on purpose. Focusing it
// guarantees each chord reaches the shortcut policy through the real DOM path.
@ -131,6 +177,7 @@ test.describe('Terminal Shortcuts', () => {
electronApp
}) => {
await installMainProcessPtyWriteSpy(electronApp)
await installMainProcessClipboardSpy(electronApp)
// Seed the buffer so Cmd+K has something to clear.
const ptyId = await discoverActivePtyId(orcaPage)
@ -138,6 +185,20 @@ test.describe('Terminal Shortcuts', () => {
await execInTerminal(orcaPage, ptyId, `echo ${marker}`)
await waitForTerminalOutput(orcaPage, marker)
// --- copy-selection chord (Mod+Shift+C) ---
// Why: must run before Cmd+K clears the buffer; selectAll + Mod+Shift+C
// must route through the shortcut policy and write the selection through
// the clipboard:writeText IPC (which the spy captured).
await selectAllActiveTerminal(orcaPage)
await focusActiveTerminal(orcaPage)
await orcaPage.keyboard.press(`${mod}+Shift+c`)
await expect
.poll(async () => (await getClipboardWrites(electronApp)).some((t) => t.includes(marker)), {
timeout: 5_000,
message: 'Mod+Shift+C did not write the terminal selection to the clipboard'
})
.toBe(true)
// --- send-input chords (platform-agnostic) ---
// Alt+←/→ → readline backward-word / forward-word (\eb / \ef).