test(ime): record the fields the trace schema was widened to hold

The recorder captured isComposing faithfully and then lost it at the POST:
JSON.stringify omits an undefined value, so the written trace had no such
key at all. null carries it instead.

Adds repeat and selectionDirection, and a contract test so a field the
recorder learns about cannot again go somewhere the schema cannot hold it.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-01 21:47:24 -07:00
co-authored by Orca
parent f4b3545025
commit eeba5e6d2d
2 changed files with 63 additions and 3 deletions
+8 -3
View File
@@ -100,6 +100,7 @@ let initial = snapshot()
function snapshot() {
return {
selectionDirection: target.selectionDirection ?? 'none',
selectionEnd: target.selectionEnd ?? 0,
selectionStart: target.selectionStart ?? 0,
value: target.value
@@ -114,20 +115,24 @@ function record(event) {
} else if (event instanceof InputEvent) {
entry.data = event.data ?? null
entry.inputType = event.inputType
// Safari reports undefined rather than false and the distinction is load-bearing,
// so it is preserved instead of coerced.
entry.isComposing = event.isComposing
// Safari reports undefined rather than false and the distinction is load-bearing.
// JSON.stringify drops an undefined value outright, so null carries it instead;
// transcribe it back to undefined in the fixture.
entry.isComposing = event.isComposing ?? null
} else if (event instanceof KeyboardEvent) {
entry.code = event.code
entry.isComposing = event.isComposing
entry.key = event.key
entry.keyCode = event.keyCode
// The accent panel a macOS press-and-hold opens is only recognisable by this.
entry.repeat = event.repeat
}
events.push(entry)
countEl.textContent = String(events.length)
const detail = [
entry.key ? 'key=' + entry.key : '',
entry.keyCode === undefined ? '' : 'keyCode=' + entry.keyCode,
entry.repeat ? 'repeat' : '',
entry.inputType ? entry.inputType : '',
entry.data === undefined ? '' : 'data=' + JSON.stringify(entry.data),
'isComposing=' + entry.isComposing,
+55
View File
@@ -0,0 +1,55 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
// The recorder writes traces that the fixture schema has to be able to hold. The two
// live in different languages and different build graphs, so nothing but this checks
// that a field one of them learns about ever reaches the other.
const scriptsDir = import.meta.dirname
const projectDir = path.resolve(scriptsDir, '../..')
const recorderSource = readFileSync(path.join(scriptsDir, 'record-ime-trace.mjs'), 'utf8')
const schemaSource = readFileSync(
path.join(projectDir, 'src/renderer/src/lib/ime-composition-trace.test-fixtures.ts'),
'utf8'
)
function declaredSchemaFields(typeName) {
const body = schemaSource.match(new RegExp(`export type ${typeName} = [^{]*\\{([^}]*)\\}`))?.[1]
if (!body) {
throw new Error(`${typeName} not found in the fixture schema`)
}
return new Set(Array.from(body.matchAll(/^\s{2}(\w+)\??:/gm), (match) => match[1]))
}
describe('record-ime-trace', () => {
it('captures nothing the trace schema cannot express', () => {
const captured = new Set(Array.from(recorderSource.matchAll(/entry\.(\w+) =/g), (m) => m[1]))
const expressible = new Set([
...declaredSchemaFields('ImeTraceKeyEvent'),
...declaredSchemaFields('ImeTraceInputEvent'),
...declaredSchemaFields('ImeTraceCompositionEvent')
])
expect(captured.size).toBeGreaterThan(0)
expect([...captured].filter((field) => !expressible.has(field))).toEqual([])
})
it('records the fields the platform quirks are recognised by', () => {
// repeat distinguishes a held key from the first press, which is the only signal
// a macOS press-and-hold accent panel produces; selectionDirection is the only way
// a backward preedit range survives as anything but a caret.
expect(recorderSource).toMatch(/entry\.repeat = event\.repeat/)
expect(recorderSource).toMatch(/selectionDirection: target\.selectionDirection/)
expect(declaredSchemaFields('ImeTraceKeyEvent')).toContain('repeat')
expect(declaredSchemaFields('ImeTraceTargetState')).toContain('selectionDirection')
})
it('carries an undefined isComposing as null rather than dropping the key', () => {
// JSON.stringify omits undefined-valued keys outright, so coercing here would
// erase the one bit a Safari trace is recorded to preserve.
expect(recorderSource).toMatch(/entry\.isComposing = event\.isComposing \?\? null/)
expect(JSON.stringify({ isComposing: undefined })).toBe('{}')
})
})