{"id":"l-ds-4-4","courseId":"design-systems","lang":"en","kind":"lesson","summary":"Turn Storybook stories into the test suite for every component state, catch unintended visual changes with screenshot comparison across themes, and run releases through a CI pipeline that checks before it publishes.","html":"<p>Northwind’s 4.2.0 release changed one line: the <code dir=\"ltr\">line-height</code> token for body text, from 1.5 to 1.45. It looked fine in every story we checked. It also pushed the last row of the dispatch app’s route table below the fold on the most common laptop resolution, in compact density, in dark mode. Nobody had looked at that combination. Since then, the rule has been simple: machines look at every combination; humans look at the differences.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Stories are test cases</h2>\n<p>A Storybook story renders a component in one state with specific props. You wrote stories for documentation in the last section; the same stories are the backbone of testing:</p>\n<ul>\n<li><strong>Rendering:</strong> every story must render without errors.</li>\n<li><strong>Interaction:</strong> a story can have a <code dir=\"ltr\">play</code> function that clicks, types and asserts, such as “pressing Escape closes the Dialog and returns focus to the trigger”.</li>\n<li><strong>Accessibility:</strong> the accessibility addon runs axe-core checks against each story and reports violations such as missing labels or failing contrast.</li>\n<li><strong>Visual:</strong> each story is screenshotted and compared with an approved baseline.</li>\n</ul>\n<p>Recent Storybook versions can run stories as tests through a Vitest integration (<code dir=\"ltr\">@storybook/addon-vitest</code>), so the same files run in the Storybook UI, in the terminal and in CI. Northwind requires one story per cell of the state matrix from the accessibility lesson. That rule gives you test coverage almost for free, because the work of designing the states was already done.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Visual regression testing</h2>\n<p>A visual regression test renders a story, takes a screenshot and compares it pixel by pixel with a stored baseline. If they differ beyond a threshold, the test fails and shows the diff. A human then decides: if the change was intended, accept it as the new baseline; if not, fix the code.</p>\n<p>Teams typically choose one of two setups. A hosted service such as Chromatic, made by Storybook’s maintainers, renders stories in the cloud and gives reviewers a web UI for accepting diffs. Or you run screenshots yourself with Playwright:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">tests/visual.spec.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { test, expect } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@playwright/test'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> stories</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-button--primary'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-button--danger'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'components-dialog--confirm'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> themes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'light'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'dark'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> densities</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'comfortable'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'compact'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> stories) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> theme</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> themes) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> density</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> densities) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">theme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">density</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">page</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">goto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`/iframe.html?id=${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}&#x26;viewMode=story`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> page.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">evaluate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(([</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">          document.documentElement.dataset.theme </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">          document.documentElement.dataset.density </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> d;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }, [theme, density]);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(page).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveScreenshot</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">theme</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">density</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}.png`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Each story renders in every combination of theme and density, plus brand in Northwind’s real suite, which is how the line-height regression would have been caught. Playwright’s <code dir=\"ltr\">toHaveScreenshot</code> disables CSS animations by default and retries until two consecutive screenshots match, which removes the most common sources of noise.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Baselines from different machines</span></p>\n<p>Fonts and anti-aliasing differ between macOS, Windows and Linux, so a baseline captured on a laptop won’t match a screenshot taken in CI. Generate and compare baselines in one fixed environment, usually the CI container, and let developers update them through CI rather than locally.</p>\n</aside>\n<p>Visual tests also guard the token tiers. Northwind adds a Stylelint rule in the same CI job that rejects any reference to a primitive token, such as <code dir=\"ltr\">var(--nw-blue-600)</code>, inside component styles. The visual suite then confirms that the rule-abiding CSS still renders what designers approved.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">The release pipeline</h2>\n<p>Testing only protects consumers if publishing can’t skip it. Northwind’s pipeline has two halves.</p>\n<p><strong>On every pull request:</strong> install with <code dir=\"ltr\">npm ci</code>, run the token checks (naming, references, contrast), typecheck, lint, unit and story tests, accessibility checks and visual tests, and require a changeset file when a package changed. The pull request can’t merge until all of these pass and a core member approves the visual diffs.</p>\n<p><strong>On main:</strong> the Changesets action collects pending changesets into a “Version Packages” pull request that bumps versions and updates <code dir=\"ltr\">CHANGELOG.md</code>. Merging that pull request runs the publish step:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">.github/workflows/release.yml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Release</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  push</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    branches</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">jobs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  release</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    runs-on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ubuntu-latest</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    permissions</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      contents</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # push version commits and tags</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      pull-requests</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # open the Version Packages pull request</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      id-token</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">write</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # npm trusted publishing</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    steps</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/checkout@v7</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">actions/setup-node@v7</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          node-version</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm ci</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run build</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">run</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm test</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">uses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">changesets/action@v2</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          publish-script</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">npm run release</span></span></code></pre></div>\n<p>Publishing happens last, only from <code dir=\"ltr\">main</code>, only after build and tests. After the packages are out, the same workflow deploys the documentation site and the Storybook build, and the core team imports the released tokens into the Figma library.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Prereleases for risky changes</span></p>\n<p>For a major release, publish prerelease versions such as <code dir=\"ltr\">5.0.0-next.1</code> under a separate npm dist-tag, so <code dir=\"ltr\">npm install</code> doesn’t pick them up by default. Two or three volunteer teams install them a few weeks early and find the migration problems your codemods missed.</p>\n</aside>\n<p>In the exercise, you review a release workflow that can publish broken code. That completes the delivery section; next, you scale the system to forty teams.</p>\n","toc":[{"id":"sec-1","text":"Stories are test cases","level":2},{"id":"sec-2","text":"Visual regression testing","level":2},{"id":"sec-3","text":"The release pipeline","level":2}],"takeaways":["A story is a documented component state, and the same story can drive interaction tests, accessibility checks and visual snapshots.","Visual regression tests compare screenshots against approved baselines, so unintended changes show up as diffs a human accepts or rejects.","Render every component in every theme combination, because the corners you never look at are the ones that break.","A release pipeline checks, then versions, then publishes; nothing is published from a branch that hasn’t passed every check."],"further":[{"title":"Visual testing (Storybook docs, official repository)","url":"https://github.com/storybookjs/storybook/blob/next/docs/writing-tests/visual-testing.mdx"},{"title":"Visual comparisons (Playwright)","url":"https://playwright.dev/docs/test-snapshots"},{"title":"Changesets GitHub Action (official repository)","url":"https://github.com/changesets/action"}],"quiz":[{"q":"<p>A visual test fails on 46 stories after a pull request that changed only the Dialog’s padding token. What should the reviewer do first?</p>","options":[{"text":"Accept all 46 new baselines, since the token change was intentional.","why":"Some of the 46 may be unintended side effects; accepting blindly turns the test into a rubber stamp."},{"text":"Look at which components changed; if anything other than Dialog and things that contain it changed, the token is used more widely than expected.","why":"Correct. Diffs are evidence about the change’s real reach, which is exactly what the reviewer needs to judge."},{"text":"Re-run the tests, since visual tests are usually flaky.","why":"Re-running a consistent 46-story diff produces the same result; flakiness shows up as random, changing failures."},{"text":"Delete the failing stories so the build passes.","why":"Removing coverage hides the problem and the next real regression with it."}],"answer":1},{"q":"<p>Visual tests pass on a developer’s laptop but fail in CI with tiny differences in text edges. What’s the most likely cause and fix?</p>","options":[{"text":"The CI machine is slower; increase the timeout.","why":"Speed doesn’t change anti-aliasing; timeouts affect waiting, not pixels."},{"text":"Screenshots are taken before fonts load; add a fixed one-second wait to every test.","why":"Fixed waits make the suite slower and still flaky; this symptom points to rendering differences between machines."},{"text":"Different operating systems render fonts differently; generate and compare baselines in one fixed environment, such as the CI container.","why":"Correct. Baselines are only comparable when they’re produced by the same browser, OS and fonts."}],"answer":2},{"q":"<p>Where should <code dir=\"ltr\">npm publish</code> run in a design system’s release workflow?</p>","options":[{"text":"After build, tests and visual checks pass, on the main branch, from the versioning step.","why":"Correct. Publishing is the last step and only happens for code that has passed every check and been merged."},{"text":"On every pull request, so reviewers can install the change.","why":"Publishing unreviewed code to the registry puts it in front of every consumer; use preview builds or a prerelease tag instead."},{"text":"Before the tests, so a failing test doesn’t block urgent fixes.","why":"An urgent fix that breaks forty teams is worse than a slightly delayed one."},{"text":"Manually from a maintainer’s laptop after merging.","why":"Local publishing skips CI’s guarantees and depends on one person’s machine and credentials."}],"answer":0}],"exercise":{"kind":"spot-bug","language":"yaml","code":"name: Release\non:\n  pull_request:\n    branches: [main]\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-node@v7\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org\n      - run: npm ci\n      - run: npm publish --workspace @northwind/ui\n      - run: npm run build\n      - run: npm test\n      - run: npm run test:visual","bugLines":[3,18],"title":"Stop the workflow from publishing broken code","prompt":"<p>This GitHub Actions workflow is meant to publish <code dir=\"ltr\">@northwind/ui</code> to npm after changes are merged and tested. As written, it can put untested, unreviewed code in front of all 40 teams. Select the line or lines that cause that.</p>\n","hints":["When does this workflow run? Has the code been reviewed and merged at that point?","Read the steps top to bottom: what happens if the build or the visual tests fail?"],"explanation":"<p>Two lines are wrong.</p>\n<ul>\n<li><strong>Line 3</strong> triggers the workflow on <code dir=\"ltr\">pull_request</code>, so every open pull request publishes, before review or merge. A release workflow should run on <code dir=\"ltr\">push</code> to <code dir=\"ltr\">main</code>.</li>\n<li><strong>Line 18</strong> publishes right after installing, before <code dir=\"ltr\">npm run build</code>, <code dir=\"ltr\">npm test</code> and the visual tests. If any of those fail, the package is already on npm. Publishing must be the last step.</li>\n</ul>\n<p>The fix: <code dir=\"ltr\">on: push: branches: [main]</code>, and move the publish step (or the Changesets action that runs it) after build, tests and visual tests.</p>\n","checkLabels":{}},"words":685,"readMinutes":3}