test(terminal): pin the macOS key-binding substitution against #11170 (#13315)

* fix(terminal): show a preedit the IME resumes without a compositionstart

Typing 2-Set Korean shows committed syllables but not the in-progress jamo, so
the user composes each syllable blind. Long-standing hole in the vendored
terminal library, not a regression: the same test fails identically against the
bundle this branch starts from.

The `.active` class that CSS keys `display: block` off is added only in
`compositionstart` and dropped in `_finalizeComposition`. Some IMEs (observed on
Windows/WSL Korean) resume a composition with a bare `compositionupdate` and no
second `compositionstart`, by which point `compositionend` has already hidden the
overlay, so the resumed preedit is written into a hidden element and never
positioned. `updateCompositionElements` also early-returned on `!_isComposing`,
so it would not lay the overlay out either.

Re-show the overlay on an update that carries data, and key the layout guard on
the shown overlay instead. `_isComposing` is deliberately left alone, so no
commit bookkeeping changes and `onData` stays byte-identical. The two guards are
equivalent on every pre-existing path: `compositionstart` sets both,
`_finalizeComposition` clears both.

The bundle hunks are the same two edits applied to the shipped minified output;
the sourcemaps are carried through unchanged.

* test(terminal): prove the resumed-preedit fix against a recorded Windows capture

The synthetic test pins the shape; this replays events a real Microsoft Korean
IME emitted on Windows/WSL. The capture holds three compositionupdates that
resume a composition with no second compositionstart — the exact ordering that
wrote the preedit into a hidden overlay.

Without the fix all three report shown:false; with it all three are visible.
Fixture derived from the sealed 11919-windows-wsl-current capture, which is
read-only and unmodified.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): stop the recorded Hangul fixture pinning a derivation artifact

The capture logs each event twice — a dispatch record and a batched next-frame
re-log. Deriving from both replayed every event twice, which made three
compositionupdates appear to land after a session had ended. Filtered to
dispatch records the capture holds zero resumes and 11 balanced sessions, so
the previous toHaveLength(3) was pinning an artifact of the derivation.

Re-scoped to what the capture does prove: the preedit stays visible across all
37 real updates. Verified by reverting the patch that this passes either way,
so it is coverage and the synthetic test remains the discriminator. Both facts
are now stated in the file.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): restore the preedit visibility patch onto its own branch

The previous commit accidentally reverted it: checking main's patch and lockfile
into the worktree to test whether a test discriminates also stages them, so the
commit that followed swept them up.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): claim printable keydowns structurally so committed text survives

Co-authored-by: Orca <help@stably.ai>

* chore(reliability-gates): retarget the IME forwarding gate after the allowlist removal

The gate listed terminal-ime-input-source.test.ts, which went with the
input-source allowlist. Points at the substituted-text commit test instead,
which covers what the gate is actually protecting: text committed outside a
composition session reaching the pty exactly once.

Co-authored-by: Orca <help@stably.ai>

* docs(terminal): record why withholding a claimed keydown needs no timer

The predicate withholds a keydown's byte until the commit arrives, so a key the
IME eats without committing would be dropped. Measured across the recorded
corpus that case does not occur, and the browser marks IME-owned presses on the
keydown itself. Both facts belong next to the predicate rather than only in a
handoff note, since the obvious fix for the imagined gap is a timer, and a timer
here once wrote a newline the user never typed.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the kitty all-keys-as-escape-codes hole explicitly

Flag 8 asks for every printable key as an escape code; this path sends the
committed text raw instead. That is a deliberate trade, not an oversight, but it
was untested — the suite only covered the disambiguate flag. Pinning it makes
the choice visible and records the gate to use if it ever needs closing.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): keep the kitty key-release report for presses that reached the pty

Claiming the keyup unconditionally suppressed xterm's release report. That was
sized for the old design, which claimed only a short punctuation list; the
structural claim takes every printable keydown, so on macOS an app that
negotiated kitty report_event_types stopped seeing releases for ordinary typing
and would treat every printable key as held down.

Suppress the release only when the press put nothing on the wire — swallowed by
the input source, or owned by a composition transaction. xterm emits nothing
from keyup unless kitty report_event_types (or win32 input mode) is on, so
letting it through is inert everywhere else.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the macOS key-binding substitution against #11170

An OS key-binding remap of the character a Korean layout puts on Backquote
is honoured everywhere on macOS except the terminal, which sent the raw
layout character to the PTY. The substitution is applied inside the system
text input path, so it exists only on keypress.charCode and the input
event's data; the keydown still carries the layout character. Nothing needs
to parse the binding file - Chromium has already applied it by the time
`input` fires.

The reported build sent the raw character. A later punctuation table
happened to list that one character, which closed the issue by enumeration
rather than by design, and the structural claim removes the table entirely.
Without a test the fix could regress silently on a change that never
mentions the issue.

Replays the reporter's captured event shape and pairs it with the same
physical key carrying no substitution, so a fix that rewrote the Backquote
position unconditionally would fail. Discrimination checked by mutation:
suppressing the structural claim, and separately removing the single table
character on a pre-rewrite tree, each make the replay send the raw layout
character while both negatives stay green.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): cover the other Korean layout on the remapped key

Korean layouts disagree about what the backquote position produces: two of
them give the currency sign the issue reports, one gives an asterisk. One
key-binding entry has to survive either, but honouring the substitution by
listing characters covers only the ones someone remembered to list - which
is why the reported character worked and this one did not.

This arm discriminates without a mutation: it fails on the pre-rewrite tree
and passes on the structural claim. The harness supplies no input-source
classification, modelling a source the older design did not recognise,
including the window before its async probe resolves. With the source
recognised the older design claimed all ASCII punctuation and covered this
too, so the gap was real but conditional; the header says so rather than
letting the failure read as unconditional.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): drop the Won-setting arm from the keybinding-dict replay

The Won-to-backquote feature was reverted, so the module this replay imported
no longer exists. The #11170 coverage is unaffected: the remaining arms pin the
substitution itself, which never depended on that setting.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): correct the fixture's provenance count

The header said two derived cases when there are four, and counted the
second layout arm as a negative when it is a positive. Each case already
carries its own recorded flag and note; this stops the summary contradicting
them, which matters in the one field whose whole job is provenance.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-08-09 16:03:52 -07:00 committed by GitHub
parent bf406720a0
commit cb9aa12fff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 417 additions and 3 deletions

View File

@ -6658,13 +6658,14 @@
"invariant": "Composition, native text forwarding, synthetic input, paste, and platform keyboard bypass paths must not send preedit/control bytes before commit and must commit text exactly once to the intended PTY.",
"oracle": "The current renderer-unit slice asserts native text commits route to the intended PTY once, composition/preedit bookkeeping does not leak premature text, input-source classification handles synthetic/native paths, paste/runtime forwarding avoids duplicate terminal payloads for covered fixtures, and Linux/Sogou candidate Space/digit selectors do not leak keydown/keypress/keyup while ordinary and long-held letter-to-digit typing remains available. The Electron/CDP live-PTY repro verifies Sogou-style Space and digit selectors submit only the committed Chinese text, while the legacy orphaned-letter-keyup sequence sends no selector byte to the PTY. Real legacy IME commit preservation and the full CJK/Vietnamese/Arabic/JIS-yen matrix run in follow-up platform soak where automation is possible.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-linux-candidate-state.test.ts",
"pnpm run test:e2e -- tests/e2e/chinese-ime-chat-input-repro.spec.ts"
],
"testFiles": [
"src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts",
"src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts",
"src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts",
@ -6687,6 +6688,13 @@
"synthetic and native input-source paths are classified for covered fixtures"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts",
"assertions": [
"an OS key-binding substitution reaches the PTY instead of the raw layout character",
"the same key with no substitution in play still sends the layout character"
]
},
{
"file": "src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts",
"assertions": ["paste/runtime forwarding avoids duplicate terminal payloads"]
@ -6739,6 +6747,15 @@
}
],
"evidenceRuns": [
{
"date": "2026-08-09",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"result": "passed",
"durationSeconds": 0.7,
"summary": "8 test file(s) passed, 143 tests, after adding the OS key-binding substitution replay. Discrimination checked by mutation rather than assumed: suppressing the structural claim, and separately removing the single character the older punctuation table happened to list, each make the replay send the raw layout character while both paired negatives stay green."
},
{
"date": "2026-07-11",
"runner": "local",
@ -6770,10 +6787,10 @@
"date": "2026-07-07",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts",
"result": "passed",
"durationSeconds": 0.7,
"summary": "7 test file(s) passed, 144 tests passed on the Linux/Sogou candidate-key branch (includes held-key repeat guard coverage)."
"summary": "7 test file(s) passed, 144 tests passed on the Linux/Sogou candidate-key branch (includes held-key repeat guard coverage). The command shown is the gate's current one, which the manifest check requires; the run itself predates two of the files it now lists. The 2026-08-09 entry is the one that covers the current slice."
},
{
"date": "2026-07-07",

View File

@ -0,0 +1,207 @@
{
"recordedFrom": "issue #11170, probe page in Chromium on macOS (reporter's hardware, Korean input source active)",
"platform": "macos",
"note": "Exactly one case is recorded: `remapped`, the reporter's verbatim capture, where keydown and keypress both still carry the layout character and only charCode and the input event's data carry the substituted one. Every other case is DERIVED from it by changing one thing - the layout character, or the substitution's presence - and is marked `recorded: false` with its own note saying what was changed and why. Do not read the four derived cases as independent captures. No beforeinput record exists in the capture; none is invented here.",
"cases": [
{
"name": "remapped",
"note": "Verbatim from the issue. ~/Library/KeyBindings/DefaultKeyBinding.dict maps the character the Korean layout puts on Backquote to a backtick. keydown.key is the raw layout character; charCode 96 and input.data carry the OS substitution.",
"recorded": true,
"expectedPty": "`",
"dom": [
{
"type": "keydown",
"key": "₩",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": ""
},
{
"type": "keypress",
"key": "₩",
"code": "Backquote",
"keyCode": 96,
"charCode": 96,
"isComposing": false,
"value": ""
},
{
"type": "input",
"data": "`",
"inputType": "insertText",
"isComposing": false,
"value": "`"
},
{
"type": "keyup",
"key": "₩",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": "`"
}
]
},
{
"name": "remapped-other-korean-layout",
"note": "DERIVED, not separately recorded: the `remapped` capture with the layout character changed to the one 세벌식 최종 puts on the same physical key. Korean layouts disagree about that position - 두벌식 and 세벌식 390 give U+20A9, 세벌식 최종 gives an asterisk - so one dict entry has to survive either. Honouring the substitution by listing characters can only cover the ones someone remembered to list.",
"recorded": false,
"expectedPty": "`",
"dom": [
{
"type": "keydown",
"key": "*",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": ""
},
{
"type": "keypress",
"key": "*",
"code": "Backquote",
"keyCode": 96,
"charCode": 96,
"isComposing": false,
"value": ""
},
{
"type": "input",
"data": "`",
"inputType": "insertText",
"isComposing": false,
"value": "`"
},
{
"type": "keyup",
"key": "*",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": "`"
}
]
},
{
"name": "no-remap-korean",
"note": "DERIVED, not separately recorded: the `remapped` capture with the dict substitution removed, so the layout's own character is what the text system commits. charCode and input.data become U+20A9 to match keydown.key. This is the paired negative - the same physical key with no remap in play must still deliver the layout character.",
"recorded": false,
"expectedPty": "₩",
"dom": [
{
"type": "keydown",
"key": "₩",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": ""
},
{
"type": "keypress",
"key": "₩",
"code": "Backquote",
"keyCode": 8361,
"charCode": 8361,
"isComposing": false,
"value": ""
},
{
"type": "input",
"data": "₩",
"inputType": "insertText",
"isComposing": false,
"value": "₩"
},
{
"type": "keyup",
"key": "₩",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": "₩"
}
]
},
{
"name": "no-remap-ascii",
"note": "DERIVED, not separately recorded. The issue states that with ABC selected keydown.key is already the backtick and nothing is wrong; this is that sequence. Guards the case where a fix rewrites the Backquote position unconditionally.",
"recorded": false,
"expectedPty": "`",
"dom": [
{
"type": "keydown",
"key": "`",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": ""
},
{
"type": "keypress",
"key": "`",
"code": "Backquote",
"keyCode": 96,
"charCode": 96,
"isComposing": false,
"value": ""
},
{
"type": "input",
"data": "`",
"inputType": "insertText",
"isComposing": false,
"value": "`"
},
{
"type": "keyup",
"key": "`",
"code": "Backquote",
"keyCode": 192,
"isComposing": false,
"value": "`"
}
]
},
{
"name": "remapped-neighbour-key",
"note": "DERIVED, not separately recorded. An ordinary letter pressed straight after the remapped key, to catch a fix that leaves the claim armed and swallows or duplicates the next keystroke.",
"recorded": false,
"expectedPty": "a",
"dom": [
{
"type": "keydown",
"key": "a",
"code": "KeyA",
"keyCode": 65,
"isComposing": false,
"value": ""
},
{
"type": "keypress",
"key": "a",
"code": "KeyA",
"keyCode": 97,
"charCode": 97,
"isComposing": false,
"value": ""
},
{
"type": "input",
"data": "a",
"inputType": "insertText",
"isComposing": false,
"value": "a"
},
{
"type": "keyup",
"key": "a",
"code": "KeyA",
"keyCode": 65,
"isComposing": false,
"value": "a"
}
]
}
]
}

View File

@ -0,0 +1,190 @@
// @vitest-environment happy-dom
// #11170: a ~/Library/KeyBindings/DefaultKeyBinding.dict remap is honoured everywhere on macOS
// except the terminal, which sends the raw layout character to the PTY.
//
// The OS applies the substitution inside the AppKit text input path, so it exists only on
// keypress.charCode and on the input event's `data` — keydown.key still carries the layout
// character. Nothing here needs to parse the dict: Chromium's renderer is already the text
// system's client and has applied it by the time `input` fires. The bug was that the keydown
// manufactured a byte and preventDefault'd, tearing the text pipeline down before the
// substitution could arrive.
//
// This pins the behaviour across a design change rather than introducing it. The reported build
// sent the raw character; a later punctuation table happened to list that one character, which
// closed the issue by enumeration. Replacing the table with a structural claim removes the entry,
// so without a test the fix could regress silently on a change that never mentions #11170.
// Both designs fail this file when their respective claim is removed.
//
// The harness supplies no input-source classification, which models a source the older design did
// not recognise — including the window before its async probe resolves. That is the condition
// under which the second layout arm separates the two designs: with the source recognised the
// older one claimed all ASCII punctuation and covered it too, so the gap was real but conditional.
// The structural claim has no such condition, which is the point.
//
// Replays the recorded event shape from the issue rather than an authored one, and pairs it with
// the same physical key carrying no substitution — a fix that rewrote the Backquote position
// unconditionally would pass the positive case and be badly wrong.
import { Terminal } from '@xterm/xterm'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import trace from './__fixtures__/macos-keybinding-dict-backquote-trace.json'
import { installTerminalImeNativeTextForwarder } from './terminal-ime-native-text-forwarder'
import { shouldBypassXtermKeyboardEvent } from './xterm-bypass-policy'
type RecordedEvent = {
type: string
key?: string
code?: string
keyCode?: number
charCode?: number
data?: string
inputType?: string
isComposing?: boolean
value?: string
}
type RecordedCase = {
name: string
expectedPty: string
dom: RecordedEvent[]
}
const CASES = trace.cases as RecordedCase[]
function caseNamed(name: string): RecordedCase {
const found = CASES.find((entry) => entry.name === name)
if (!found) {
throw new Error(`fixture case '${name}' missing`)
}
return found
}
function buildEvent(recorded: RecordedEvent): Event {
if (recorded.type === 'input' || recorded.type === 'beforeinput') {
const input = new InputEvent(recorded.type, {
isComposing: recorded.isComposing,
bubbles: true
})
// happy-dom drops these from InputEventInit; Chromium supplies them.
Object.defineProperty(input, 'inputType', { value: recorded.inputType ?? '' })
Object.defineProperty(input, 'data', { value: recorded.data ?? null })
Object.defineProperty(input, 'composed', { value: true })
return input
}
const keyboard = new KeyboardEvent(recorded.type, {
key: recorded.key,
code: recorded.code,
isComposing: recorded.isComposing,
bubbles: true,
cancelable: true
})
Object.defineProperty(keyboard, 'keyCode', { value: recorded.keyCode ?? 0 })
Object.defineProperty(keyboard, 'charCode', { value: recorded.charCode ?? 0 })
return keyboard
}
/** Mirrors the handler order in use-terminal-pane-lifecycle.ts: the native-text claim,
* then the bypass policy. */
function open() {
const container = document.createElement('div')
document.body.appendChild(container)
const terminal = new Terminal()
terminal.open(container)
const forwarder = installTerminalImeNativeTextForwarder({
terminalElement: terminal.element,
isComposing: () => false,
sendInput: (data) => terminal.input(data)
})
terminal.attachCustomKeyEventHandler((event) => {
if (forwarder.claimKeyEvent(event)) {
return false
}
return !shouldBypassXtermKeyboardEvent(event, {
isMac: true,
hasSelection: false,
kittyKeyboardFlags: 0
})
})
const emitted: string[] = []
terminal.onData((data) => emitted.push(data))
return { emitted, terminal, forwarder }
}
/** Replays recorded cases in order and returns the bytes that reached the PTY. */
function replay(names: string[]): string {
const { emitted, terminal, forwarder } = open()
const textarea = terminal.textarea!
for (const name of names) {
for (const recorded of caseNamed(name).dom) {
const event = buildEvent(recorded)
// Why: the recorded keydown is what a real browser only emits the rest of the sequence
// after, so a claim that preventDefaults must truncate the replay exactly as Chromium would.
if (recorded.type !== 'keydown' && recorded.type !== 'keyup') {
textarea.value = recorded.value ?? ''
textarea.setSelectionRange(textarea.value.length, textarea.value.length)
}
textarea.dispatchEvent(event)
if (recorded.type === 'keydown' && event.defaultPrevented) {
break
}
}
}
forwarder.dispose()
terminal.dispose()
return emitted.join('')
}
describe('#11170 — a DefaultKeyBinding.dict remap reaches the PTY', () => {
beforeEach(() => {
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
measureText: () => ({ width: 10 })
} as unknown as CanvasRenderingContext2D)
})
afterEach(() => {
vi.restoreAllMocks()
document.body.replaceChildren()
})
it('sends the remapped character, not the raw layout character', () => {
expect(replay(['remapped'])).toBe(caseNamed('remapped').expectedPty)
})
// Same physical key, same dict entry, on the Korean layout that puts an asterisk there instead.
// This is the arm that discriminates without a mutation: the older design honoured the
// substitution by listing characters, and listed the reported one but not this one.
it('sends the remapped character on the other Korean layout too', () => {
const remapped = caseNamed('remapped-other-korean-layout')
expect(replay([remapped.name])).toBe(remapped.expectedPty)
})
// The paired negative. Same physical key, same keydown.key, no substitution in play.
it('still sends the layout character when nothing remaps the key', () => {
expect(replay(['no-remap-korean'])).toBe(caseNamed('no-remap-korean').expectedPty)
})
it('still sends the plain backquote under an ASCII layout', () => {
expect(replay(['no-remap-ascii'])).toBe(caseNamed('no-remap-ascii').expectedPty)
})
it('leaves the next keystroke untouched', () => {
expect(replay(['remapped', 'remapped-neighbour-key'])).toBe('`a')
})
// The dict's "~₩" rule (Option+the key -> the layout character) is out of scope: Option chords
// are deliberately excluded from the claim, and the option-as-alt setting consumes Option first.
it('does not claim the Option chord the dict also binds', () => {
const { emitted, terminal, forwarder } = open()
const textarea = terminal.textarea!
const chord = new KeyboardEvent('keydown', {
key: '₩',
code: 'Backquote',
altKey: true,
bubbles: true,
cancelable: true
})
Object.defineProperty(chord, 'keyCode', { value: 192 })
textarea.dispatchEvent(chord)
forwarder.dispose()
terminal.dispose()
expect(emitted.join('')).not.toBe('')
})
})