feat: section-by-section diffing (--sections)#1
Conversation
Add a --sections mode to visual-diff.mjs that localizes where a page
differs instead of only reporting a whole-page number. It auto-detects
each <section> on the local page by its first semantic class, clips the
same selector on both sides via a new captureScreenshot({ selector })
option, and diffs each region independently. The report.md gains a
Section column; per-section PNGs are <section>-{local,live,diff}.png.
A page may override auto-detection with a sections array (selectors or
{selector,label}). Whole-page mode is unchanged.
- capture.mjs: selector clip (scrollIntoView + element screenshot)
- visual-diff.mjs: --sections, auto-detect, per-section report column
- SKILL.md / config.example.json: document --sections + override
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cb7715a38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const cls = [...el.classList].filter((c) => !GENERIC.has(c)); | ||
| if (cls.length) list.push({ selector: '.' + cls[0], label: cls[0] }); |
There was a problem hiding this comment.
Use a real section selector for detected sections
When --sections sees a local section whose first non-section class is a utility/layout token, e.g. <section class="container hero"> or Tailwind <section class="md:py-24 hero">, this emits .container/.md:py-24 as the selector. The former can capture an unrelated first .container elsewhere on the page, and the latter is not a usable unescaped CSS class selector, so the report can pass/fail the wrong region or error before reaching the actual section. Derive a selector for the actual <section> element and escape class names instead of trusting the first remaining token.
Useful? React with 👍 / 👎.
| return page.sections.map((s) => | ||
| typeof s === 'string' ? { selector: s, label: s.replace(/^[.#]/, '') } : s); | ||
| } | ||
| return detectSections(LOCAL_BASE + page.path, vp); |
There was a problem hiding this comment.
Detect live-only sections before passing
In --sections mode this enumerates only the local page, so a live/staging page with an extra banner or section inserted between otherwise matching sections can still report every row as pass: each local selector is captured as an isolated element, which ignores page position and never checks for live-only sections. Add a live-side section scan (or a whole-page guard row) so section mode cannot return exit code 0 when the candidate page contains extra unmatched content.
Useful? React with 👍 / 👎.
| document.querySelectorAll('section').forEach((el) => { | ||
| const cls = [...el.classList].filter((c) => !GENERIC.has(c)); | ||
| if (cls.length) list.push({ selector: '.' + cls[0], label: cls[0] }); |
There was a problem hiding this comment.
Skip hidden sections during auto-detection
This collects every <section> in the DOM, including sections hidden at the current viewport by responsive CSS or feature toggles. In those pages --sections will later call locator(...).screenshot() for a hidden element, which waits for visibility and reports an error even though the hidden section is not part of the rendered page being compared. Filter detection to visible sections for the active viewport before adding them to the section list.
Useful? React with 👍 / 👎.
- Expand GENERIC skip-list to include layout utility tokens (container, wrapper, inner, content, row, col) and filter Tailwind-style responsive prefixes (md:py-24, lg:) via regex so they are never used as selectors - Use CSS.escape() on the chosen class name so special chars (colons, slashes) produce a valid CSS selector; qualify with `section.CLASS` to target the <section> element itself rather than any unrelated .cls node - Skip sections hidden by CSS or feature toggles (display:none / visibility:hidden) during auto-detection — Playwright would timeout waiting for visibility on a hidden element - Add a whole-page guard row at the end of each section-mode pass so live-only sections (inserted banners, extra sections) are caught even when every matched local section passes individually
wpfyorg
left a comment
There was a problem hiding this comment.
All three P2 comments addressed in commit c3b4b3b:
Use a real section selector (line 243): Selector now uses section.CLASS (targets the <section> element directly, not arbitrary DOM nodes) with CSS.escape() so class names containing colons or other special chars produce valid selectors. GENERIC skip-list expanded to include common layout utility tokens (container, wrapper, inner, content, row, col); an isUtility() predicate also rejects Tailwind-style responsive prefixes (md:py-24, lg:, etc.).
Skip hidden sections (line 243): detectSections now checks getComputedStyle for display:none / visibility:hidden before adding a section to the list, so Playwright never attempts to screenshot a hidden element.
Detect live-only sections (line 228): A whole-page guard row is appended after each page's per-section rows. Per-section rows only confirm matched sections; the guard catches banners or extra sections that exist on live but not on local — and would have caused a silent exit-code 0.
…en each diff image Agents tend to report diff percentages without opening the diff images, so they never localize what actually changed. Make the directive imperative and unmissable: report.md now ends with a checklist of the exact diff PNGs to Read (failing rows only), the header explains % shows whether not what, and the console epilogue says the same. SKILL.md step 4 made mandatory.
What
Adds a
--sectionsmode to the visual-diff skill that localizes where a page differs instead of only reporting a whole-page diff number. One tall section can drag a whole-page percentage; this drops to per-section so you can see exactly which region is off.How it works
<section>on the local page by its first semantic class (hero,pricing,faq, …), skipping the genericsectionutility token and de-duping by selector.captureScreenshot({ selector })option (scrolls the element into view to fire reveal animations, then element-screenshots it so both sides are the same region/size).report.mdgains a Section column; per-section PNGs are<section>-local.png/<section>-live.png/<section>-diff.png.⚠️ error("Selector not found") — a signal the ported section never got its semantic class.sectionsarray (plain selectors, or{selector,label}to target a child).Whole-page mode is unchanged.
Usage
Files
scripts/lib/capture.mjs—selectorclip optionscripts/visual-diff.mjs—--sections, auto-detect, per-section report column, shareddiffPairhelperSKILL.md/config.example.json— document--sections+ the overrideTesting
Ran both modes against a live local site (homepage, both bases pointed at localhost):
--sectionsdetected all 11 sections and reported each (all identical / pass); whole-page mode unchanged. Verified inside the repo's ownskills/visual-diff/scripts/layout.