yukey-testkit ============= A dependency-free CDP client for driving a page with *trusted* input, and reading its state between steps. It exists because synthetic DOM events can't drive real focus traversal: a `KeyboardEvent` dispatched from JavaScript won't move focus on Tab, won't type into an input, and won't trigger hover. Trusted input (`Input.dispatchKeyEvent` and friends over the Chrome DevTools Protocol) is what a real keyboard and mouse produce, so a test built on it exercises the same paths a user does. That's the only kind of test that can prove a keyboard-first UI actually works from the keyboard. Node 22+, using its built-in `WebSocket`. Nothing else. What's in it ------------ - `yukey-testkit/cdp`: the `Cdp` class. `Cdp.connect(wsUrl)` to a page target; then `key`, `click`, `mouseMove`, `wheel` for trusted input, `js(expr)` for state reads, `screenshot()`, and `cmd` for any other protocol method. Key names are DOM `KeyboardEvent.key` strings (`"Tab"`, `"ArrowDown"`, `" "`, `"a"`); an unknown name is an error, not typed text. `js` rejects if the expression throws in the page, with the exception text. - `yukey-testkit/checks`: the `Checks` collector (a PASS/FAIL line protocol with counts), the `Scenario` and `ScenarioContext` shapes a suite runner can share, and `shot` for optional screenshot evidence (written only when `YUKEY_E2E_SHOTS_DIR` is set). - `yukey-testkit/sleep`: a promise-returning sleep for polling loops. Launching Chrome and serving your page are yours to do; they vary too much per app to belong here. A complete run -------------- Launch a Chrome with `--remote-debugging-port=0`, find the page target, connect, drive, check, tear down. `Checks` only prints and counts; exiting non-zero is up to you. ```js import { spawn } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Cdp } from "yukey-testkit/cdp"; import { Checks } from "yukey-testkit/checks"; import { sleep } from "yukey-testkit/sleep"; const profile = mkdtempSync(join(tmpdir(), "chrome-")); const chrome = spawn("/path/to/chrome", [ "--headless", "--remote-debugging-port=0", `--user-data-dir=${profile}`, "data:text/html,", ], { stdio: "ignore" }); // Chrome writes its chosen port to the profile dir; /json on it lists the targets. let port = 0; while (port === 0) { await sleep(50); try { port = Number(readFileSync(join(profile, "DevToolsActivePort"), "utf8").split("\n")[0]); } catch {} } const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json(); const page = targets.find((t) => t.type === "page"); const cdp = await Cdp.connect(page.webSocketDebuggerUrl); while ((await cdp.js("document.activeElement?.id === 'i'")) !== true) await sleep(50); const checks = new Checks(); await cdp.key("h"); await cdp.key("i"); checks.check("typed into the input", (await cdp.js("document.getElementById('i').value")) === "hi"); console.log(`${checks.passed}/${checks.total} passed`); cdp.close(); chrome.kill(); await new Promise((r) => chrome.once("exit", r)); rmSync(profile, { recursive: true, force: true }); process.exit(checks.failed > 0 ? 1 : 0); ``` Yukey's own `packages/e2e/harness.ts` is the same shape with the edges handled: static files on an ephemeral port, a throwaway profile, signal handlers, and a Chrome that dies failing the run. License ------- SPDX-License-Identifier: Apache-2.0 OR MIT