// Live browser check: read → click → read, type → read, staleness, tabs. Needs Chrome/Edge
// installed (or ADA_BROWSER set). Run with: npm run check:browser
import assert from "node:assert/strict";
import { createServer } from "node:http";
import { browserAction, tabAction } from "./client/browser.ts";
const page = `
check
covered
`;
async function main(): Promise {
const srv = createServer((_, res) => {
res.setHeader("content-type", "text/html");
res.end(page);
});
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
const url = `http://127.0.0.1:${(srv.address() as { port: number }).port}/`;
try {
await browserAction("open", { url });
let r = await browserAction("read", {});
const btn = /button "Do thing" \[(ref_\d+)\]/.exec(r.text)?.[1];
assert.ok(btn, `no button ref in tree:\n${r.text}`);
// click mutates the DOM the way the click implies
await browserAction("click", { ref: btn });
r = await browserAction("read", {});
assert.ok(r.text.includes("clicked"), `click did not land:\n${r.text}`);
// type lands in the input's value
const input = /textbox "Name"[^[]*\[(ref_\d+)\]/.exec(r.text)?.[1];
assert.ok(input, `no input ref in tree:\n${r.text}`);
await browserAction("type", { ref: input, text: "hello" });
r = await browserAction("read", {});
assert.ok(r.text.includes("hello"), `typed text did not land:\n${r.text}`);
// acting after navigation fails with a read-again error
await browserAction("open", { url });
const stale = await browserAction("click", { ref: input }).then(
() => "",
(e) => String(e),
);
assert.ok(/`read`/.test(stale), `stale ref should demand a read, got: ${stale || "(no error)"}`);
// coordinate click + character keys reach the page as real input events
await browserAction("open", { url });
await browserAction("click", { x: 200, y: 150 });
r = await browserAction("read", {});
assert.ok(r.text.includes("200,150"), `coordinate click did not land:\n${r.text}`);
await browserAction("press", { key: "w" });
r = await browserAction("read", {});
assert.ok(r.text.includes("key=w"), `character key did not land:\n${r.text}`);
await browserAction("press", { key: "space", hold: 300 }); // held key must not throw
const shot = await browserAction("screenshot", {});
assert.ok(shot.screenshot && shot.screenshot.length > 1024, "screenshot too small to be a real PNG");
// targeting without a `read` first: CSS selector and visible text both reach the element
await browserAction("open", { url });
await browserAction("click", { find: "Do thing" });
let body = await browserAction("eval", { expression: "document.getElementById('out').textContent" });
assert.ok(body.text.includes("clicked"), `find-by-text click did not land:\n${body.text}`);
await browserAction("type", { selector: "#email", text: "a@b.c" });
body = await browserAction("eval", { expression: "document.getElementById('email').value" });
assert.ok(body.text.includes("a@b.c"), `selector type did not land:\n${body.text}`);
// fill writes through the native setter (what React listens to), select matches by label
await browserAction("fill", { fields: { "#email": "filled@example.com" } });
body = await browserAction("eval", { expression: "document.getElementById('email').value" });
assert.ok(body.text.includes("filled@example.com"), `fill did not land:\n${body.text}`);
await browserAction("select", { selector: "#plan", value: "Beta" });
body = await browserAction("eval", { expression: "document.getElementById('plan').value" });
assert.ok(body.text.includes("b"), `select did not land:\n${body.text}`);
// wait blocks for content that shows up after load, and reports a timeout otherwise
await browserAction("open", { url });
const waited = await browserAction("wait", { find: "arrived late", timeout: 5000 });
assert.ok(/after \d+ms/.test(waited.text), waited.text);
const timedOut = await browserAction("wait", { selector: "#nope", timeout: 600 }).then(
() => "",
(e) => String(e),
);
assert.ok(/timed out/.test(timedOut), `wait should time out, got: ${timedOut || "(no error)"}`);
// An element covered by a sibling overlay: a coordinate click lands on the overlay and the real
// handler never runs, which is indistinguishable from a broken click. Must fall back to
// dispatching on the element itself.
await browserAction("open", { url });
const covered = await browserAction("click", { selector: "#row" });
r = await browserAction("eval", { expression: "document.getElementById('out').textContent" });
assert.ok(r.text.includes("row"), `covered element's handler never ran:\n${r.text}`);
assert.ok(/covered/.test(covered.text), `the fallback should say it was used: ${covered.text}`);
// hover, reload and pdf must all survive a round trip
await browserAction("hover", { find: "Do thing" });
await browserAction("reload", {});
const pdf = await browserAction("pdf", {});
assert.ok(pdf.pdf && pdf.pdf.subarray(0, 4).toString() === "%PDF", "pdf output is not a PDF");
// tabs: list shows origins, new adds one, close removes it
const before = (await tabAction("tabs")).split("\n").length;
const id = (await tabAction("tab_new")).replace("opened tab ", "");
const listed = await tabAction("tabs");
assert.ok(listed.split("\n").length === before + 1, listed);
assert.ok(!listed.includes("check"), "tab list must not show page titles");
await tabAction("tab_close", id);
assert.equal((await tabAction("tabs")).split("\n").length, before);
console.log("browser check: ok");
} finally {
srv.close();
srv.closeAllConnections();
}
}
await main();