<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Igor Luchenkov</title>
    <description>The latest articles on DEV Community by Igor Luchenkov (@igrlk).</description>
    <link>https://dev.to/igrlk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1288006%2F2a152062-4b49-4d13-a89d-5a70cf7a10e6.jpeg</url>
      <title>DEV Community: Igor Luchenkov</title>
      <link>https://dev.to/igrlk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/igrlk"/>
    <language>en</language>
    <item>
      <title>Your coding agent can write the UI. It can't see that it broke it.</title>
      <dc:creator>Igor Luchenkov</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:23:27 +0000</pubDate>
      <link>https://dev.to/igrlk/your-coding-agent-can-write-the-ui-it-cant-see-that-it-broke-it-3bi</link>
      <guid>https://dev.to/igrlk/your-coding-agent-can-write-the-ui-it-cant-see-that-it-broke-it-3bi</guid>
      <description>&lt;p&gt;Give a coding agent a failing test and it fixes itself. Give it a stack trace, a type error, a red CI log, and it reads the signal, reasons about the cause, and corrects. That loop is the whole reason agents feel autonomous on the backend.&lt;/p&gt;

&lt;p&gt;On the UI it falls apart.&lt;/p&gt;

&lt;p&gt;The agent edits some CSS, sees nothing, and tells you it "looks great." It has no eyes on the result. A blind reviewer rubber-stamps everything, and an agent reviewing its own UI change with no picture of it is exactly that. So it ships, and you find the regression later: a button that shrank two pixels, a card that reflowed on mobile, a shadow that vanished in dark mode. Nothing your test suite asserts on. Nothing the agent could see.&lt;/p&gt;

&lt;p&gt;I've been building visual testing for this exact gap, and the fix turned out to be boring. &lt;strong&gt;Give the agent the actual diff.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing piece was never a smarter model
&lt;/h2&gt;

&lt;p&gt;Every team I talk to running agents at volume has built the ugly version of this by hand. They stand up a little server just to get a rendered image in front of the agent, because you can't hand a raw PNG to an agent through a pull request and image handling in the PR is painful. They end up maintaining, by hand, the exact review surface the agent needed all along.&lt;/p&gt;

&lt;p&gt;The thing that actually closes the loop is unglamorous. Screenshot the UI in the cloud on every PR, diff it against the baseline, and post a check and comment right on the pull request, the same place your agent already looks when something goes red.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0919781jfihug9tynul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0919781jfihug9tynul.png" alt="A GitHub pull request comment from the ui-verify bot: 25 changed stories, 21 intended and 4 likely regressions, with a link to the visual changeset." width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It starts on the PR: the comment lands with the changed stories already split, so you read the shape of the change without opening a dashboard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two details matter more than the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The diff is cropped to what moved.&lt;/strong&gt; Not a full-page screenshot. Hand a vision model a 1440px-tall page and it will not reliably notice a button's padding shrank. Hand it the crop, baseline and candidate side by side, just the region that changed, and it catches it. That granularity is the difference between an agent that reviews and one that rubber-stamps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8h8lnkbd6mbha3nopkm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8h8lnkbd6mbha3nopkm.png" alt="A UI Verify diff, baseline on the left versus the PR on the right: the Add-to-cart button changed from a filled coral button to a faint outline. The AI judge flags it a high-confidence regression, unrelated to the PR's stated intent." width="800" height="679"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The regression the judge caught: the Add-to-cart button flipped to a faint outline on a page the rating-badge PR never meant to touch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pixels come back inline.&lt;/strong&gt; The agent's vision model looks at the actual crop, not a URL it can't open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent actually does
&lt;/h2&gt;

&lt;p&gt;An &lt;a href="https://uiverify.ai/docs/the-ai-judge" rel="noopener noreferrer"&gt;AI judge&lt;/a&gt; labels each change against the PR's stated intent, intended or regression, with a reason, so the list is already triaged before the agent looks at a single pixel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foja7akfjx6wptt2ehmra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foja7akfjx6wptt2ehmra.png" alt="A UI Verify build dashboard: 29 stories, 9 changed, the AI review splitting them into 2 regressions and 5 intended, each tagged with a confidence." width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The build view: the judge sorts the changed stories into regressions and intended restyles before anyone opens a diff.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the MCP is connected (one command with your project key), the agent has a small set of tools that map onto the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;list_build_stories  { status: "changed" }              -&amp;gt; the stories that moved this build
render_diff_image   { storyId, which: "before_after" } -&amp;gt; before/after, cropped, inline pixels
get_diff            { storyId }                         -&amp;gt; the AI judge's verdict + reasoning + image URLs
accept_build        { ... }                             -&amp;gt; promote the intended changes to new baselines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put together, the whole review runs without you in the middle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent opens the PR and the visual check renders every affected story in the cloud.&lt;/li&gt;
&lt;li&gt;It lists the changed stories and pulls the cropped before/after for each.&lt;/li&gt;
&lt;li&gt;It classifies each one, intended change or real regression against what &lt;em&gt;this PR&lt;/em&gt; set out to do, reading the judge's verdict alongside its own look at the pixels.&lt;/li&gt;
&lt;li&gt;It fixes the regressions, accepts the intended changes as new baselines, and posts a summary comment. The required check flips green before a human opens anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F703dvdk2dbqbyrverrgd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F703dvdk2dbqbyrverrgd.png" alt="A Claude Code terminal: the agent pulls the diff over MCP, decides the Button restyle was intended and the Add-to-cart outline was a regression, edits Button.tsx to restore the fill, re-runs the check, and reports zero regressions." width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Then it moves to the terminal: the agent runs get_diff over MCP, decides intended vs regression, fixes the real one, re-runs, and reports back ready to merge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The step-by-step version, tool by tool, is in &lt;a href="https://uiverify.ai/docs/triage-with-your-agent" rel="noopener noreferrer"&gt;Triage visual changes from your coding agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule I don't let the agent break
&lt;/h2&gt;

&lt;p&gt;A changed story the diff &lt;strong&gt;cannot explain&lt;/strong&gt; is not something to accept or wave off as flake. If a story moved that this branch had no business touching, that's a signal to stop and investigate the baseline, not to rubber-stamp it.&lt;/p&gt;

&lt;p&gt;Accepting the intended changes is the easy 90 percent. Flagging the one that doesn't fit is the entire reason you gave the agent eyes in the first place. An agent that accepts everything is just a faster way to ship a regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this is where visual testing goes
&lt;/h2&gt;

&lt;p&gt;Everything else in the agent loop already closes itself: tests, types, lint, CI. The UI was the one surface that still needed a human to look. Once agents write most of the UI, the reviewer has to reach the change the same way the author did, programmatically, in context, with no human ferrying screenshots between a dashboard and a terminal.&lt;/p&gt;

&lt;p&gt;Give the agent eyes on the diff and that last open loop closes too.&lt;/p&gt;

&lt;p&gt;If you want the ready-made version, the &lt;a href="https://uiverify.ai/skills/triage-visual-changes" rel="noopener noreferrer"&gt;triage-visual-changes skill&lt;/a&gt; packages this whole review into one command your agent runs, and the &lt;a href="https://uiverify.ai/skills" rel="noopener noreferrer"&gt;full skills catalog&lt;/a&gt; has the rest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://uiverify.ai" rel="noopener noreferrer"&gt;UI Verify&lt;/a&gt;, visual testing shaped for coding agents: cloud render, an AI judge whose verdict travels with the diff, and an MCP the agent drives itself. I write about this stuff as I build it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>How I cut my Chromatic bill 10x (works on any visual testing tool)</title>
      <dc:creator>Igor Luchenkov</dc:creator>
      <pubDate>Tue, 04 Aug 2026 21:00:58 +0000</pubDate>
      <link>https://dev.to/igrlk/how-i-cut-my-chromatic-bill-10x-works-on-any-visual-testing-tool-28an</link>
      <guid>https://dev.to/igrlk/how-i-cut-my-chromatic-bill-10x-works-on-any-visual-testing-tool-28an</guid>
      <description>&lt;p&gt;I have been a huge Storybook and Chromatic fan for years. But at some point the bill got my attention, and when I looked into why, the fix turned out to be simple. This is the write-up of what I changed. It works on any per-snapshot tool, not just Chromatic.&lt;/p&gt;

&lt;p&gt;First, some backstory on how I got here, because it explains why the cost crept up in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ended up paying for a lot of snapshots
&lt;/h2&gt;

&lt;p&gt;In the past I would build a gigantic end-to-end pipeline that was flaky as hell and made me spend time every week fixing it. It took 40 minutes to run, and when it went red someone would assume it was just flaky, merge the change anyway, and then find out it truly did break the system.&lt;/p&gt;

&lt;p&gt;So I stopped writing lots of E2Es and moved to Storybook for interaction and visual testing. Much better. But because I was rendering every state of every component as its own story to get the screenshots in place, I was generating a lot of screenshots. And every snapshot tool, Chromatic, Percy, Playwright screenshots, UI Verify, renders and bills per story. So the number of stories is the cost, and it is also the noise surface: more stories means more places for a diff to flake.&lt;/p&gt;

&lt;p&gt;I ended up paying a lot, which made me think about whether there were ways to optimise it. There were. Here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: combine states into one story
&lt;/h2&gt;

&lt;p&gt;The naive pattern is one story per variant times state times theme. A component with 5 sizes, 3 states, and 2 themes is &lt;strong&gt;30 snapshots&lt;/strong&gt; the naive way. The whole idea below is to collapse that matrix into a handful of stories while keeping full coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move 1: one gallery story, not N stories
&lt;/h3&gt;

&lt;p&gt;For something like a Button, there is no need to have separate Primary, Secondary, and Tertiary stories. I prefer one &lt;code&gt;AllVariants&lt;/code&gt; story that maps through the prop combinations and renders them in a grid. One snapshot then covers the entire matrix. As a bonus you get a nice grid that shows every permutation at a glance, with no extra clicks to see the variations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Button.stories.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ONE story covers every size x variant&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AllVariants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gridTemplateColumns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repeat(3, max-content)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;danger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 9 buttons, 1 snapshot instead of 9.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Move 2: drive states from data, not from N stories
&lt;/h3&gt;

&lt;p&gt;Same idea for a component that loads data. Instead of a &lt;code&gt;Loading&lt;/code&gt;, &lt;code&gt;Empty&lt;/code&gt;, and &lt;code&gt;Error&lt;/code&gt; story each, I add an optional prop like &lt;code&gt;storybookData&lt;/code&gt; and pass the response shape straight in. Then one &lt;code&gt;AllStates&lt;/code&gt; story maps through the shapes and renders the component in each state. Deterministic input in, deterministic pixels out, many states, few shots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// List.stories.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empty&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;many&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;many&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AllStates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;CASES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h4&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tradeoff: say it out loud
&lt;/h2&gt;

&lt;p&gt;Consolidating into one snapshot is coarser granularity. A diff anywhere in the gallery flags the whole story, and you lose the per-variant baseline and accept. So this is a balance, not merge everything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt; variants that change together and share one component: the size, state, and theme matrix of one Button. This is where consolidation wins cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep separate&lt;/strong&gt; genuinely independent surfaces: different pages, different flows, things that evolve on their own timelines. Collapsing those just couples unrelated diffs.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Rule of thumb: merge a single component's own matrix, keep independent pages and flows apart. A tool with per-region diffing softens the coarseness because it can tell you which cell in the gallery moved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Applying these two moves across the codebase easily reduced the number of stories I had by roughly 10x. On the components with the worst explosion the win is even bigger: a size times variant times theme matrix plus four data states can go from 28 stories to 2 snapshots, about 93% fewer billed shots, with the same coverage.&lt;/p&gt;

&lt;p&gt;Same pixels under test, a fraction of the bill, and fewer places to flake. If you are actively writing stories and do not want to burn money, this is the cheapest change you can make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it automatic
&lt;/h2&gt;

&lt;p&gt;I got tired of remembering to do this by hand, so I packaged the whole pattern as a coding-agent skill. Install it and your agent writes stories the economical way from the start, and refactors the exploding ones for you. It is free, and it is tool-agnostic: it helps whether you are on Chromatic, Percy, Playwright screenshots, or UI Verify.&lt;/p&gt;

&lt;p&gt;Grab it here: &lt;a href="https://uiverify.ai/skills/economical-visual-tests" rel="noopener noreferrer"&gt;Write economical visual tests&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about visual testing while building &lt;a href="https://uiverify.ai" rel="noopener noreferrer"&gt;UI Verify&lt;/a&gt;. This post was originally published &lt;a href="https://uiverify.ai/blog/cut-chromatic-bill-10x" rel="noopener noreferrer"&gt;on my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>storybook</category>
      <category>react</category>
    </item>
  </channel>
</rss>
