diff --git a/README.md b/README.md index b2b1086..01c1c9e 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ renderdiff [options] | `--selector ` | Compare the one element this Playwright selector matches, on both sides, instead of the whole page. Applies to every route without its own `@selector`, over the config's per-route `selector`. | | `--left-selector `, `--right-selector ` | That side's selector, over `--selector`. One without the other side (or `--selector`) is an error. | | `--mask ` | Paint this Playwright selector over on both sides, on every route; repeatable. Added to the config's masks and listed in the report as coverage given up. | +| `--hide ` | Hide this CSS selector (not Playwright) on both sides, on every route; repeatable. Added to the config's `hideSelectors`. For chrome that should be absent on both sides; content that drifts belongs in `--mask`. | | `--mask-media` | Mask every `iframe` on every route, as config `maskMedia: true` does. Video isn't masked: it is frozen by default, so the player's box is still compared. | | `--no-freeze-video` | Leave videos playing and video.js overlays visible, as config `freezeVideo: false` does. By default every video is paused on frame 0 (its poster if it never loads) ([how it works](https://github.com/tmashara/renderdiff/blob/main/docs/how-it-works.md)); unfrozen, expect unstable pixels or `did not settle`. `--mask-media` doesn't mask them either. | | `--storage-state ` | Start both sides with this Playwright storage state (cookies, localStorage), e.g. signed in. Relative to cwd. Over the config's `storageState`. | diff --git a/skills/renderdiff/reference.md b/skills/renderdiff/reference.md index 22dde75..37c0a20 100644 --- a/skills/renderdiff/reference.md +++ b/skills/renderdiff/reference.md @@ -327,7 +327,7 @@ export default { A mask takes exactly one of `selector` or `locate: (page) => Locator`; without `urlPath` it applies to every route, and on a pair it matches either side's path. `--mask ` adds to the -config's masks. Also: `threshold`, `outDir`, `concurrency`, `extraCss`, `before`, +config's masks; `--hide ` (CSS) adds to `hideSelectors`. Also: `threshold`, `outDir`, `concurrency`, `extraCss`, `before`, `beforeShot`, `storageState`. `hideSelectors`: match the element **exactly** (consent scripts stamp classes on ``; a loose selector blanks the whole document and blank matches blank), and they hide rather than remove. diff --git a/src/cli.ts b/src/cli.ts index f992814..3ff8cd9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,6 +116,10 @@ function parseArgs(argv: string[]): Args { const s = next().trim(); if (!s) throw new Error("--mask needs a value"); (a.masks ??= []).push(s); + } else if (v === "--hide") { + const s = next().trim(); + if (!s) throw new Error("--hide needs a value"); + (a.hideSelectors ??= []).push(s); } else if (v === "--selector" || v === "--left-selector" || v === "--right-selector") { const s = next(); if (!s) throw new Error(`${v} needs a value`); @@ -238,6 +242,7 @@ const HELP = ` --left-selector , --right-selector That side's selector, over --selector. --mask Paint this selector over on both sides, every route; repeatable. + --hide Hide this CSS selector on both sides, every route; repeatable. --mask-media Mask every iframe on every route. --no-freeze-video Leave videos playing and video.js overlays visible. --storage-state @@ -270,8 +275,8 @@ const HELP = ` Default widths: 1440, 1152, 896, 704, 375 - Per-route masks, hidden selectors, blocked URLs and route lists go in a config file; CLI flags - win over it. See https://github.com/tmashara/renderdiff#readme + Per-route masks, blocked URLs and route lists go in a config file; CLI flags win over it + or add to it. See https://github.com/tmashara/renderdiff#readme `; const usage = () => { diff --git a/src/config.ts b/src/config.ts index bbc7ef2..f9eb962 100644 --- a/src/config.ts +++ b/src/config.ts @@ -568,6 +568,8 @@ export interface CliOverrides { aria?: boolean; /** From --mask, appended to the config's masks. */ masks?: string[]; + /** From --hide, appended to the config's hideSelectors. */ + hideSelectors?: string[]; maskMedia?: boolean; freezeVideo?: boolean; } @@ -593,7 +595,11 @@ export function resolveConfig(file: RenderdiffConfig, cli: CliOverrides): Resolv ? { left: file.storageState, right: file.storageState } : file.storageState; - const hideSelectors = [...DEFAULT_HIDE_SELECTORS, ...(file.hideSelectors ?? [])]; + const hideSelectors = [ + ...DEFAULT_HIDE_SELECTORS, + ...(file.hideSelectors ?? []), + ...(cli.hideSelectors ?? []), + ]; const freezeVideo = cli.freezeVideo ?? file.freezeVideo ?? true; return { @@ -607,7 +613,8 @@ export function resolveConfig(file: RenderdiffConfig, cli: CliOverrides): Resolv hideSelectors, neutralizeCss: STILL_CSS + - `${hideSelectors.join(",")}{display:none!important}\n` + + // One rule each, so an invalid selector drops only its own rule. + hideSelectors.map((s) => `${s}{display:none!important}\n`).join("") + // visibility, not display, so the player keeps its layout. (freezeVideo ? ".vjs-text-track-display,.vjs-control-bar,.vjs-control-bar *{visibility:hidden!important}\n" diff --git a/tests/unit/cli-args.test.ts b/tests/unit/cli-args.test.ts index 894fe9d..b5c617f 100644 --- a/tests/unit/cli-args.test.ts +++ b/tests/unit/cli-args.test.ts @@ -153,6 +153,18 @@ describe("flag validation", () => { ); }); + test("--hide needs a value", async () => { + await rejects(["/a", "--left", L, "--right", R, "--dry-run", "--hide"], "--hide needs a value"); + await rejects( + ["/a", "--left", L, "--right", R, "--dry-run", "--hide="], + "--hide needs a value", + ); + await rejects( + ["/a", "--left", L, "--right", R, "--dry-run", "--hide", " "], + "--hide needs a value", + ); + }); + test("--width rejects non-numbers", () => rejects( ["/a", "--left", L, "--right", R, "--width", "wide"], diff --git a/tests/visual/guardrails.test.ts b/tests/visual/guardrails.test.ts index 4b95991..ec5ff90 100644 --- a/tests/visual/guardrails.test.ts +++ b/tests/visual/guardrails.test.ts @@ -604,6 +604,16 @@ describe("selector masks", () => { assert.deepEqual(parseReport(run.report()!).maskReasons, ["[data-drift] (--mask)"]); }); + test("--hide hides a selector on every route, and a bad one drops only its own rule", async () => { + const run = await capture(["/drifting"], { + extra: ["--no-recheck", "--hide", "text=nope", "--hide", "[data-drift]"], + }); + assert.equal(run.code, 0, context(run)); + const res = results(run.outDir); + assert.equal(result(res, "/drifting", 1440).diffPixels, 0, context(run)); + for (const s of ["text=nope", "[data-drift]"]) assert.ok(res.meta.hideSelectors.includes(s)); + }); + test("--mask adds to the config's masks", async () => { const run = await capture(["/drifting"], { config: path.join(CONFIGS, "mask-selector.mjs"),