mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
* Revert "test(ime): restore coverage the composition-ownership change removed (#13168)" This reverts commit25a8c517e1. * Revert "refactor(terminal): return IME composition ownership to xterm (#13128)" This reverts commit17b3dff3c4. * test(ime): keep the architecture-neutral Korean trace coverage The recorded IBus/fcitx5 and Windows MS-Korean traces from #13168 assert PTY byte order, not composition ownership, so they still hold once the terminal composition layer is restored. The mobile accessory-order test pinned the new handleLiveInputChange signature and does not. Co-authored-by: Orca <help@stably.ai> * fix(terminal): keep the macOS Backslash bypass through the revert The restored native-text forwarder only claims keys for input sources in its hardcoded CJK allowlist, so third-party IMEs off that list (Qingg, #10896) still get a raw backslash. #13128 added this bypass as a partial replacement; keep it rather than trade the open issue back. Scoped to the bare backslash key. The rest of shouldBypassXtermForMacNativeText bypassed all unmodified non-ASCII text, which would race the restored forwarder. Co-authored-by: Orca <help@stably.ai> * fix(mobile): move the mirror-step ref write out of render The restored hook assigned runMirrorStepRef during render, which is not replay-safe — React can discard render work, so the mutation can leak from UI that never commits. Its only read is inside the held-commit timer, which fires long after commit, and the ref has a safe default, so an effect is soon enough. Surfaced by the changed-lines React Doctor gate: the rule postdates this code, so restoring the file re-introduced it as a new violation. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
import { execFileSync } from 'node:child_process'
|
|
import type { Page, TestInfo } from '@stablyai/playwright-test'
|
|
import { expect, test } from './helpers/orca-app'
|
|
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
|
import {
|
|
focusActiveTerminalInput,
|
|
sendToTerminal,
|
|
waitForActivePanePtyId,
|
|
waitForActiveTerminalManager
|
|
} from './helpers/terminal'
|
|
import {
|
|
attachTerminalImeBoundaryEvidence,
|
|
disposeTerminalImeBoundaryProbe,
|
|
installTerminalImeBoundaryProbe,
|
|
readTerminalImeBoundaryTrace
|
|
} from './terminal-ime-boundary-probe'
|
|
import {
|
|
createTerminalImeByteReader,
|
|
removeTerminalImeByteReader,
|
|
startTerminalImeByteReader,
|
|
waitForTerminalImeBytes
|
|
} from './terminal-ime-byte-reader'
|
|
|
|
const TWO_SET_KOREAN_ID = 'com.apple.inputmethod.Korean.2SetKorean'
|
|
|
|
function typeNativeTwoSetKorean(processId: number, keyCodes: readonly number[]): void {
|
|
execFileSync('osascript', [
|
|
'-e',
|
|
`tell application "System Events" to set frontmost of first application process whose unix id is ${processId} to true`,
|
|
'-e',
|
|
`tell application "System Events" to key code {${keyCodes.join(', ')}}`
|
|
])
|
|
}
|
|
|
|
function typeNativeTwoSetKoreanPreedit(processId: number, keyCodes: readonly number[]): void {
|
|
execFileSync('osascript', [
|
|
'-e',
|
|
`tell application "System Events" to set frontmost of first application process whose unix id is ${processId} to true`,
|
|
'-e',
|
|
'tell application "System Events"',
|
|
'-e',
|
|
`repeat with currentKeyCode in {${keyCodes.join(', ')}}`,
|
|
'-e',
|
|
'key code (currentKeyCode as integer)',
|
|
'-e',
|
|
'delay 0.1',
|
|
'-e',
|
|
'end repeat',
|
|
'-e',
|
|
'end tell'
|
|
])
|
|
}
|
|
|
|
function commitNativeComposition(): void {
|
|
execFileSync('osascript', ['-e', 'tell application "System Events" to key code 36'])
|
|
}
|
|
|
|
async function readActiveComposition(page: Page): Promise<string | null> {
|
|
return page.evaluate(() => {
|
|
const textarea = document.querySelector<HTMLTextAreaElement>('.xterm-helper-textarea:focus')
|
|
const composition = textarea?.parentElement?.querySelector<HTMLElement>(
|
|
'.composition-view.active'
|
|
)
|
|
return composition?.textContent?.replaceAll('\u200e', '') ?? null
|
|
})
|
|
}
|
|
|
|
async function runNativeScenario(
|
|
page: Page,
|
|
testInfo: TestInfo,
|
|
testRepoPath: string,
|
|
processId: number,
|
|
keyCodes: readonly number[],
|
|
expectedText: string,
|
|
preCommit?: { committedText: string; preeditText: string }
|
|
): Promise<void> {
|
|
await waitForSessionReady(page)
|
|
await waitForActiveWorktree(page)
|
|
await ensureTerminalVisible(page)
|
|
await waitForActiveTerminalManager(page, 30_000)
|
|
await expect(page.evaluate(() => window.api.app.getKeyboardInputSourceId())).resolves.toBe(
|
|
TWO_SET_KOREAN_ID
|
|
)
|
|
|
|
const ptyId = await waitForActivePanePtyId(page)
|
|
const reader = createTerminalImeByteReader(testRepoPath, 1)
|
|
let completed = false
|
|
try {
|
|
await startTerminalImeByteReader(page, ptyId, reader)
|
|
await focusActiveTerminalInput(page)
|
|
await installTerminalImeBoundaryProbe(page)
|
|
if (preCommit) {
|
|
typeNativeTwoSetKoreanPreedit(processId, keyCodes)
|
|
await expect.poll(() => readActiveComposition(page)).toBe(preCommit.preeditText)
|
|
await expect
|
|
.poll(async () => (await readTerminalImeBoundaryTrace(page)).onData.join(''))
|
|
.toBe(preCommit.committedText)
|
|
commitNativeComposition()
|
|
} else {
|
|
typeNativeTwoSetKorean(processId, keyCodes)
|
|
}
|
|
|
|
const receivedBytes = await waitForTerminalImeBytes(page, reader)
|
|
expect(receivedBytes).toEqual([Buffer.from(`${expectedText}\n`).toString('hex')])
|
|
const trace = await readTerminalImeBoundaryTrace(page)
|
|
expect(trace.onData.join('')).toBe(`${expectedText}\r`)
|
|
completed = true
|
|
} finally {
|
|
await attachTerminalImeBoundaryEvidence(page, testInfo, 'native-macos-2set-boundaries').catch(
|
|
() => undefined
|
|
)
|
|
await disposeTerminalImeBoundaryProbe(page).catch(() => undefined)
|
|
if (!completed) {
|
|
await sendToTerminal(page, ptyId, '\x03').catch(() => undefined)
|
|
}
|
|
removeTerminalImeByteReader(reader)
|
|
}
|
|
}
|
|
|
|
test.describe('Native macOS 2-Set Korean terminal input @headful', () => {
|
|
test.skip(
|
|
process.platform !== 'darwin' || process.env.ORCA_E2E_NATIVE_MACOS_KOREAN !== '1',
|
|
'Requires macOS with 2-Set Korean selected and Accessibility access'
|
|
)
|
|
|
|
test('forwards physical Hangul input as exact PTY bytes', async ({
|
|
electronApp,
|
|
orcaPage,
|
|
testRepoPath
|
|
}, testInfo) => {
|
|
await runNativeScenario(
|
|
orcaPage,
|
|
testInfo,
|
|
testRepoPath,
|
|
electronApp.process().pid!,
|
|
[5, 40, 1, 15, 46, 3, 36],
|
|
'한글'
|
|
)
|
|
})
|
|
|
|
test('preserves leading vowels and composes the following syllable', async ({
|
|
electronApp,
|
|
orcaPage,
|
|
testRepoPath
|
|
}, testInfo) => {
|
|
await runNativeScenario(
|
|
orcaPage,
|
|
testInfo,
|
|
testRepoPath,
|
|
electronApp.process().pid!,
|
|
[31, 40, 0, 16, 36],
|
|
'ㅐㅏ묘'
|
|
)
|
|
})
|
|
|
|
test('flushes each syllable while the next remains in preedit', async ({
|
|
electronApp,
|
|
orcaPage,
|
|
testRepoPath
|
|
}, testInfo) => {
|
|
await runNativeScenario(
|
|
orcaPage,
|
|
testInfo,
|
|
testRepoPath,
|
|
electronApp.process().pid!,
|
|
[15, 40, 1, 40, 14, 40],
|
|
'가나다',
|
|
{ committedText: '가나', preeditText: '다' }
|
|
)
|
|
})
|
|
})
|