mirror of
https://github.com/SawyerHood/dev-browser.git
synced 2026-09-20 13:44:44 +08:00
16dec89cfe
* doobie: skeleton — client, daemon, vm runner, page helpers, build Co-Authored-By: Claude <noreply@anthropic.com> * doobie: snapshot engine, waitForLoad, docs, tests, bench, Bun-native fast client Co-Authored-By: Claude <noreply@anthropic.com> * doobie: fix round 1 from adversarial review (transport backpressure, profiles per mode, page mutex, bringToFront, zombie runs, shot DPR, dialogs, snapshot names/frames/boxes, docs) Co-Authored-By: Claude <noreply@anthropic.com> * docs: handoff notes Co-Authored-By: Claude <noreply@anthropic.com> * launch: mark clean exit to skip session restore, cache --no-sandbox; untrack build artifact Co-Authored-By: Claude <noreply@anthropic.com> * snapshot: drop [cursor=pointer] on inherently interactive roles, no row content names (HN interactive 29k -> 21k chars) Co-Authored-By: Claude <noreply@anthropic.com> * ci: bench tolerance on shared runners; release: npm publish only with NPM_TOKEN Co-Authored-By: Claude <noreply@anthropic.com> * mcp: stdio MCP server over the daemon frames (doobie mcp); ci: explicit test timeout Co-Authored-By: Claude <noreply@anthropic.com> * docs: bb integration note (socket source contract) Co-Authored-By: Claude <noreply@anthropic.com> * fix round 2: front lock (no stale cache), run gate covers handles/frames/popups, --connect extends only touched tabs, doobie chrome verifies launch, self-healing shim + devDependencies, snapshot name fallbacks/pointer inheritance/shadow refs, docs Co-Authored-By: Claude <noreply@anthropic.com> * daemon: drain active requests before exit; tests: realpath-safe and node-optional packaging tests; chrome: share sandbox helpers Co-Authored-By: Claude <noreply@anthropic.com> * relative file paths resolve against the caller's cwd; readFile("downloads/<name>"); TimeoutError for ref waits Co-Authored-By: Claude <noreply@anthropic.com> * runtime: share host Error constructors with the script realm Co-Authored-By: Claude <noreply@anthropic.com> * shim: sh/JS polyglot so bun-only machines run it; v0.1.1 Co-Authored-By: Claude <noreply@anthropic.com> * launch: automation profile prefs (leak-detection dialog off, no password/autofill UI) — fixes dead input after logins in new headless Co-Authored-By: Claude <noreply@anthropic.com> * v0.1.2 Co-Authored-By: Claude <noreply@anthropic.com> * client: retry when racing a shutting-down daemon (flaky stop/status on CI) Co-Authored-By: Claude <noreply@anthropic.com> * feat!: make doobie the dev-browser 1.0 runtime * ci: use Node 24 GitHub actions * fix: isolate authenticated CDP sessions * fix: close run gate escape paths * fix: preserve UTF-8 across protocol chunks * test: stabilize browser context gate coverage * test: isolate browser context gate coverage --------- Co-authored-by: Sawyer Hood <kirbyhood@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
139 lines
6.1 KiB
TypeScript
139 lines
6.1 KiB
TypeScript
/**
|
|
* Docs consistency: docs/help.md (embedded in `dev-browser --help`) must stay within
|
|
* the line budget, carry no claims the review found stale, and its examples must
|
|
* behave as documented (review-2 docs findings).
|
|
*/
|
|
import { test, expect, afterAll } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { makeCliEnv } from "../helpers/cli.ts";
|
|
import { startServer } from "../helpers/server.ts";
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "../..");
|
|
const HELP = fs.readFileSync(path.join(ROOT, "docs/help.md"), "utf8");
|
|
const README = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");
|
|
const SKILL = fs.readFileSync(path.join(ROOT, "skills/dev-browser/SKILL.md"), "utf8");
|
|
const DESIGN = fs.readFileSync(path.join(ROOT, "docs/design-decisions.md"), "utf8");
|
|
|
|
const cli = makeCliEnv("dev-browser-docs-");
|
|
afterAll(async () => {
|
|
await cli.cleanup();
|
|
});
|
|
|
|
function section(name: string): string {
|
|
const parts = HELP.split("\n## ");
|
|
const hit = parts.find((p) => p.startsWith(`${name}\n`));
|
|
if (!hit) throw new Error(`no help section ${name}`);
|
|
return hit.slice(name.length + 1);
|
|
}
|
|
|
|
test("help.md stays within the line budget and every topic is present", () => {
|
|
expect(HELP.replace(/\n$/, "").split("\n").length).toBeLessThanOrEqual(330); // as `wc -l`
|
|
for (const t of "quickstart workflow scripts pages snapshot refs screenshots waiting forms errors output connect chrome config migration json mcp examples tips".split(" ")) {
|
|
expect(HELP).toContain(`\n## ${t}\n`);
|
|
}
|
|
// usage line and chrome section advertise --headless
|
|
expect(HELP).toContain("dev-browser chrome [--profile NAME] [--port N] [--chrome PATH] [--headless] [--list] [URL]");
|
|
expect(section("chrome")).toContain("--headless");
|
|
expect(section("chrome")).toContain("/json/version");
|
|
expect(section("chrome")).toContain("chrome-logs/NAME.log");
|
|
});
|
|
|
|
test("help.md no longer carries the stale claims from the review", () => {
|
|
expect(HELP).not.toContain("not two on one page");
|
|
expect(HELP).not.toContain("-> TypeError");
|
|
expect(HELP).not.toContain('"await p.$$eval(".athing .titleline a"');
|
|
expect(HELP).toContain('.athing .titleline > a');
|
|
expect(HELP).not.toContain("lists tabs for every running browser\n (with -b/--connect it launches/attaches that browser first)");
|
|
expect(HELP).toContain("lists only its tabs");
|
|
const scripts = section("scripts");
|
|
expect(scripts).toContain("ReferenceError");
|
|
expect(scripts).toContain("synchronous CPU-bound code");
|
|
expect(scripts).toContain("bring-to-front lock");
|
|
expect(scripts).toContain("Two scripts on one named page interleave");
|
|
expect(scripts).toMatch(/setDefaultTimeout.*setRequestInterception\(true\)/);
|
|
const pages = section("pages");
|
|
expect(pages).toContain("BrowserStoppedError");
|
|
expect(pages).toContain("only tabs you touch");
|
|
expect(pages).toContain("downloads/<name>");
|
|
expect(pages).toContain("ERR_ABORTED");
|
|
expect(pages).toMatch(/`pages`\/`status`/);
|
|
const refs = section("refs");
|
|
expect(refs).toContain("page.$/$$ return null/[]");
|
|
expect(refs).toContain("TimeoutError");
|
|
const forms = section("forms");
|
|
expect(forms).toContain("pdf({ path })");
|
|
expect(forms).toContain("auto-accepted");
|
|
const errors = section("errors");
|
|
expect(errors).toContain("BrowserStoppedError");
|
|
expect(errors).toContain('(goto "URL" failed)');
|
|
expect(errors).toContain("help <unknown topic>");
|
|
expect(section("snapshot")).toContain('button "Choose File"');
|
|
});
|
|
|
|
test("README, SKILL and design doc reflect the current behaviour", () => {
|
|
expect(README).toContain("medians of 9 runs");
|
|
expect(README).not.toContain("~570 ms");
|
|
expect(README).toContain("DEV_BROWSER_DOWNLOAD_BASE");
|
|
expect(README).toContain("## Releasing");
|
|
expect(README).toContain("bring-to-front lock");
|
|
expect(SKILL).toContain("bun add -g dev-browser");
|
|
expect(README).toContain("migrate-from-doobie");
|
|
expect(DESIGN).toContain("BrowserStoppedError");
|
|
expect(DESIGN).toContain("auto-accepted");
|
|
expect(DESIGN).not.toContain("`serve`/MCP");
|
|
});
|
|
|
|
test("documented behaviours hold: help exit 2, ASI ReferenceError, stale ref $ -> null, ref wait is a TimeoutError, pages -b", async () => {
|
|
const bogus = await cli.run(["help", "bogus-topic"]);
|
|
expect(bogus.code).toBe(2);
|
|
expect(bogus.stderr).toContain('No help topic "bogus-topic"');
|
|
|
|
const asi = await cli.run(["--headless"], {
|
|
stdin: 'const p = await browser.getPage("docs")\n(await p.shot()).path\n',
|
|
});
|
|
expect(asi.code).toBe(1);
|
|
expect(asi.stderr).toContain("ReferenceError: Cannot access 'p' before initialization");
|
|
|
|
const srv = await startServer({
|
|
"/": '<a id=l href="/two">two</a><input type=file id=f><input placeholder=Search>',
|
|
"/two": "<h1>two</h1>",
|
|
});
|
|
try {
|
|
const snap = await cli.run([
|
|
"--headless",
|
|
"-e",
|
|
`const p = await browser.getPage("docs"); await p.goto(${JSON.stringify(srv.url("/"))}); await p.snapshot()`,
|
|
]);
|
|
expect(snap.stdout).toContain('button "Choose File"');
|
|
expect(snap.stdout).toContain('textbox "Search"');
|
|
|
|
const stale = await cli.run([
|
|
"--headless",
|
|
"-e",
|
|
`const p = await browser.getPage("docs"); await p.goto(${JSON.stringify(srv.url("/two"))}); ({ one: await p.$("ref/e1"), all: await p.$$("ref/e1") })`,
|
|
]);
|
|
expect(stale.code).toBe(0);
|
|
expect(JSON.parse(stale.stdout)).toEqual({ one: null, all: [] });
|
|
|
|
const wait = await cli.run([
|
|
"--headless",
|
|
"-e",
|
|
`const p = await browser.getPage("docs"); let r; try { await p.waitForSelector("ref/e1", { timeout: 300 }) } catch (e) { r = { name: e.name, msg: e.message } } r`,
|
|
]);
|
|
const parsed = JSON.parse(wait.stdout);
|
|
expect(parsed.name).toBe("TimeoutError");
|
|
expect(parsed.msg).toContain("Waiting for selector `ref/e1` failed");
|
|
} finally {
|
|
await srv.stop();
|
|
}
|
|
|
|
// `pages -b NAME` launches that browser and lists only its tabs
|
|
const one = await cli.run(["--headless", "-b", "docs-b", "pages"]);
|
|
expect(one.stdout).toContain("docs-b:headless:");
|
|
expect(one.stdout).not.toContain("default:headless:");
|
|
const all = await cli.run(["--headless", "pages"]);
|
|
expect(all.stdout).toContain("default:headless:");
|
|
expect(all.stdout).toContain("docs-b:headless:");
|
|
}, 120_000);
|