Skip to main content

component-vrt

Focus visual regression testing on ONE component instead of a whole page, so the diff is small enough to read and to hand to a model. Mounts a story from a Playwright component-testing gallery and screenshots only that component (`vlmkit check story`), then loops baseline → diff → fix → re-run to green. Includes copyable gallery reference implementations (vanilla / React / Vue) because Playwright ships no template for the gallery. Use when repairing or restyling a specific component, when a full-page diff is too noisy or cascades, or when a project wants per-component VRT baselines. Not for whole-page comparison (use vrt-visual-diff) or for generating browser tests from a spec (use spec-to-playwright).

Source facts

Repository
mizchi/vlmkit
Last source activity
October 2, 2026 at 08:04
Detected SKILL.md language
English
Stars
29
Forks
3

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
8 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
component-vrt
description
Focus visual regression testing on ONE component instead of a whole page, so the diff is small enough to read and to hand to a model. Mounts a story from a Playwright component-testing gallery and screenshots only that component (`vlmkit check story`), then loops baseline → diff → fix → re-run to green. Includes copyable gallery reference implementations (vanilla / React / Vue) because Playwright ships no template for the gallery. Use when repairing or restyling a specific component, when a full-page diff is too noisy or cascades, or when a project wants per-component VRT baselines. Not for whole-page comparison (use vrt-visual-diff) or for generating browser tests from a spec (use spec-to-playwright).
metadata
{"internal":true}
# component-vrt Repairing one component with a full-page diff is the wrong instrument. The image is large, the diff **cascades** — nudge a header and every row below it reports as changed — and the part under repair is buried in the part that is not. `vlmkit check story` mounts one story and screenshots **only that component**. Measured on this skill's own fixture: 30,448px across three stories against 1,440,000px for the same count of full-viewport shots — **47x smaller** — and a story that did not change stays green when a shared stylesheet does. ## When to use this instead of a page gate | Situation | Use | |---|---| | Restyling / repairing one component | **this skill** | | Component renders differently in two places on one page | `vlmkit check drift component` | | Whole page vs whole page, two current renders | `vrt-visual-diff` | | Natural-language spec → browser test | `spec-to-playwright` | | Edited markup, no reference, want correctness gates | `markup-assist` | ### Two cases where a story diff is the *weaker* instrument Measured over 65 trials ([report](https://github.com/mizchi/vlmkit/blob/main/docs/reports/2026-08-06-component-vs-page-vrt-signal.md)). Both are about `check story` being a **pixel** instrument, while `diff html` also reads computed styles: 1. **A large component with the default threshold.** On a 1258x203 hero, story diffs missed 6 of 12 seeded changes where `diff html` missed 0. A corner-radius change moves ~60 pixels — over 1.6% of a button, 0.02% of that hero, under the 0.5% default. Get a per-story threshold from `vlmkit build gallery`, or compute one; do not reuse the default on a big component. 2. **Sub-perceptual style drift.** A pale-palette shift moved 96% of that hero's pixels by ≤8/255 per channel, which the comparator scores as **0% changed** — no threshold reaches that. `diff html` catches it from the changed `background-image` declaration. `check story` now *reports* it as `sub-perceptual-drift` (a warn, so the verdict is unchanged) rather than letting it read as a clean pass — but the comparator still passes it, so a project that treats tint drift as a regression must promote the rule. So story VRT narrows what a page diff has to catch; **it does not replace it.** Keep a page-level `diff html` in the suite. ## Prerequisite: does the project have a gallery? A **gallery** is one page exposing `window.mount({ story, props })` / `window.unmount()` that renders into `#root`. It is the half of Playwright component testing that lives in the user's repo, and Playwright's docs are explicit that it is framework-specific with **no template to copy**. Check in this order: 1. **A gallery already exists** — look for `window.mount` in the repo, or a `baseURL` in `playwright.config.*` pointing at a gallery/ path. Use it. 2. **No gallery** — copy one from `assets/`. Do not write one from scratch; the templates already handle the two things that break screenshots (awaiting layout, freezing animation). Read [`assets/_gallery-contract.md`](./assets/_gallery-contract.md) first, then: | Stack | Copy | |---|---| | React | `assets/gallery.react.template.tsx` + `assets/gallery.host.html` | | Vue | `assets/gallery.vue.template.ts` + `assets/gallery.host.html` | | No framework / want zero setup | `assets/gallery.vanilla.html` (runs over `file://`) | Drop `.template` from the filename and adjust the `import.meta.glob` to the project's story layout. That glob is the only non-portable line. 3. **No stories either** — `assets/button.story.template.tsx` shows the shape, including the hidden-form pattern for state. 4. **The components are already built and rendered on a page, and you only want VRT over them** — generate the gallery from that page instead of writing one: ```bash vlmkit build gallery dist/index.html --out .vlmkit/gallery ``` It captures each component's markup and the page's CSS, and prints the `check story` commands with a per-story threshold derived from each component's area. The tradeoff: captured markup is frozen, so `props` do nothing and behaviour is not exercised. Take option 2 over this whenever the component's states matter. **Do not add a Playwright version bump for this.** The `mount` *fixture* is 1.62+, but `check story` does not use it — it drives the page-side contract via `page.evaluate`, exactly as the fixture does. Only add `assets/playwright.ct.config.template.ts` if the user also wants behavioural component *specs*. Storybook is **not** drop-in: its iframe renders from a URL query param and exposes no `window.mount`, so `check story` reports `mount-failed`. A shim in `.storybook/preview.js` would bridge it; that is unverified, so say so rather than promising it. ## The loop ```bash G="http://localhost:5173/playwright/gallery/index.html" # or file://$PWD/gallery.vanilla.html # 1. First run writes baselines and reports new-baseline — NOT a pass. vlmkit check story components/Button/Primary --gallery "$G" # 2. Edit the component. Re-run: the diff is the component, at component size. vlmkit check story components/Button/Primary --gallery "$G" # ✗ components/Button/Primary 88x40 15.97% diff (562/3520px) # heatmap: .vlmkit/stories/components-Button-Primary/...heatmap.png # region 0,0 96x64 content # 3. Read the heatmap and the region list, fix, re-run until green. # 4. When the change was intended, approve it: vlmkit check story components/Button/Primary --gallery "$G" --update-baseline ``` Several stories share one browser — list them together rather than looping in the shell: ```bash vlmkit check story components/Button/Primary components/Button/Disabled components/Card/Default --gallery "$G" ``` Useful flags: `--props '{"title":"Hello"}'`, `--viewport 400x300`, `--threshold 0.02`, `--root '#app'`, `--settle 200`, `--out <dir>`. Machine-readable: `--json` gives the shared gate envelope, with region geometry and shift estimates under `findings[].evidence.regions` — read that rather than parsing the prose. ## Reading the outcome Three rules, and the difference between the first two decides what you do next. | Rule | Means | Action | |---|---|---| | `story-drift` | The component changed vs its baseline | The finding you asked for. Read regions, fix, or `--update-baseline` if intended. | | `mount-failed` | **Nothing was measured** — unknown story id, render throw, page is not a gallery | Fix the id or the gallery. **Never** `--rule check.story/mount-failed=off` to reach green: that makes a typo'd story id read as a passing component. | | `sub-perceptual-drift` | The story **passed**, but most of its pixels moved by less than the comparator counts — the signature of a palette / gradient / opacity change, not of antialiasing | Look at it. This is the one blind spot a story diff has that a page `diff html` does not (it reads computed styles). Promote to `suspect` in `vlmkit.gates.json` if your project treats tint drift as a regression. | | `new-baseline` | No baseline existed, so one was written | Re-run to actually compare. Commit the baseline. | ## Done conditions - Every targeted story reports `unchanged`, or its change is approved with `--update-baseline` and the new baseline is committed. - No `mount-failed` findings. - `vlmkit check story <stories> --gallery <url>` exits 0 without `--advisory`. - Any story over ~50,000px carries a derived `--threshold` rather than the default, and a page-level `diff html` still covers what story diffs cannot see (see above). ## Persisting the set Once the stories matter, list them in `vlmkit.gates.json` so CI runs the same set and the story ids stop drifting: ```jsonc { "defaults": { "gates": [ "check story components/Button/Primary --gallery http://localhost:5173/playwright/gallery/index.html", "check story components/Card/Default --gallery http://localhost:5173/playwright/gallery/index.html" ], "rules": { "check.story/new-baseline": "off" } } } ``` That config is also the fix for the one sharp edge: **baselines are keyed on the story id as written.** The gallery owns id resolution and the contract offers no way to ask what an id resolved to, so `Button/Primary` and `components/Button/Primary` get *separate* baselines. Pick one spelling per story and keep it. ## Flake A component-scoped shot removes most page-level flake by construction. What is left, in order of how often it is the cause: 1. **Fonts.** A fallback substituting mid-run reads as real drift. Self-host the fonts the component uses. 2. **Animation.** Freeze it in the gallery's `window.mount` — the templates here already do. `--settle <ms>` is the fallback, not the fix: waiting slows every run and only reduces the flake. 3. **Threshold.** Default is `0.005`, deliberately tighter than a page default: on a 3,520px button a handful of stray pixels is already a percent. Raise it per-story with `--threshold` rather than globally. ## Related - `vlmkit rules check story` — the rule table with default severities. - [`examples/story-gallery/`](https://github.com/mizchi/vlmkit/tree/main/examples/story-gallery) in the vlmkit repo — the vanilla gallery as a runnable project, plus a README with the React + Vite version.
View on GitHub