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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ Everything lands in the out dir (`./renderdiff-out` by default):
and writes `evidence.html` there: one self-contained page (a Parity / Differs / Not verified
banner, your verdict, crops embedded) to attach to a ticket or publish as proof:
[docs/output.md](https://github.com/tmashara/renderdiff/blob/main/docs/output.md#evidencehtml).
For an intended change, run with `--threshold 0 --no-recheck`, then
`renderdiff report <out> --markdown pr.md` writes a before/after table per route for the PR
description ([docs/output.md](https://github.com/tmashara/renderdiff/blob/main/docs/output.md#markdown)).

`renderdiff merge <full-out> <partial-out> --out <dir>` writes the full run's results to a new
out dir with the shots a partial run also took (say, a few routes rerun under a new mask)
Expand Down
8 changes: 8 additions & 0 deletions docs/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,14 @@ and a card per shot that isn't clean, then one per accepted shot, with crops emb
WebP. Crops past 12MB, or missing from the out dir, are left out and the banner says so.
Build it after a fresh full run.

## markdown

`renderdiff report <out-dir> --markdown <file>` also writes a Markdown file for documenting an
intended change: a `## <route>` section per route with the before (right) and after (left)
URLs, and a `| Width | Before | After |` table linking each width's full-page shots, with
`(h±Npx)` when the heights differ and the error in place of an errored shot. Links are
relative to the Markdown file; uploading the images to GitHub stays manual.

## merge

`renderdiff merge <full-out> <partial-out> --out <dir>` writes a new out dir: the full run's
Expand Down
3 changes: 3 additions & 0 deletions skills/renderdiff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ After a fresh final run (never after `--rerun-failed`), write the four parts to
`evidence.html`. Offer to publish it as an Artifact for a link that opens in a browser; a ticket
attachment is the archive copy. Accepted entries are OS-specific: build it on the OS that
recorded them.

For an intended change (a redesign), run with `--threshold 0 --no-recheck` and
`report <out> --markdown pr.md` for the PR description's before/after tables.
6 changes: 4 additions & 2 deletions skills/renderdiff/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,10 @@ rather than remove.

## `report`

`renderdiff report <out> [--verdict <file>] [--title <text>]` rewrites `<out>/report.html`
and writes `<out>/evidence.html` from `results.json`, no browser. `report.html` has a
`renderdiff report <out> [--verdict <file>] [--title <text>] [--markdown <file>]` rewrites
`<out>/report.html` and writes `<out>/evidence.html` from `results.json`, no browser.
`--markdown` also writes a section per route with a `| Width | Before | After |` table (right
before, left after, `(h±Npx)` on a height change) linking the shots relative to that file. `report.html` has a
filterable route sidebar coloured by `--threshold` and a summary of only the differing
shots. `evidence.html`: a Parity / Differs / Not verified banner, the `renderdiff:` line,
the verdict file as plain text, coverage, and cards for shots that aren't clean, then
Expand Down
17 changes: 11 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const HELP = `

renderdiff http://localhost:3000 https://example.com / /about
renderdiff http://localhost:3000 https://example.com /pricing::/plans
renderdiff report <out-dir> [--verdict <file>] [--title <text>]
renderdiff report <out-dir> [--verdict <file>] [--title <text>] [--markdown <file>]
renderdiff merge <full-out> <partial-out> --out <dir>
renderdiff probe <url> [--width <n>] [--loads <n>]

Expand All @@ -208,7 +208,9 @@ const HELP = `

renderdiff report reads results.json in <out-dir>, rewrites report.html, and writes
evidence.html beside it: one self-contained page (crops embedded) with a parity banner and
the --verdict file's text.
the --verdict file's text. --markdown <file> also writes a before/after table per route
(right before, left after, links to the shots) for a PR description; for an intended change,
run with --threshold 0 --no-recheck first.

renderdiff merge writes <full-out>'s results to --out with each shot <partial-out> also
has replaced by the partial run's, and its other shots added. Both must compare the same origins.
Expand Down Expand Up @@ -359,10 +361,10 @@ async function sitemapRoutes(spec: string | true, routes: Route[]): Promise<Rout
async function report(argv: string[]): Promise<never> {
try {
let dir: string | undefined;
const opt: { "--verdict"?: string; "--title"?: string } = {};
const opt: { "--verdict"?: string; "--title"?: string; "--markdown"?: string } = {};
for (let i = 0; i < argv.length; i++) {
const v = argv[i]!;
if (v === "--verdict" || v === "--title") {
if (v === "--verdict" || v === "--title" || v === "--markdown") {
const value = argv[++i];
if (value === undefined) throw new Error(`${v} needs a value`);
opt[v] = value;
Expand All @@ -371,16 +373,19 @@ async function report(argv: string[]): Promise<never> {
else dir = v;
}
if (!dir)
throw new Error("usage: renderdiff report <out-dir> [--verdict <file>] [--title <text>]");
throw new Error(
"usage: renderdiff report <out-dir> [--verdict <file>] [--title <text>] [--markdown <file>]",
);
const file = path.join(dir, "results.json");
if (!fs.existsSync(file)) throw new Error(`no ${file}; run renderdiff with --out ${dir} first`);
const verdict =
opt["--verdict"] === undefined ? null : fs.readFileSync(opt["--verdict"], "utf8");
const { writeEvidence, writeReport } = await import("./report.ts");
const { writeEvidence, writeMarkdown, writeReport } = await import("./report.ts");
const data: Results = JSON.parse(fs.readFileSync(file, "utf8"));
console.log(await writeReport(data.results, data.meta, dir, data.summary.stale));
const out = await writeEvidence(data, dir, verdict, opt["--title"]);
console.log(`${out} (${Math.ceil(fs.statSync(out).size / 1024)} KB)`);
if (opt["--markdown"]) console.log(writeMarkdown(data, dir, opt["--markdown"]));
process.exit(0);
} catch (e) {
console.error(`✗ ${e instanceof Error ? e.message : String(e)}`);
Expand Down
27 changes: 27 additions & 0 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,3 +806,30 @@ ${neutralized(meta, results)}
}
return out;
}

/** Before/after tables for a PR description: one section per route, right side as before. */
export function writeMarkdown(data: Results, outDir: string, file: string): string {
const link = (img: string) =>
encodeURI(path.relative(path.dirname(file), path.join(outDir, img)).split(path.sep).join("/"));
const sections = [...Map.groupBy(data.results, name)].map(([rt, shots]) => {
const r0 = shots[0]!;
const rows = shots.map((r) =>
r.status === "error"
? `| ${r.width} | error: ${r.error.replace(/\s+/g, " ").replace(/\|/g, "\\|")} | |`
: `| ${r.width}${r.heightDelta ? ` (h${signed(r.heightDelta)}px)` : ""} | ![before](${link(r.images.right)}) | ![after](${link(r.images.left)}) |`,
);
return [
`## ${rt}`,
"",
`Before: ${data.meta.right}${r0.right} `,
`After: ${data.meta.left}${r0.left}`,
"",
"| Width | Before | After |",
"| --- | --- | --- |",
...rows,
].join("\n");
});
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, sections.join("\n\n") + "\n");
return file;
}
29 changes: 29 additions & 0 deletions tests/unit/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,35 @@ describe("writeEvidence", () => {
});
});

describe("renderdiff report --markdown", () => {
test("a section per route, right as before, left as after, links relative to the file", async () => {
const out = path.join(tempDir(), "shot out");
fs.mkdirSync(out);
const imgs = (stem: string) => ({
left: `${stem}.left.png`,
right: `${stem}.right.png`,
diff: `${stem}.diff.png`,
});
const results = [
shot("/", { images: imgs("index.1440"), heightDelta: 24 }),
shot("/", { width: 375, images: imgs("index.375") }),
{ ...shot("/b"), status: "error", error: "boom" } as RouteResult,
];
writeResults(results, meta({ widths: [1440, 375] }), out, []);
const md = path.join(out, "..", "pr", "pr.md");
const r = await runCli(["report", out, "--markdown", md], { noOut: true });
assert.equal(r.code, 0, r.stderr);
const text = fs.readFileSync(md, "utf8");
assert.deepEqual(text.match(/^## .*/gm), ["## /", "## /b"]);
assert.match(text, /Before: https:\/\/example\.com\/ {2}\nAfter: http:\/\/localhost:3000\//);
assert.deepEqual(text.match(/^\| \d+.*/gm), [
"| 1440 (h+24px) | ![before](../shot%20out/index.1440.right.png) | ![after](../shot%20out/index.1440.left.png) |",
"| 375 | ![before](../shot%20out/index.375.right.png) | ![after](../shot%20out/index.375.left.png) |",
"| 1440 | error: boom | |",
]);
});
});

describe("verdict", () => {
const error = { ...shot("/e"), status: "error", error: "boom" } as RouteResult;
const at = (score: number) => shot("/a", { score, diffPixels: 1 });
Expand Down
Loading