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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ 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)).
description, with `--base-url <url>` for absolute image links once the out dir is uploaded
([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
30 changes: 29 additions & 1 deletion docs/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,35 @@ Build it after a fresh full run.
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.
relative to the Markdown file, or `<url>/<shot>` with `--base-url <url>`: upload the out dir
anywhere, then pass its URL so the images render in a PR description. `evidence.html` is
self-contained and can go anywhere on its own.

Cloudflare Pages gives `report.html` a per-PR URL:

```bash
npx wrangler pages project create <p> --production-branch main # once
npx wrangler pages deploy renderdiff-out --project-name <p> --branch pr-123
renderdiff report renderdiff-out --markdown pr.md --base-url https://pr-123.<p>.pages.dev
```

In CI, wrangler needs `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Keep branch names
simple like `pr-123`: the alias is lowercased, sanitized and truncated. Cloudflare Access can
keep `report.html` private, but then the images won't render in the PR: GitHub fetches
images through its anonymous camo proxy. For private inline images, use a reports repo:

```bash
git clone --depth 1 https://github.com/<owner>/<repo> reports
mkdir -p reports/pr-123 && cp -R renderdiff-out/. reports/pr-123/
git -C reports add . && git -C reports commit -m pr-123 && git -C reports push
renderdiff report renderdiff-out --markdown pr.md \
--base-url https://github.com/<owner>/<repo>/raw/main/pr-123
```

Those links render for viewers signed in with read access to the reports repo. Don't copy
`raw.githubusercontent.com/...?token=` URLs instead: the token expires. Use a separate repo,
not a branch of the main one: every clone of the main repo would download the screenshots, and
its history keeps them after the branch is deleted.

## merge

Expand Down
3 changes: 2 additions & 1 deletion skills/renderdiff/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,5 @@ attachment is the archive copy. Accepted entries are OS-specific: build it on th
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.
`report <out> --markdown pr.md` for the PR description's before/after tables; add
`--base-url <url>` once the out dir is uploaded, so the images render in the PR.
21 changes: 15 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,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>] [--markdown <file>]
renderdiff report <out-dir> [--verdict <file>] [--title <text>] [--markdown <file> [--base-url <url>]]
renderdiff merge <full-out> <partial-out> --out <dir>
renderdiff probe <url> [--width <n>] [--loads <n>]

Expand Down Expand Up @@ -214,7 +214,8 @@ const HELP = `
evidence.html beside it: one self-contained page (crops embedded) with a parity banner and
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.
run with --threshold 0 --no-recheck first. --base-url <url> is a URL the shot names are
appended to in those links, for an out dir uploaded there.

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 @@ -366,10 +367,15 @@ 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; "--markdown"?: string } = {};
const opt: {
"--verdict"?: string;
"--title"?: string;
"--markdown"?: string;
"--base-url"?: string;
} = {};
for (let i = 0; i < argv.length; i++) {
const v = argv[i]!;
if (v === "--verdict" || v === "--title" || v === "--markdown") {
if (v === "--verdict" || v === "--title" || v === "--markdown" || v === "--base-url") {
const value = argv[++i];
if (value === undefined) throw new Error(`${v} needs a value`);
opt[v] = value;
Expand All @@ -379,8 +385,10 @@ async function report(argv: string[]): Promise<never> {
}
if (!dir)
throw new Error(
"usage: renderdiff report <out-dir> [--verdict <file>] [--title <text>] [--markdown <file>]",
"usage: renderdiff report <out-dir> [--verdict <file>] [--title <text>] [--markdown <file> [--base-url <url>]]",
);
if (opt["--base-url"] !== undefined && !opt["--markdown"])
throw new Error("--base-url needs --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 =
Expand All @@ -390,7 +398,8 @@ async function report(argv: string[]): Promise<never> {
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"]));
if (opt["--markdown"])
console.log(writeMarkdown(data, dir, opt["--markdown"], opt["--base-url"]));
process.exit(0);
} catch (e) {
console.error(`✗ ${e instanceof Error ? e.message : String(e)}`);
Expand Down
13 changes: 11 additions & 2 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,9 +808,18 @@ ${neutralized(meta, results)}
}

/** 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 {
export function writeMarkdown(
data: Results,
outDir: string,
file: string,
baseUrl?: string,
): string {
const link = (img: string) =>
encodeURI(path.relative(path.dirname(file), path.join(outDir, img)).split(path.sep).join("/"));
baseUrl
? `${baseUrl.replace(/\/+$/, "")}/${encodeURI(img)}`
: 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) =>
Expand Down
9 changes: 8 additions & 1 deletion tests/unit/cli-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@ describe("help and version", () => {
if (!options.includes(f))
problems.push(`the README table lists ${f}, --help's Options don't`);

const foreign = new Set(["--immutable"]); // yarn install, in docs/case-study.md
// yarn install in docs/case-study.md; wrangler and git in docs/output.md
const foreign = new Set([
"--immutable",
"--production-branch",
"--project-name",
"--branch",
"--depth",
]);
for (const f of ["README.md", ...md("docs"), ...md("skills")])
for (const flag of flags(read(f)))
if (!known.has(flag) && !foreign.has(flag))
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,28 @@ describe("renderdiff report --markdown", () => {
"| 1440 | error: boom | |",
]);
});

test("--base-url links the shots under that URL, not relative to the file", async () => {
const out = path.join(tempDir(), "shot out");
fs.mkdirSync(out);
const images = { left: "index.1440.left.png", right: "index.1440.right.png", diff: "x.png" };
writeResults([shot("/", { images })], meta(), out, []);
const md = path.join(out, "..", "pr", "pr.md");
const args = ["report", out, "--markdown", md, "--base-url", "https://r.example/pr-1/"];
const r = await runCli(args, { noOut: true });
assert.equal(r.code, 0, r.stderr);
assert.deepEqual(fs.readFileSync(md, "utf8").match(/^\| \d+.*/gm), [
"| 1440 | ![before](https://r.example/pr-1/index.1440.right.png) | ![after](https://r.example/pr-1/index.1440.left.png) |",
]);
});

test("--base-url without --markdown is a usage error", async () => {
const r = await runCli(["report", tempDir(), "--base-url", "https://r.example"], {
noOut: true,
});
assert.equal(r.code, 2);
assert.match(r.stderr, /--base-url needs --markdown/);
});
});

describe("verdict", () => {
Expand Down
Loading