From 0b1a36b21e4716a544fc5360e028bc7cbdb7c7a6 Mon Sep 17 00:00:00 2001 From: Timur Mashara <62305878+tmashara@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:20:05 +0100 Subject: [PATCH] probe: refuse run-only flags, pick the storage state by side, name the width probe takes an allow-list of the flags it uses; any other flag (--dry-run, --out, --sitemap, --threshold, --accept, --left-/--right-storage-state, ...) exits 2 instead of being ignored. It uses the storage state of the config side whose origin the URL is on (else right's if set, else left's) and prints it, and prints the width it falls back to without --width. Co-Authored-By: Claude Opus 5.5 --- src/cli.ts | 44 ++++++++++++++++++++++++++++++++----- tests/unit/cli-args.test.ts | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index b20cb93..31628de 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -233,9 +233,10 @@ const HELP = ` has replaced by the partial run's, and its other shots added. Both must compare the same origins. renderdiff probe loads one URL --loads times (default 8, in parallel up to --concurrency), - each in a fresh browser context as a run loads a side, and lists the elements whose computed - style or position in their parent varies between loads: what to mask. Exits 0 if nothing - varies, 1 if something does, 3 if a load errored. + each in a fresh browser context as a run loads a side (with the storage state of the config + side the URL is on), and lists the elements whose computed style or position in their parent + varies between loads: what to mask. Exits 0 if nothing varies, 1 if something does, 3 if a + load errored. Writes report.html, results.json and crops of each change to --out. Exits 1 if any shot differs and isn't accepted (with --threshold, only if one scores below it; @@ -496,13 +497,25 @@ function only(values: string[], routes: Route[]): Route[] { return routes.filter((r) => picked.has(r)); } +const PROBE_FLAGS = [ + "--help", + "--version", + "--left", + "--config", + "--width", + "--concurrency", + "--mask", + "--hide", + "--mask-media", + "--no-freeze-video", + "--storage-state", +]; + /** `renderdiff probe `: load one URL several times and list what varies between loads. */ async function probe(a: Args, loads: number): Promise { if (!a.left) die("probe needs a URL: renderdiff probe "); if (a.right || a.routes.length) die("probe takes one URL, with its path"); if ((a.widths?.length ?? 0) > 1) die("probe takes one --width"); - if (a.selector) die("probe does not take --selector; it walks the whole page"); - if (a.only) die("probe does not take --only"); if (!isHttpUrl(a.left)) die(`not an http(s) URL: ${a.left}`); const u = new URL(a.left); let route: Route; @@ -517,7 +530,24 @@ async function probe(a: Args, loads: number): Promise { } catch (e) { die(e instanceof Error ? e.message : String(e)); } + const at = (o?: string) => !!o && isHttpUrl(o) && new URL(o).origin === u.origin; + const side = at(fileConfig.right) + ? "right" + : at(fileConfig.left) + ? "left" + : cfg.storageState.right + ? "right" + : "left"; + const s = cfg.storageState[side]; + cfg.storageState = { left: s, right: s }; if (configPath) console.log(`Config: ${configPath}`); + if (s) console.log(`Storage state: ${s} (${side})`); + if (!a.widths) { + const more = cfg.widths.length > 1; + console.log( + `Width: ${cfg.widths[0]}${more ? ` (first of ${cfg.widths.join(", ")}; --width for another)` : ""}`, + ); + } await preflight(cfg.left, cfg.left); const { probe } = await import("./probe.ts"); try { @@ -538,6 +568,10 @@ try { const argv = process.argv.slice(probing ? 3 : 2); if (probing) loads = takeLoads(argv); args = parseArgs(argv); + const extra = + probing && + argv.map((t) => t.split("=")[0]!).find((f) => f.startsWith("--") && !PROBE_FLAGS.includes(f)); + if (extra) throw new Error(`probe does not take ${extra}`); } catch (e) { die(e instanceof Error ? e.message : String(e)); } diff --git a/tests/unit/cli-args.test.ts b/tests/unit/cli-args.test.ts index ee6da45..d4aba70 100644 --- a/tests/unit/cli-args.test.ts +++ b/tests/unit/cli-args.test.ts @@ -527,6 +527,50 @@ describe("probe", () => { await rejects(["probe", U, "--width", "1440", "--width", "375"], "probe takes one --width"); await rejects(["probe", U, "--selector", "main"], "probe does not take --selector"); }); + + test("refuses flags it would ignore", async () => { + await rejects(["probe", U, "--dry-run"], "probe does not take --dry-run"); + await rejects(["probe", U, "--out=x"], "probe does not take --out"); + await rejects( + ["probe", U, "--left-storage-state", "s.json"], + "probe does not take --left-storage-state", + ); + }); + + test("names the width it falls back to", async () => { + const r = await runCli(["probe", "http://127.0.0.1:1/a"], { noOut: true }); + assert.equal(r.code, 2, r.stderr); + assert.match( + r.stdout, + /Width: 1440 \(first of 1440, 1152, 896, 704, 375; --width for another\)/, + ); + const w = await runCli(["probe", "http://127.0.0.1:1/a", "--width", "375"], { noOut: true }); + assert.equal(w.code, 2, w.stderr); + assert.doesNotMatch(w.stdout, /Width:/); + }); + + test("uses the storage state of the side the URL is on, else the configured one", async () => { + const project = (storageState: string) => { + const cwd = tempDir(); + fs.writeFileSync( + path.join(cwd, "renderdiff.config.mjs"), + `export default { left: "http://127.0.0.1:1", right: "http://127.0.0.1:2", storageState: ${storageState} };`, + ); + for (const f of ["l.json", "r.json"]) fs.writeFileSync(path.join(cwd, f), "{}"); + return cwd; + }; + const state = async (cwd: string, url: string) => { + const r = await runCli(["probe", url], { cwd, noOut: true }); + assert.match(r.stderr, /not responding/); + return r.stdout; + }; + const both = project(`{ left: "l.json", right: "r.json" }`); + assert.match(await state(both, "http://127.0.0.1:1/a"), /Storage state: .*l\.json \(left\)$/m); + assert.match(await state(both, "http://127.0.0.1:2/a"), /Storage state: .*r\.json \(right\)$/m); + assert.match(await state(both, "http://127.0.0.1:3/a"), /Storage state: .*r\.json \(right\)$/m); + const left = project(`{ left: "l.json" }`); + assert.match(await state(left, "http://127.0.0.1:3/a"), /Storage state: .*l\.json \(left\)$/m); + }); }); describe("preflight", () => {