test(terminal): preserve Windows IME Shift commits (#12276)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: yoke233 <yoke2012@gmail.com>
This commit is contained in:
parent
339045b150
commit
6f7a30ac2e
|
|
@ -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<ImeEventLogEntry[]> {
|
|||
})
|
||||
}
|
||||
|
||||
async function readActiveCompositionText(page: Page): Promise<string> {
|
||||
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<HTMLElement>('.composition-view')
|
||||
return view?.classList.contains('active')
|
||||
? (view.textContent?.replaceAll('\u200e', '') ?? '')
|
||||
: ''
|
||||
})
|
||||
}
|
||||
|
||||
async function reloadWithWindowsImePolicy(page: Page): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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' })
|
||||
}
|
||||
Loading…
Reference in New Issue