Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <url>`: load one URL several times and list what varies between loads. */
async function probe(a: Args, loads: number): Promise<never> {
if (!a.left) die("probe needs a URL: renderdiff probe <url>");
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;
Expand All @@ -517,7 +530,24 @@ async function probe(a: Args, loads: number): Promise<never> {
} 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 <n> for another)` : ""}`,
);
}
await preflight(cfg.left, cfg.left);
const { probe } = await import("./probe.ts");
try {
Expand All @@ -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));
}
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/cli-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n> 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", () => {
Expand Down
Loading