Configure Storybook test-runner for headless CI interaction tests
Step‑by‑step guide to running Storybook interaction tests headlessly in CI using @storybook/test-runner, covering prerequisites, build/serve/run commands, optional Jest config, expected results, and common failure recoveries.
30 Jun 2026, 18:15 UTC

Desired outcome
Run Storybook interaction tests in a CI pipeline using @storybook/test-runner, producing machine‑readable JUnit XML results and failing the build when any play‑function assertion fails.
Prerequisites
- A Storybook project using Component Story Format 3 (CSF3) with at least one story that exports a
playfunction. - Framework‑specific testing library installed (e.g.,
@storybook/testing-reactfor React,@storybook/testing-vue3for Vue 3). - CI runner with Node.js ≥ 18 and a headless browser available (Chromium via
playwrightorpuppeteeris typical). - Storybook builds successfully with
build-storybook.
Focused procedure
- Install the test runner in the project root:
npm install --save-dev @storybook/test-runner - Add a script to
package.jsonthat builds Storybook, serves the static output, then runs the test runner against the served URL:{ "scripts": { "build-storybook": "build-storybook", "serve-storybook": "npx http-server storybook-static -p 6006", "test:storybook": "test-runner --url http://127.0.0.1:6006" } }Adjust the port if your CI environment reserves 6006.
- Run the pipeline steps in order (example for GitHub Actions):
- name: Install dependencies run: npm ci - name: Build Storybook run: npm run build-storybook - name: Start static server run: npm run serve-storybook & env: PORT: 6006 - name: Wait for server run: sleep 5 && curl -f http://127.0.0.1:6006 - name: Run interaction tests run: npm run test:storybookThe
&backgrounds the server; thecurlcheck ensures the server is ready before the test runner connects.
Configuration (optional)
Create test-runner-jest.config.js at the project root to extend Jest defaults. Common adjustments:
- Increase timeout for slow play functions:
module.exports = { testTimeout: 60000 }; - Add a JUnit reporter for CI dashboards:
module.exports = { reporters: [ 'default', ['jest-junit', { outputDirectory: '.', outputName: 'storybook-junit.xml' }] ] };Install
jest-junitas a dev dependency if you use this reporter.
Expected checks
- The
test-runnerprocess exits with code0when every play function passes. - Console output lists each story ID that contains a play function (e.g.,
Button--primary). - JUnit XML (if configured) contains one
<testcase>per story with a play function; failed assertions appear as<failure>elements.
Recovery options
| Symptom | Cause | Remedy |
|---|---|---|
browser not found or Executable doesn't exist | No headless browser installed in CI image | Add a browser installation step (e.g., npx playwright install --with-deps chromium) or use a CI image that bundles Chromium. |
| Timeout errors (default 30 s) | Play function exceeds Jest timeout | Raise testTimeout in test-runner-jest.config.js or split heavy stories into separate test suites via --testPathPattern. |
Connection refused to http://127.0.0.1:6006 | Static server not started or wrong port | Verify the serve-storybook command runs and the port matches the --url argument. |
| Zero tests discovered | No stories export a play function or build output missing | Confirm build-storybook produced storybook-static/ and that at least one story defines play. |
Limitations
- The test runner executes play functions in a real browser; it does not replace unit‑test mocks for network requests, timers, or module boundaries. Use MSW or similar for API mocking inside play functions.
- Storybook 8 changed default test‑runner behavior and introduced portable stories; verify package compatibility (
@storybook/test-runner≥ 0.19 for Storybook 8) before upgrading. - Serving the static build is a separate step; omitting it causes immediate connection‑refused failures.
Verification steps
- Local dry‑run: Start Storybook dev server (
npm run storybook), then in another terminal run:
Confirm all stories with play functions are discovered and pass.npx test-runner --url http://127.0.0.1:6006 - Failure injection: Temporarily add a failing assertion in a play function (e.g.,
await expect(screen.getByText('missing')).toBeInTheDocument()). Re‑run the command; it should exit non‑zero and the JUnit report (if enabled) marks that story as failed. - JUnit inspection: Open the generated
storybook-junit.xmland verify:- Total
<testcase>count equals the number of stories that have a play function. - Each
nameattribute matches the Storybook story ID (e.g.,Button--primary).
- Total
Once these checks pass locally, the same command sequence can be promoted to the CI configuration with confidence.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.