Files
orca/tests/e2e/terminal-ime-preedit-overlay-probe.ts
NeilandOrca 41ba29d79a test(e2e): headless preedit-geometry coverage for Korean and CJK terminal input (#14500)
* test(e2e): headless preedit-geometry coverage for Korean and CJK terminal input

Both IME defects that shipped and were reverted passed a suite of ~3000 IME
assertions, because every one of them checked bytes reaching the pty and a
preedit rendered into a hidden overlay satisfies all of them while the user
composes blind. The one arm that asserted real geometry was headful-gated and
macOS-only, so it never ran in CI.

Drives composition through CDP Input.imeSetComposition rather than a native
input source, which removes the accessibility grant, the system input source
and the visible window that forced that gate, so this runs in the ordinary
electron-headless project.

The load-bearing assertion is the composition overlay's real bounding rect.
Verified to have teeth: with max-width 0 and overflow hidden injected, the
active class, the textContent, display block and checkVisibility all still
pass, and only the rect assertion fails.

* test(e2e): restore the CDP composition drivers the preedit specs need

The trimmed copy on main kept only the key-dispatch helpers, so the composition
drivers the geometry specs import were missing. Adds them back: setImeComposition,
commitImeText, dispatchImeProcessKey, composeHangulSyllable and
dispatchResumedCompositionUpdate. The shared helpers are unchanged.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-14 01:47:03 -07:00

103 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
/**
* Samples the xterm preedit overlay's real geometry.
*
* Why geometry and not the `active` class: an overlay forced to
* `max-width: 0; overflow: hidden` is invisible on screen, yet keeps its class, its
* `textContent`, `display: block` and `checkVisibility() === true`. The bounding rect is the
* only property that discriminates, and it is also the only one a DOM emulator cannot produce —
* happy-dom reports all-zero rects in every state, so a unit-level arm passes over an overlay
* that never renders.
*/
export type PreeditOverlaySample = {
found: boolean
active: boolean
text: string
rect: { width: number; height: number }
checkVisibility: boolean | null
display: string
visibility: string
opacity: string
maxWidth: string
overflow: string
}
export function readPreeditOverlay(): PreeditOverlaySample {
const textarea =
document.querySelector<HTMLTextAreaElement>('.xterm-helper-textarea:focus') ??
document.querySelector<HTMLTextAreaElement>('.xterm-helper-textarea')
const view = textarea?.parentElement?.querySelector<HTMLElement>('.composition-view') ?? null
if (!view) {
return {
found: false,
active: false,
text: '',
rect: { width: 0, height: 0 },
checkVisibility: null,
display: '',
visibility: '',
opacity: '',
maxWidth: '',
overflow: ''
}
}
const style = getComputedStyle(view)
const rect = view.getBoundingClientRect()
return {
found: true,
active: view.classList.contains('active'),
// The overlay wraps its text in LRM marks; strip them so assertions read as the user sees it.
text: (view.textContent ?? '').replaceAll('‎', ''),
rect: { width: rect.width, height: rect.height },
checkVisibility: typeof view.checkVisibility === 'function' ? view.checkVisibility() : null,
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
maxWidth: style.maxWidth,
overflow: style.overflow
}
}
export async function samplePreeditOverlay(page: Page): Promise<PreeditOverlaySample> {
return page.evaluate(readPreeditOverlay)
}
export async function expectPreeditRendered(
page: Page,
expectedText: string,
message: string
): Promise<PreeditOverlaySample> {
await expect
.poll(async () => (await samplePreeditOverlay(page)).text, { message })
.toBe(expectedText)
const sample = await samplePreeditOverlay(page)
assertPreeditRendered(sample, expectedText, message)
return sample
}
export function assertPreeditRendered(
sample: PreeditOverlaySample,
expectedText: string,
message: string
): void {
expect(sample.found, `${message}: no composition overlay exists`).toBe(true)
expect(sample.text, `${message}: overlay text`).toBe(expectedText)
expect(sample.active, `${message}: overlay is not marked active`).toBe(true)
expect(sample.display, `${message}: overlay is display:none`).not.toBe('none')
expect(sample.visibility, `${message}: overlay is not visible`).toBe('visible')
expect(sample.checkVisibility, `${message}: overlay fails checkVisibility`).not.toBe(false)
// The load-bearing pair. Do not weaken these to a class or visibility check.
expect(sample.rect.width, `${message}: overlay has zero width`).toBeGreaterThan(0)
expect(sample.rect.height, `${message}: overlay has zero height`).toBeGreaterThan(0)
expect(sample.maxWidth, `${message}: overlay is clipped to zero width`).not.toBe('0px')
}
export async function expectPreeditHidden(page: Page, message: string): Promise<void> {
await expect.poll(async () => (await samplePreeditOverlay(page)).active, { message }).toBe(false)
const sample = await samplePreeditOverlay(page)
expect(sample.rect.width, `${message}: overlay still occupies width`).toBe(0)
expect(sample.rect.height, `${message}: overlay still occupies height`).toBe(0)
}