mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(e2e): restore the helpers the macOS key-remap spec imports (#13758)
#13314 landed terminal-macos-system-key-remap.spec.ts but its final commit, "chore: drop non-mergeable IME e2e scratch files", deleted the three modules it imports. The spec survived; terminal-ime-pane-arena, terminal-ime-cdp-composition and terminal-ime-platform-policy did not. Playwright resolves every spec before running any of them, so the unresolved import is fatal for the whole run, not just that file: `playwright test --list` on main reports "Total: 0 tests in 0 files". All ten scheduled E2E shards have been failing at collection since, and the macOS system-key-remap coverage the PR was for has never run once. No typecheck project includes tests/, which is why this stayed invisible outside the E2E lane. Restored from the commit before the drop. pane-arena and platform-policy come back verbatim; cdp-composition keeps only the four entry points this spec uses, since the rest served specs that were not merged. Its doc comment is retargeted accordingly — the composition-session drivers are the part that went. Collection is back to 528 tests in 222 files and all six remap tests pass.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import type { CDPSession } from '@stablyai/playwright-test'
|
||||
|
||||
/**
|
||||
* Dispatches the key shapes an input source or a system text substitution produces, through CDP.
|
||||
*
|
||||
* Driving these from the browser protocol rather than a native input source is what removes the
|
||||
* accessibility grant and the system input source that would otherwise force a `@headful`,
|
||||
* macOS-only gate, so the specs they feed run in the normal headless project on CI.
|
||||
*/
|
||||
export type ImeKeyIdentity = {
|
||||
key: string
|
||||
code: string
|
||||
keyCode: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A printable keydown whose `key` the input source has already rewritten to the glyph it will
|
||||
* commit — the shape a CJK source produces for punctuation and full-width digits, which arrive
|
||||
* with no composition session at all.
|
||||
*/
|
||||
export async function dispatchImeRewrittenPrintableKey(
|
||||
session: CDPSession,
|
||||
identity: ImeKeyIdentity
|
||||
): Promise<void> {
|
||||
await session.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
key: identity.key,
|
||||
code: identity.code,
|
||||
windowsVirtualKeyCode: identity.keyCode,
|
||||
nativeVirtualKeyCode: identity.keyCode,
|
||||
text: identity.key,
|
||||
unmodifiedText: identity.key
|
||||
})
|
||||
await session.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
key: identity.key,
|
||||
code: identity.code,
|
||||
windowsVirtualKeyCode: identity.keyCode,
|
||||
nativeVirtualKeyCode: identity.keyCode
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A printable keydown that still carries the **physical layout key**, followed by the substituted
|
||||
* glyph arriving through the text system.
|
||||
*
|
||||
* This is the macOS shape for full-width punctuation and digits, and for a
|
||||
* `DefaultKeyBinding.dict` remap: the substitution happens inside `insertText:`, not on the
|
||||
* keydown, so the keydown Chromium delivers is the plain layout character and the substituted one
|
||||
* only ever appears in the `input` event. Anything that produces terminal bytes from the keydown
|
||||
* emits the layout form and destroys the real one.
|
||||
*/
|
||||
export async function dispatchImeSubstitutedTextKey(
|
||||
session: CDPSession,
|
||||
identity: ImeKeyIdentity,
|
||||
committedText: string
|
||||
): Promise<void> {
|
||||
// rawKeyDown carries no `text`, so Chromium generates no character of its own and the only
|
||||
// committed text is the one the text system supplies below.
|
||||
await session.send('Input.dispatchKeyEvent', {
|
||||
type: 'rawKeyDown',
|
||||
key: identity.key,
|
||||
code: identity.code,
|
||||
windowsVirtualKeyCode: identity.keyCode,
|
||||
nativeVirtualKeyCode: identity.keyCode,
|
||||
text: '',
|
||||
unmodifiedText: ''
|
||||
})
|
||||
await session.send('Input.insertText', { text: committedText })
|
||||
await session.send('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
key: identity.key,
|
||||
code: identity.code,
|
||||
windowsVirtualKeyCode: identity.keyCode,
|
||||
nativeVirtualKeyCode: identity.keyCode
|
||||
})
|
||||
}
|
||||
|
||||
export async function dispatchPlainEnter(session: CDPSession): Promise<void> {
|
||||
for (const type of ['rawKeyDown', 'keyUp'] as const) {
|
||||
await session.send('Input.dispatchKeyEvent', {
|
||||
type,
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
windowsVirtualKeyCode: 13,
|
||||
nativeVirtualKeyCode: 13
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { CDPSession, Page, TestInfo } from '@stablyai/playwright-test'
|
||||
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
import {
|
||||
focusActiveTerminalInput,
|
||||
sendToTerminal,
|
||||
waitForActivePanePtyId,
|
||||
waitForActiveTerminalManager
|
||||
} from './helpers/terminal'
|
||||
import {
|
||||
attachTerminalImeBoundaryEvidence,
|
||||
disposeTerminalImeBoundaryProbe,
|
||||
installTerminalImeBoundaryProbe
|
||||
} from './terminal-ime-boundary-probe'
|
||||
|
||||
/** A focused terminal pane with a CDP session and the boundary probe already attached. */
|
||||
export type TerminalImePaneArena = {
|
||||
page: Page
|
||||
session: CDPSession
|
||||
ptyId: string
|
||||
}
|
||||
|
||||
export async function openTerminalImePaneArena(page: Page): Promise<TerminalImePaneArena> {
|
||||
await waitForSessionReady(page)
|
||||
await waitForActiveWorktree(page)
|
||||
await ensureTerminalVisible(page)
|
||||
await waitForActiveTerminalManager(page, 30_000)
|
||||
const ptyId = await waitForActivePanePtyId(page)
|
||||
const session = await page.context().newCDPSession(page)
|
||||
await focusActiveTerminalInput(page)
|
||||
await installTerminalImeBoundaryProbe(page)
|
||||
return { page, session, ptyId }
|
||||
}
|
||||
|
||||
/**
|
||||
* `interrupt` sends Ctrl+C only when the test did not reach its end, so a spec that failed
|
||||
* mid-composition cannot leave a byte reader holding the pane for the next test in the worker.
|
||||
*/
|
||||
export async function closeTerminalImePaneArena(
|
||||
arena: TerminalImePaneArena,
|
||||
testInfo: TestInfo,
|
||||
evidenceName: string,
|
||||
interrupt: boolean
|
||||
): Promise<void> {
|
||||
await attachTerminalImeBoundaryEvidence(arena.page, testInfo, evidenceName).catch(() => undefined)
|
||||
await disposeTerminalImeBoundaryProbe(arena.page).catch(() => undefined)
|
||||
await arena.session.detach().catch(() => undefined)
|
||||
if (interrupt) {
|
||||
await sendToTerminal(arena.page, arena.ptyId, '\x03').catch(() => undefined)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Page } from '@stablyai/playwright-test'
|
||||
|
||||
/**
|
||||
* Pins the renderer's IME ownership policy to one platform, from any runner.
|
||||
*
|
||||
* Every platform-dependent IME decision in the terminal reads `navigator.userAgent` and nothing
|
||||
* else — the forwarder that owns printable keydowns installs only when the UA reports macOS, the
|
||||
* candidate-key guards only when it reports desktop Linux, and the standalone `keyCode 229`
|
||||
* keydown reaches xterm on macOS and Linux but not on Windows. Overriding the UA is therefore the
|
||||
* whole platform decision, which is what lets the Windows and Linux shapes run headless on the
|
||||
* Linux CI shards instead of needing three runners.
|
||||
*
|
||||
* What it does NOT simulate is the input framework itself. That comes from the recorded traces
|
||||
* replayed against the pinned policy, because the frameworks disagree on ordering in ways no
|
||||
* hand-authored sequence would have predicted.
|
||||
*/
|
||||
export type ImePlatformPolicy = 'mac' | 'windows' | 'linux'
|
||||
|
||||
const USER_AGENT_BY_POLICY: Record<ImePlatformPolicy, string> = {
|
||||
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/146 Safari/537.36',
|
||||
windows: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/146 Safari/537.36',
|
||||
// X11 rather than Wayland in the UA string: Chromium reports the same token for both, so the
|
||||
// Wayland traces run under this policy too.
|
||||
linux: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/146 Safari/537.36'
|
||||
}
|
||||
|
||||
export async function applyImePlatformPolicy(page: Page, policy: ImePlatformPolicy): Promise<void> {
|
||||
await page.addInitScript((userAgent) => {
|
||||
Object.defineProperty(navigator, 'userAgent', {
|
||||
get: () => userAgent,
|
||||
configurable: true
|
||||
})
|
||||
}, USER_AGENT_BY_POLICY[policy])
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await page.waitForFunction(() => Boolean(window.__store), null, { timeout: 30_000 })
|
||||
}
|
||||
|
||||
/** Fails loudly if the override did not take, so a policy-scoped spec cannot pass on the wrong one. */
|
||||
export async function expectImePlatformPolicy(
|
||||
page: Page,
|
||||
policy: ImePlatformPolicy
|
||||
): Promise<void> {
|
||||
const observed = await page.evaluate(() => ({
|
||||
mac: navigator.userAgent.includes('Mac'),
|
||||
windows: navigator.userAgent.includes('Windows'),
|
||||
linux: navigator.userAgent.includes('Linux') && !/Android|CrOS/.test(navigator.userAgent)
|
||||
}))
|
||||
if (!observed[policy] || (policy !== 'mac' && observed.mac)) {
|
||||
throw new Error(`IME platform policy '${policy}' did not take: ${JSON.stringify(observed)}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user