Files
orca/tests/e2e/terminal-korean-composing-chord-order.spec.ts
Neil a663f1bd00 fix(terminal): hold a cursor chord until the composing syllable commits (#14730)
* fix(terminal): hold a cursor chord until the composing syllable commits

The composed glyph reaches the pty from the composition session-end handler,
which runs after the chord's keydown. Only Enter was held for that, so every
other chord went straight out on the transport and overtook the text it was
typed after: with 가나 on the line, typing 가나다 and pressing Cmd+Left left
다가나, the composing 다 landed at the cursor's destination.

Defer any sendInput chord while a composition is live or its session has not
yet flushed. Korean 2-Set shows the shape most clearly — the platform replays
the chord unmarked after keyup, so isComposing is already false while the
session is still pending.

No fallback timer on this path. A newline arriving late still arrives, which is
what that timer is for; a chord arriving mid-preedit is the corruption the wait
exists to prevent, and a conversion can hold its candidate window open for
seconds. Dropping the chord costs one keypress, firing early costs a line.

Pane commands are unaffected: they are not sendInput actions.

Fixes #12871

* test(e2e): pin the composing-chord order at the pty

The unit coverage asserts the handler's ordering against a synthetic transport.
This asserts it where it is actually observable: the committed glyph and the
chord reach the pty by two different routes, and only their merged order is
visible to the shell.

Verified to discriminate — against keyboard-handlers.ts from main the same spec
reads 01 eb8ba4 0a, the chord ahead of the syllable, which is the reported
corruption byte-for-byte.

* refactor(terminal): add the composing-chord deferral without touching the Enter path

Nesting the new branch inside the Enter condition re-indented the whole Enter
block, which is the kind of diff that can silently change it. Keeping them as
sibling conditions leaves the Enter path out of the diff entirely.

* test(e2e): pin the renderer to macOS for the Cmd+Left chord

Cmd+Left resolves to \x01 only under the macOS branch of the shortcut policy, so
on a Linux shard the chord produced no byte and the spec passed by measuring
nothing — it failed in CI for that reason, not for the behaviour under test.
Pinning the platform is the established pattern for these specs, and
expectImePlatformPolicy fails loudly if the override does not take.
2026-08-15 00:13:27 -07:00

88 lines
3.7 KiB
TypeScript

/**
* #12871: a cursor chord pressed while a syllable is still composing reached the pty ahead of the
* text it was typed after. With `가나` on the line, typing `가나다` and pressing Cmd+Left left
* `다가다` — the composing `다` landed at the cursor's destination.
*
* The unit coverage in `keyboard-handlers-ime-composing-chord.test.tsx` pins the handler's
* ordering against a synthetic transport. This asserts the same ordering at the pty, where the
* committed glyph and the chord arrive by two different routes — the composition session-end
* handler and the transport — and only their merged order is observable.
*/
import { expect, test } from './helpers/orca-app'
import { closeTerminalImePaneArena, openTerminalImePaneArena } from './terminal-ime-pane-arena'
import {
commitImeText,
composeHangulSyllable,
dispatchPlainEnter,
type ImeKeyIdentity
} from './terminal-ime-cdp-composition'
import {
createTerminalImeByteReader,
removeTerminalImeByteReader,
startTerminalImeByteReader,
waitForTerminalImeBytes
} from './terminal-ime-byte-reader'
import { applyImePlatformPolicy, expectImePlatformPolicy } from './terminal-ime-platform-policy'
const JAMO: Record<string, ImeKeyIdentity> = {
ㄷ: { key: 'ㄷ', code: 'KeyE', keyCode: 229 },
ㅏ: { key: 'ㅏ', code: 'KeyK', keyCode: 229 }
}
/** ㄷ → ㅏ assembles 다, which stays composing because a final consonant could still follow. */
const DA_FRAMES = [
{ jamoKey: JAMO['ㄷ'], preedit: 'ㄷ' },
{ jamoKey: JAMO['ㅏ'], preedit: '다' }
] as const
/** Cmd+Left resolves to Ctrl+A (readline start-of-line) — the chord from the report. */
const CMD_LEFT_BYTE = '\x01'
test.describe('Terminal 2-Set Korean composing-chord order', () => {
test('sends the composing syllable before a Cmd+Left pressed during it', async ({
orcaPage,
testRepoPath
}, testInfo) => {
// Cmd+Left resolves to \x01 only under the macOS branch of the shortcut policy, so a Linux
// runner produces no chord byte at all and the spec would pass by measuring nothing. Pinning
// the renderer's platform is what lets the reported chord run on any shard; the assertion
// below fails loudly if the override did not take.
await applyImePlatformPolicy(orcaPage, 'mac')
await expectImePlatformPolicy(orcaPage, 'mac')
const arena = await openTerminalImePaneArena(orcaPage)
const reader = createTerminalImeByteReader(testRepoPath, 1)
let completed = false
try {
await startTerminalImeByteReader(orcaPage, arena.ptyId, reader)
await composeHangulSyllable(arena.session, orcaPage, DA_FRAMES)
// Pressed while the syllable is still in preedit. Before the fix this reached the pty
// immediately, ahead of the 다 that had been typed first.
await arena.session.send('Input.dispatchKeyEvent', {
type: 'rawKeyDown',
key: 'ArrowLeft',
code: 'ArrowLeft',
windowsVirtualKeyCode: 37,
nativeVirtualKeyCode: 37,
modifiers: 4
})
await commitImeText(arena.session, '다')
// The held chord flushes a macrotask after the composition session ends. Enter is only here
// to terminate the line for the reader, so let the chord land before adding it — otherwise
// the newline overtakes it and the assertion measures the wrong pair.
await orcaPage.waitForTimeout(250)
await dispatchPlainEnter(arena.session)
const received = await waitForTerminalImeBytes(orcaPage, reader)
expect(received).toEqual([Buffer.from(`다${CMD_LEFT_BYTE}\n`).toString('hex')])
completed = true
} finally {
await closeTerminalImePaneArena(arena, testInfo, 'korean-composing-chord-order', !completed)
removeTerminalImeByteReader(reader)
}
})
})