--- name: economical-visual-tests description: Author economical visual tests — full visual coverage in the fewest billable snapshots. Applies to any per-snapshot visual tool (UI Verify, Chromatic, Percy, Playwright screenshots), which all bill and diff per rendered story/test. Use when a component's variants/states are exploding into one snapshot per combination, when the visual-testing bill or noise is driven by snapshot count, or when writing new stories/captures and you want the cheapest layout that still covers every variant. Covers gallery/matrix stories, data-driven states, and the coarse-granularity tradeoff. --- # Author economical visual tests Every per-snapshot visual tool — UI Verify, Chromatic, Percy, Playwright screenshots — renders and bills **per story**. So the number of stories *is* the cost and the noise surface. The naive pattern (one story per variant × state × theme) multiplies both fast: a component with 5 sizes × 3 states × 2 themes is **30 snapshots** the naive way. This skill collapses that to a handful while keeping full coverage. The technique is tool-agnostic; the payoff (fewer billed shots, fewer places to flake) is the same everywhere. ## Move 1 — one "all variants" gallery story, not N stories Render the whole matrix **together** in a single story: a grid of every size/state side by side. One snapshot then covers the entire matrix. ```tsx // Button.stories.tsx — ONE story covers every size × variant export const AllVariants = () => (
{(['sm', 'md', 'lg'] as const).flatMap((size) => (['primary', 'secondary', 'danger'] as const).map((variant) => ( )), )}
); ``` 9 buttons, **1 snapshot** instead of 9. ## Move 2 — drive states from data/props, not from N stories For a component that normally *fetches*, don't write a story per response state. Render each state from a fixed fixture — a table of props/mock-responses — in one story. Deterministic input in, deterministic pixels out, many states, few shots. ```tsx const CASES = [ { label: 'empty', items: [] }, { label: 'one', items: [fixtures.one] }, { label: 'many', items: fixtures.many }, { label: 'error', error: 'Failed to load' }, ]; export const AllStates = () => (
{CASES.map((c) => (

{c.label}

))}
); ``` (If the component fetches internally rather than taking props, mock the request instead — see `storybook-visual-testing`, "mock data." The point is the same: fixtures, not N stories.) ## The tradeoff — say it out loud, don't just merge everything Consolidating into one snapshot is **coarser granularity**: a diff *anywhere* in the gallery flags the whole story, and you lose the per-variant baseline/accept. So this is a balance, not "merge everything": - **Merge** variants that change together and share one component — a pure component matrix (sizes, states, themes of one Button). This is where consolidation wins cleanly. - **Keep separate** genuinely independent surfaces — different pages, different flows, things that evolve on their own timelines. Collapsing those just couples unrelated diffs. - UI Verify's AI review + per-region diff soften the coarse-granularity cost (it can tell you *which* cell in the gallery moved), so lean toward consolidation for component matrices. ## Estimate the win When you refactor, state the delta so the value is legible: > This collapses **28 stories → 2 snapshots** (the size×variant×theme matrix into one gallery, the four > data states into one story). ~93% fewer billed shots, same coverage. ## Anti-patterns - **One story per prop combination** → gallery story (move 1). - **One story per fetch state** (`Loading`, `Empty`, `Error`, `Loaded` as four stories) → one data-driven story (move 2). - **Merging independent pages into one snapshot** → over-consolidation; you've coupled unrelated diffs. Keep those separate.