diff --git a/README.md b/README.md index 1689384..b2b1086 100644 --- a/README.md +++ b/README.md @@ -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 --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 --out ` 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) diff --git a/docs/output.md b/docs/output.md index 8c682bc..45e432c 100644 --- a/docs/output.md +++ b/docs/output.md @@ -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 --markdown ` also writes a Markdown file for documenting an +intended change: a `## ` 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 --out ` writes a new out dir: the full run's diff --git a/skills/renderdiff/SKILL.md b/skills/renderdiff/SKILL.md index f75a82b..05a378d 100644 --- a/skills/renderdiff/SKILL.md +++ b/skills/renderdiff/SKILL.md @@ -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 --markdown pr.md` for the PR description's before/after tables. diff --git a/skills/renderdiff/reference.md b/skills/renderdiff/reference.md index 7eaa05b..6f03f7c 100644 --- a/skills/renderdiff/reference.md +++ b/skills/renderdiff/reference.md @@ -330,8 +330,10 @@ rather than remove. ## `report` -`renderdiff report [--verdict ] [--title ]` rewrites `/report.html` -and writes `/evidence.html` from `results.json`, no browser. `report.html` has a +`renderdiff report [--verdict ] [--title ] [--markdown ]` rewrites +`/report.html` and writes `/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 diff --git a/src/cli.ts b/src/cli.ts index 3d0cbe4..f992814 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 [--verdict ] [--title ] + renderdiff report [--verdict ] [--title ] [--markdown ] renderdiff merge --out renderdiff probe [--width ] [--loads ] @@ -208,7 +208,9 @@ const HELP = ` renderdiff report reads results.json in , 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 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 's results to --out with each shot also has replaced by the partial run's, and its other shots added. Both must compare the same origins. @@ -359,10 +361,10 @@ async function sitemapRoutes(spec: string | true, routes: Route[]): Promise { 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; @@ -371,16 +373,19 @@ async function report(argv: string[]): Promise { else dir = v; } if (!dir) - throw new Error("usage: renderdiff report [--verdict ] [--title ]"); + throw new Error( + "usage: renderdiff report [--verdict ] [--title ] [--markdown ]", + ); 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)}`); diff --git a/src/report.ts b/src/report.ts index 9e52ca5..76e101c 100644 --- a/src/report.ts +++ b/src/report.ts @@ -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; +} diff --git a/tests/unit/report.test.ts b/tests/unit/report.test.ts index 072906a..49f02c9 100644 --- a/tests/unit/report.test.ts +++ b/tests/unit/report.test.ts @@ -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 });