mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test(ime): stop the trace harness discarding fields it records
The schema could not express repeat or selectionDirection, and the replayer coerced an absent isComposing to false — the one distinction the field is typed `boolean | undefined` to capture. repeat is what a press-and-hold accent panel is recognised by, so a trace recorded for that quirk could not reproduce it. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -20,6 +20,8 @@ export type ImeTraceTargetState = {
|
||||
value: string
|
||||
selectionStart: number
|
||||
selectionEnd: number
|
||||
/** Absent means 'none'. Without it a preedit range can only ever be a caret. */
|
||||
selectionDirection?: 'backward' | 'forward' | 'none'
|
||||
}
|
||||
|
||||
type ImeTraceEventBase = { state: ImeTraceTargetState }
|
||||
@@ -30,6 +32,8 @@ export type ImeTraceKeyEvent = ImeTraceEventBase & {
|
||||
code: string
|
||||
keyCode: number
|
||||
isComposing: boolean
|
||||
/** macOS press-and-hold drives its accent panel off this; absent means false. */
|
||||
repeat?: boolean
|
||||
altKey?: boolean
|
||||
ctrlKey?: boolean
|
||||
metaKey?: boolean
|
||||
@@ -88,13 +92,12 @@ function applyLegacyKeyCode(event: KeyboardEvent, keyCode: number): void {
|
||||
}
|
||||
|
||||
function applyIsComposing(event: Event, isComposing: boolean | undefined): void {
|
||||
if (
|
||||
isComposing === undefined ||
|
||||
(event as { isComposing?: boolean }).isComposing === isComposing
|
||||
) {
|
||||
if ((event as { isComposing?: boolean }).isComposing === isComposing) {
|
||||
return
|
||||
}
|
||||
// Testing libraries cannot set isComposing any other way.
|
||||
// Testing libraries cannot set isComposing any other way. `undefined` is a real
|
||||
// recorded value, not a missing one: the constructor coerces it to false, which
|
||||
// is the one thing a Safari trace exists to tell apart.
|
||||
Object.defineProperty(event, 'isComposing', { configurable: true, value: isComposing })
|
||||
}
|
||||
|
||||
@@ -109,6 +112,7 @@ export function createImeTraceKeyboardEvent(event: ImeTraceKeyEvent): KeyboardEv
|
||||
key: event.key,
|
||||
keyCode: event.keyCode,
|
||||
metaKey: event.metaKey ?? false,
|
||||
repeat: event.repeat ?? false,
|
||||
shiftKey: event.shiftKey ?? false
|
||||
})
|
||||
applyLegacyKeyCode(created, event.keyCode)
|
||||
@@ -170,6 +174,7 @@ export type ImeTraceReplayOptions = {
|
||||
|
||||
function readTargetState(target: HTMLTextAreaElement | HTMLInputElement): ImeTraceTargetState {
|
||||
return {
|
||||
selectionDirection: target.selectionDirection ?? 'none',
|
||||
selectionEnd: target.selectionEnd ?? 0,
|
||||
selectionStart: target.selectionStart ?? 0,
|
||||
value: target.value
|
||||
@@ -181,7 +186,7 @@ function stampTargetState(
|
||||
state: ImeTraceTargetState
|
||||
): void {
|
||||
target.value = state.value
|
||||
target.setSelectionRange(state.selectionStart, state.selectionEnd)
|
||||
target.setSelectionRange(state.selectionStart, state.selectionEnd, state.selectionDirection)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createImeTraceInputEvent,
|
||||
createImeTraceKeyboardEvent,
|
||||
createImeTraceTextarea,
|
||||
EMPTY_IME_TRACE_STATE,
|
||||
extractImeCommitsFromTrace,
|
||||
interpretImeCommits,
|
||||
replayImeCompositionTrace
|
||||
@@ -15,6 +18,15 @@ import {
|
||||
IME_COMPOSITION_TRACES
|
||||
} from './ime-recorded-composition-traces.test-fixtures'
|
||||
|
||||
const KEY_EVENT_WITHOUT_REPEAT: ImeTraceKeyEvent = {
|
||||
code: 'KeyE',
|
||||
isComposing: false,
|
||||
key: 'e',
|
||||
keyCode: 69,
|
||||
state: EMPTY_IME_TRACE_STATE,
|
||||
type: 'keydown'
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
@@ -191,4 +203,68 @@ describe('replayImeCompositionTrace', () => {
|
||||
// Process keys are composing; the interleaved 'a' and the trailing Enter are not.
|
||||
expect(composing).toEqual([true, false, true, false])
|
||||
})
|
||||
|
||||
it("replays Safari's undefined isComposing as undefined rather than false", () => {
|
||||
// The whole reason the field is `boolean | undefined`: code that branches on
|
||||
// `=== undefined` must see the two apart, and the constructor coerces.
|
||||
const safari = createImeTraceInputEvent({
|
||||
data: 'a',
|
||||
inputType: 'insertText',
|
||||
isComposing: undefined,
|
||||
state: EMPTY_IME_TRACE_STATE,
|
||||
type: 'input'
|
||||
})
|
||||
const chromium = createImeTraceInputEvent({
|
||||
data: 'a',
|
||||
inputType: 'insertText',
|
||||
isComposing: false,
|
||||
state: EMPTY_IME_TRACE_STATE,
|
||||
type: 'input'
|
||||
})
|
||||
|
||||
expect(safari.isComposing).toBeUndefined()
|
||||
expect(chromium.isComposing).toBe(false)
|
||||
})
|
||||
|
||||
it('replays the repeat flag macOS press-and-hold is distinguished by', () => {
|
||||
const held = createImeTraceKeyboardEvent({
|
||||
code: 'KeyE',
|
||||
isComposing: false,
|
||||
key: 'e',
|
||||
keyCode: 69,
|
||||
repeat: true,
|
||||
state: EMPTY_IME_TRACE_STATE,
|
||||
type: 'keydown'
|
||||
})
|
||||
|
||||
expect(held.repeat).toBe(true)
|
||||
expect(createImeTraceKeyboardEvent(KEY_EVENT_WITHOUT_REPEAT).repeat).toBe(false)
|
||||
})
|
||||
|
||||
it('replays a backward selection so a preedit range is not flattened to a caret', async () => {
|
||||
const textarea = createImeTraceTextarea(document)
|
||||
const directions: (string | null)[] = []
|
||||
textarea.addEventListener('keydown', () => directions.push(textarea.selectionDirection))
|
||||
|
||||
await replayImeCompositionTrace(textarea, {
|
||||
...IBUS_HANGUL_RETAINED_COMMIT_TRACE,
|
||||
events: [
|
||||
{
|
||||
code: 'KeyA',
|
||||
isComposing: false,
|
||||
key: 'a',
|
||||
keyCode: 65,
|
||||
state: {
|
||||
selectionDirection: 'backward',
|
||||
selectionEnd: 2,
|
||||
selectionStart: 0,
|
||||
value: 'ab'
|
||||
},
|
||||
type: 'keydown'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(directions).toEqual(['backward'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user