Vitest Browser Mode: Track Component Health
Track component health with Vitest browser mode and Storybook. Set up run history, inspect failures, and spot flaky stories before they slow your team down.

From Test Results to Component Health: Vitest and Storybook
A green test run is helpful. A red one is helpful too - just not in the way people expect. If your design system has hundreds of browser tests, a report that says "3 out of 400 failed" immediately raises practical questions: Which components are affected? Is this a new regression or something we've seen before? Does it happen only sometimes? And when the assertion broke, what did the browser actually render?
The gap usually isn't in the tests themselves. It's in how the results get organized. Test runners report execution units - files, cases, and assertions - while teams want to understand component and story health over time. Vitest browser mode, Storybook's Vitest addon, and a reporting layer like Kinora can connect those views.
This guide explains the approach, shows how to configure reporting, and clarifies what the resulting history can (and can't) tell you. The goal is simple: make failures easier to pin down and investigate, without turning observability into another system nobody has time to maintain.
Why test output is not component health
A test runner's job is to run tests and report whether they passed. Its natural unit is the test case - often tied to a file and a title. That's the right unit for running the suite. It's not always the right unit for answering product and design-system questions.
A single Storybook story might have a render check, an interaction test, and a visual assertion. Meanwhile, one test file can cover multiple stories. When an assertion fails, someone has to translate the test name back into a component, then dig through previous CI runs to figure out whether this is new.
That translation is manageable for small suites. It gets painful fast when a design system ships with hundreds of stories and you run browser tests on every pull request. The cost isn't just reading logs - it's the repeated context switching between CI output, Storybook, the source code, and earlier builds.
Use the story as the unit people recognize
Storybook already gives you a shared vocabulary. Button/Primary is easier for developers, designers, and product teams to react to than something like button.test.tsx plus a case title describing a specific assertion.
A component-oriented view asks a different, more operational question: for each story, what happened in recent runs? It doesn't replace the test runner's report. It adds a view that ties test outcomes back to the thing you're trying to keep healthy.
This distinction matters because stories and tests don't line up one-to-one. A story can have multiple tests, and a test can cover multiple stories. So the reporting layer needs a rule for how to relate them. In this setup, a story's status for a run is the worst status among its associated tests. If the render test passes but the interaction test fails, the story is marked as failing.
That rule is conservative - and it's easy to explain. It also keeps expectations realistic: you get a summary, not a replacement for inspecting the underlying failing test.
Browser tests add context, not just coverage
Vitest browser mode runs tests in a real browser using a provider such as Playwright. Storybook's Vitest addon turns stories into tests, so teams can validate rendered UI and browser behavior instead of relying only on isolated Node-based tests.
That matters because browser behavior includes things a unit test won't always capture: layout details, DOM behavior, browser APIs, and interactions across rendered components. It also tends to produce better debugging artifacts when traces or screenshot diffs are recorded.
For background, see the Vitest Browser Mode documentation. For the Storybook integration, see the Storybook Vitest addon documentation.
How to send Vitest and Storybook results to a component view
Kinora accepts Vitest results through a reporter that runs alongside Vitest's default reporter. The setup stays small: install the package, add it to the reporter list, set a project identifier, and pass an API token when running the tests.
Install the reporter as a development dependency:
pnpm add -D @kinora/vitest-reporter
Then configure it in vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporters: [
'default',
['@kinora/vitest-reporter', {
project: { slug: 'design-system' },
}],
],
},
})
Run the suite with a token in your environment:
KINORA_TOKEN=<token> npx vitest run
Put the token in your CI secret store, not in the repo. The project slug gives uploaded runs a stable destination - so pick a slug you won't hate later when the design system or pipeline changes.
What the reporter includes by default
By default, the reporter uploads browser tests. That's a sensible baseline for a component-health view, since browser tests usually map closely to what a user sees in Storybook.
Node-based unit tests can still be useful, but they're often numerous, fast, deterministic, and not tied to a visible story. Flooding a component dashboard with thousands of them can add noise without improving the questions the dashboard is supposed to answer. If you intentionally want to include everything, set include: 'all' in the reporter options.
With Storybook's Vitest addon, the stories already show up as Vitest tests. The reporter records the story associated with each test (for example, Button/Primary), so you don't need an extra mapping file for the standard Storybook route.
One more practical detail: uploading results doesn't decide whether the test run passes. If the reporting service can't be reached, Vitest still passes or fails based on the test assertions. Keeping reporting separate from outcomes matters in CI - otherwise a temporary reporting outage could accidentally "change the meaning" of your test results.
Keep the reporting boundary explicit
Before you attach reporting everywhere, decide what the component view is supposed to represent. Many teams start with browser-based component validation rather than "every unit test in the repository." A focused signal is easier to interpret and less likely to turn into a dashboard people stop checking.
Start small: one design-system project and one CI workflow. Confirm that the story names that get uploaded match what people use in Storybook, that recent runs show up as expected, and that an outage doesn't break the suite. Then expand once the basics are behaving.
How to read component history and spot regressions
Once runs with stories are uploaded, the project gets a Components page. Stories are grouped by component. Each story shows its latest status, a timeline covering the last 20 runs, and the tests tied to that story.
The latest status answers, "What's broken right now?" The timeline provides context. If a story is failing now but passed in the previous 19 runs, that's a strong sign of a regression. If it alternates between passing and failing, you may be dealing with flakiness.
Still, none of this is proof of cause. A new failure could come from infrastructure changes, browser/dependency updates, a data issue, or a test that became timing-sensitive. And recurring intermittent failures can still be triggered by recent code changes. History helps you prioritize where to look; it doesn't replace investigation.
A simple triage sequence
When a story turns red, try a short sequence before opening every CI log:
- Check the latest status. Confirm which story is marked failing and which tests are linked to it.
- Compare recent runs. Look for a clean "passing → failing" transition, or patterns that repeat (especially intermittency).
- Inspect the failing test. The story view tells you what broke; the test result tells you which assertion failed.
- Open the trace or screenshot diff. Use the artifact to tell the difference between a rendering change, an interaction issue, and an environment problem.
- Compare with the code change. If the failure is new, treat the pull request as the suspect - not the conclusion.
This shifts the conversation from "CI is red" to something more concrete: "The Select/WithOptions story started failing in this run - does the change look like rendering, interaction, or test setup?" It's a better starting point for anyone investigating, human or automated.
Why the worst-status rule is useful - and imperfect
Marking a story as failed if any associated test fails is a practical summary rule. It prevents a passing render test from hiding a broken interaction test. But it also compresses different outcomes into one status, so you still need the underlying test list to understand what actually went wrong.
At its best, this mapping is most precise when each test renders one story. If a test renders multiple stories, a failure can be attributed to all of them. The reporter can tell you which stories were mounted, but it still might not know which one caused the failing assertion. That's the main limitation to keep in mind when a "component is red" situation happens.
How traces and screenshots make failures easier to debug
History can help you spot when a story began failing. What it can't do by itself is explain what happened in the browser. That's where Vitest browser mode's Playwright traces come in (recorded through the Playwright provider).
A basic configuration looks like this:
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
trace: 'retain-on-failure',
},
},
})
With trace: 'retain-on-failure', traces are kept for failing tests rather than for every test. It's a good default when you want diagnostic detail without storing artifacts for the entire suite. Also: keep an eye on storage and retention behavior in your CI and reporting setup as the test volume grows.
The reporter can upload the trace.zip for each failing test. In Kinora, the trace opens in an embedded viewer with DOM snapshots, console output, and network activity. That gives you a path from the component status to browser-level evidence without downloading a CI artifact first.
Use screenshot diffs for visual assertions
Visual assertions add another layer of evidence. When a toMatchScreenshot assertion fails, you can upload the reference image, the actual image, and the diff for side-by-side comparison. Diffs can make small changes - like spacing or padding shifts - much easier to spot than a textual assertion message alone.
A screenshot comparison doesn't tell you whether the change is a defect. It might be intentional, or the capture might be affected by fonts, rendering differences, or unstable test data. The real value is narrowing the investigation: you can see what changed, then decide whether to update the reference or fix the implementation.
Playwright component tests are another path
If your team already uses Playwright's component-testing mount fixture, you don't have to switch to Vitest to get component-level reporting. Playwright component tests can upload like other Playwright tests. The missing piece is the story identity - the link back to the Storybook story that was mounted. An import from the reporting package can record that association:
import { expect, test } from '@kinora/reporter/ct'
test('disabled button is disabled', async ({ mount }) => {
const component = await mount('Button/Disabled')
await expect(component.getByRole('button')).toBeDisabled()
})
Stories from both the Vitest and Playwright component-test paths can show up on the same Components page. That's useful if you already have Playwright in place - you can improve organization and history without rewriting everything just to standardize the runner.
Limits, workflow, and when this approach is useful
This setup tends to work best when browser tests already exist, story names are consistent, and you regularly need to compare failures across CI runs. It's less helpful when there are only a handful of tests, story attribution is inconsistent, or nobody has time to investigate what the dashboard flags.
There are two limitations worth planning around.
First, a test that renders several stories can "count" against each of them. The reporter knows which stories were mounted, but it may not know which specific story caused the failing assertion. If precise attribution matters, aim for one story per test where it's practical. This isn't a demand that tests be microscopic. It's a trade-off: broader tests can cover realistic flows, while narrower tests make failures easier to locate.
Second, Storybook play functions aren't the same as Playwright actions. A trace from a Storybook test can show the rendered story, but it doesn't necessarily expose each play-function step as a sequence of Playwright actions. Don't assume you'll always get the same level of interaction detail as a test written directly with Playwright.
A practical adoption plan
A low-risk rollout looks like this:
- Add the reporter to your existing Vitest configuration instead of replacing the default reporter.
- Upload browser tests first; consider Node tests only if they add useful component context.
- Start with one design system (or one component package) and verify story attribution.
- Enable traces on failure and screenshot artifacts where visual assertions already exist.
- Use the recent run history during normal pull-request triage, not only after major incidents.
- Keep tests focused when accurate story-level attribution is important.
The real test is operational: does the component view reduce the time it takes to figure out what changed and where to inspect? If the team still has to reconstruct the same context manually, you'll likely need to adjust naming, test boundaries, or what information gets uploaded before expanding further.
This same component-health data can also be exposed to coding agents via Kinora's MCP server. Its component_health tool can answer questions like which stories of a given component are failing or flaky, and which tests render them. That gives an agent structured context instead of forcing it to guess component ownership from raw test logs. It's a strong input, not a substitute for reviewing traces, code, and expected behavior.
Conclusion
Vitest and Storybook can produce useful browser tests, but a simple list of passing and failing test cases isn't always the clearest way to see design-system health. Grouping by story adds useful history, helps distinguish regressions from recurring flakiness, and connects failures to traces and screenshot diffs when those artifacts exist.
The trade-off is straightforward: component-level reporting adds context, but accuracy depends on story attribution and test boundaries. Start with browser tests, keep uploads separate from test outcomes, and use the timeline as evidence - not as a final verdict.
If your team already runs Vitest browser tests or Storybook tests in CI, try the reporter on one project first. See whether it helps you understand the next red build faster before rolling it out more broadly. For the original implementation details, see the source article.