From 6f7a30ac2e43db980d91265d5d83c09e05ce4dfd Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 3 Aug 2026 02:21:56 -0700 Subject: [PATCH] test(terminal): preserve Windows IME Shift commits (#12276) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: yoke233 --- .../e2e/chinese-ime-chat-input-repro.spec.ts | 81 ++++++++++++++++++- .../e2e/helpers/windows-ime-native-events.ts | 48 +++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/helpers/windows-ime-native-events.ts diff --git a/tests/e2e/chinese-ime-chat-input-repro.spec.ts b/tests/e2e/chinese-ime-chat-input-repro.spec.ts index 3cdac5246..8c57b8e6a 100644 --- a/tests/e2e/chinese-ime-chat-input-repro.spec.ts +++ b/tests/e2e/chinese-ime-chat-input-repro.spec.ts @@ -12,6 +12,11 @@ import { waitForActiveTerminalManager, waitForTerminalOutput } from './helpers/terminal' +import { + dispatchWindowsImeShiftToggle, + readPtyInputCount, + readPtyInputs +} from './helpers/windows-ime-native-events' type ImeEventLogEntry = { type: string @@ -81,12 +86,16 @@ const CODEX_TRUST_PROMPT_RE = /Do you trust|trust this folder|Trust this/i const CODEX_UPDATE_PROMPT_RE = /update available|install update|Skip for now/i const LINUX_IME_POLICY_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/146 Safari/537.36' +const WINDOWS_IME_POLICY_USER_AGENT = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/150 Safari/537.36' -function terminalImeHarnessScript(runId: string): string { +function terminalImeHarnessScript(runId: string, inputLogPath?: string): string { return ` const readline = require('node:readline') +const { appendFileSync } = require('node:fs') const runId = ${JSON.stringify(runId)} +const inputLogPath = ${JSON.stringify(inputLogPath ?? null)} let model = '' let cursor = 0 const submitted = [] @@ -130,6 +139,7 @@ function removeBeforeCursor() { } function handleData(data) { + if (inputLogPath) appendFileSync(inputLogPath, JSON.stringify(data) + '\\n') let index = 0 while (index < data.length) { if (data.startsWith('\\x1b[D', index)) { @@ -244,6 +254,30 @@ async function readImeEventLog(page: Page): Promise { }) } +async function readActiveCompositionText(page: Page): Promise { + return page.evaluate(() => { + const active = document.activeElement + if (!(active instanceof HTMLTextAreaElement)) { + throw new Error('xterm helper textarea is not focused') + } + const view = active.closest('.xterm')?.querySelector('.composition-view') + return view?.classList.contains('active') + ? (view.textContent?.replaceAll('\u200e', '') ?? '') + : '' + }) +} + +async function reloadWithWindowsImePolicy(page: Page): Promise { + await page.addInitScript((userAgent) => { + Object.defineProperty(navigator, 'userAgent', { + get: () => userAgent, + configurable: true + }) + }, WINDOWS_IME_POLICY_USER_AGENT) + await page.reload({ waitUntil: 'domcontentloaded' }) + await page.waitForFunction(() => Boolean(window.__store), null, { timeout: 30_000 }) +} + async function reloadWithLinuxImePolicy(page: Page): Promise { await page.addInitScript((userAgent) => { Object.defineProperty(navigator, 'userAgent', { @@ -561,6 +595,51 @@ test.describe('Chinese IME terminal chat input repro', () => { } }) + test('commits the active Pinyin preedit when Shift toggles the Windows IME', async ({ + orcaPage, + testRepoPath + }, testInfo) => { + await reloadWithWindowsImePolicy(orcaPage) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + await waitForActiveTerminalManager(orcaPage, 30_000) + + const ptyId = await waitForActivePanePtyId(orcaPage) + const runId = randomUUID() + const scriptPath = path.join(testRepoPath, `.orca-windows-shift-ime-${runId}.cjs`) + const inputLogPath = path.join(testRepoPath, `.orca-windows-shift-ime-${runId}.jsonl`) + writeFileSync(scriptPath, terminalImeHarnessScript(runId, inputLogPath)) + writeFileSync(inputLogPath, '') + const session = await orcaPage.context().newCDPSession(orcaPage) + + try { + await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) + await waitForTerminalOutput(orcaPage, `IME_HARNESS_READY_${runId}`, 10_000, 20_000) + await focusActiveTerminalInput(orcaPage) + await installImeEventProbe(orcaPage) + + await setImeComposition(session, 's') + const inputCountBeforeShift = readPtyInputCount(inputLogPath) + await dispatchWindowsImeShiftToggle(session) + + await waitForLivePrompt(orcaPage, 's') + await expect.poll(() => readActiveCompositionText(orcaPage)).toBe('') + await expect.poll(async () => (await readPromptState(orcaPage))?.submitted).toEqual([]) + await expect + .poll(() => readPtyInputs(inputLogPath).slice(inputCountBeforeShift)) + .toEqual(['s']) + } finally { + await attachImeEvidence(orcaPage, testInfo, 'windows-shift-ime-evidence').catch( + () => undefined + ) + await session.detach().catch(() => undefined) + await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined) + rmSync(scriptPath, { force: true }) + rmSync(inputLogPath, { force: true }) + } + }) + test('keeps Sogou-style candidate selection keys out of the PTY while committing Chinese text', async ({ orcaPage, testRepoPath diff --git a/tests/e2e/helpers/windows-ime-native-events.ts b/tests/e2e/helpers/windows-ime-native-events.ts new file mode 100644 index 000000000..d30708dd9 --- /dev/null +++ b/tests/e2e/helpers/windows-ime-native-events.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs' +import type { CDPSession } from '@stablyai/playwright-test' + +export function readPtyInputs(inputLogPath: string): string[] { + return readFileSync(inputLogPath, 'utf8') + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line) as string) +} + +export function readPtyInputCount(inputLogPath: string): number { + return readPtyInputs(inputLogPath).length +} + +export async function dispatchWindowsImeShiftToggle(session: CDPSession): Promise { + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + key: 'Process', + code: 'ShiftLeft', + windowsVirtualKeyCode: 229, + nativeVirtualKeyCode: 229, + modifiers: 8 + }) + await session.send('Input.dispatchKeyEvent', { + type: 'rawKeyDown', + key: 'Shift', + code: 'ShiftLeft', + windowsVirtualKeyCode: 16, + nativeVirtualKeyCode: 16, + modifiers: 8 + }) + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'Process', + code: 'ShiftLeft', + windowsVirtualKeyCode: 229, + nativeVirtualKeyCode: 229, + modifiers: 8 + }) + await session.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: 'Shift', + code: 'ShiftLeft', + windowsVirtualKeyCode: 16, + nativeVirtualKeyCode: 16 + }) + await session.send('Input.insertText', { text: 's' }) +}