diff --git a/config/scripts/record-ime-trace.mjs b/config/scripts/record-ime-trace.mjs new file mode 100644 index 00000000000..32a6fec66f7 --- /dev/null +++ b/config/scripts/record-ime-trace.mjs @@ -0,0 +1,261 @@ +// Captures a real IME composition trace from your own machine and your own input +// method, in the exact JSON shape src/renderer/src/lib/*.test-fixtures.ts consumes. +// +// Why this exists: CDP-dispatched keystrokes bypass the OS input-method layer, so no +// amount of page.keyboard.type() produces a real composition. The Linux CI matrix +// drives ibus through X11 and covers hangul/libpinyin/anthy/unikey, but there is no +// macOS or Windows IME CI. On those platforms a human typing into this page is the +// only way to get a trace that is honestly labelled `recorded`. +// +// pnpm ime:record +// +// Then open the printed URL, switch to the input method you want to characterize, +// type, and press Save. Everything runs on localhost and nothing is uploaded. + +import { createServer } from 'node:http' +import { mkdirSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +const projectDir = path.resolve(import.meta.dirname, '../..') +const outputDir = path.join(projectDir, 'test-results', 'ime-recordings') +const host = '127.0.0.1' +const port = Number(process.env.ORCA_IME_RECORDER_PORT ?? 7331) + +// Kept identical to tests/e2e/terminal-ime-boundary-probe.ts so traces captured here +// and traces captured in CI deserialize through the same loader. +const RECORDED_EVENT_TYPES = [ + 'compositionstart', + 'compositionupdate', + 'compositionend', + 'beforeinput', + 'input', + 'keydown', + 'keypress', + 'keyup' +] + +const PAGE = /* html */ ` + + + +Orca IME trace recorder + + + +

Orca IME trace recorder

+

+ Switch to the input method you want to characterize, then compose in the box below. + Every composition, input and key event is captured together with the textarea's + value and selection offsets at that exact instant — the offsets are what make a + trace recorded rather than reconstructed. +

+

+ Type the sequence you care about, including the committing key and any candidate + navigation. Then name it after the behavior it demonstrates and press Save. +

+ + + +
+ + +
+
+ + + 0 events + +
+ +
+ + + + +` + +function inferEnvironment(userAgent) { + const platform = userAgent.includes('Mac') + ? 'darwin' + : userAgent.includes('Windows') + ? 'win32' + : 'linux' + // Electron reports a Chrome token too, so WebKit and Gecko are checked first. + const browser = userAgent.includes('Firefox') + ? 'gecko' + : userAgent.includes('Chrome') + ? 'chromium' + : 'webkit' + return { browser, platform } +} + +function toTraceFile(payload) { + const { browser, platform } = inferEnvironment(payload.userAgent ?? '') + return { + committed: payload.committed ?? '', + env: { + browser, + // Filled in by hand: the page cannot see which input method produced the events. + engine: 'TODO: name the input method, e.g. macOS Pinyin - Simplified', + platform + }, + events: payload.events, + final: payload.final, + initial: payload.initial, + name: payload.name, + origin: + `Recorded by config/scripts/record-ime-trace.mjs on ${platform}. ` + + `User agent: ${payload.userAgent ?? 'unknown'}. ` + + 'TODO: describe what the human typed and the issue this belongs to.', + provenance: 'recorded' + } +} + +function readBody(request) { + return new Promise((resolve, reject) => { + const chunks = [] + request.on('data', (chunk) => chunks.push(chunk)) + request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + request.on('error', reject) + }) +} + +const server = createServer(async (request, response) => { + if (request.method === 'GET' && (request.url === '/' || request.url === '/index.html')) { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + response.end(PAGE) + return + } + if (request.method === 'POST' && request.url === '/trace') { + try { + const payload = JSON.parse(await readBody(request)) + const safeName = payload.name.replaceAll(/[^a-z0-9]+/gi, '-').toLowerCase() + mkdirSync(outputDir, { recursive: true }) + const filePath = path.join(outputDir, `${safeName}.json`) + writeFileSync(filePath, `${JSON.stringify(toTraceFile(payload), null, 2)}\n`) + const relative = path.relative(projectDir, filePath) + console.log(`[ime-recorder] wrote ${relative} (${payload.events.length} events)`) + response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }) + response.end(relative) + } catch (error) { + console.error('[ime-recorder] failed to save trace:', error) + response.writeHead(500) + response.end('save failed') + } + return + } + response.writeHead(404) + response.end() +}) + +server.listen(port, host, () => { + console.log(`[ime-recorder] open http://${host}:${port}/`) + console.log(`[ime-recorder] traces are written to ${path.relative(projectDir, outputDir)}`) + console.log('[ime-recorder] press Ctrl+C when finished') +}) diff --git a/package.json b/package.json index 2a2708c44d7..da00d8f2a68 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "win-crash-survival-e2e": "node tests/tools/win-crash-survival-e2e/run.mjs", "test:e2e:ssh-codex-artifacts-repro": "node config/scripts/run-ssh-codex-artifacts-repro-e2e.mjs", "test:e2e:headful": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project electron-headful", + "ime:record": "node config/scripts/record-ime-trace.mjs", "test:e2e:terminal-ime-native": "node config/scripts/run-terminal-ibus-engine-e2e.mjs", "test:e2e:computer": "vitest run --config tests/e2e/vitest.config.ts", "bench:idle-cpu": "pnpm run ensure:electron-runtime && node config/scripts/run-idle-cpu-benchmark.mjs",