yukey

testkit README: a complete run, and two contract notes

commit 945c8644e5b48bbae406e1aefadcc1d6935fc59a · parent 9993779312 · browse this tree
Ximianth, 2026-10-10 05:58:06 UTC

The README said what each module exports but not the shape of a run:
how to find the page target's webSocketDebuggerUrl, that connect wants a
page target, that Checks doesn't exit for you.  The snippet was run as-is
against the built dist/ from a scratch consumer, 10/10 clean (after two
rounds: wait for Chrome's exit before rm'ing the profile, and wait for
autofocus before typing).

Also states that key names are KeyboardEvent.key strings and that js()
rejects on a page exception.
diff --git a/packages/yukey-testkit/README.md b/packages/yukey-testkit/README.md
index a97f3e7..21c5212 100644
--- a/packages/yukey-testkit/README.md
+++ b/packages/yukey-testkit/README.md
@@ -20,13 +20,64 @@ 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.
-- `yukey-testkit/checks`: the `Checks` collector (a PASS/FAIL line protocol with a summary),
+  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.
-Yukey's own `packages/e2e/harness.ts` is one worked example (static files on an ephemeral port, a headless Chrome with a throwaway profile).
+
+
+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,<input id=i autofocus>",
+], { 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