Skip to content

Testing โ€‹

Stories can be tested with AAA - Arrange, Act, Assert. The story itself arranges the scene, you add acting and assertions.

Vitest Browser Tests โ€‹

The @storybook/addon-vitest addon runs all your stories through Vitest in a real browser (Playwright), including their play functions, which functions contains the test code.

sh
pnpm exec storybook add @storybook/addon-vitest
sh
npx storybook add @storybook/addon-vitest
sh
yarn storybook add @storybook/addon-vitest
sh
bun x storybook add @storybook/addon-vitest

The wizard wires up a Vitest project in your vite.config.js:

vite.config.js
ts
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vite';

const dirname = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
  // ...
  test: {
    projects: [
      {
        extends: true,
        plugins: [
          storybookTest({
            configDir: path.join(dirname, '.storybook'),
            storybookScript: 'pnpm storybook --no-open',
          }),
        ],
        test: {
          name: 'storybook',
          browser: {
            enabled: true,
            provider: playwright({}),
            headless: true,
            instances: [{ browser: 'chromium' }],
          },
        },
      },
    ],
  },
});

Tag your stories so the addon picks them up โ€” in preview.ts:

.storybook/preview.ts
ts
export default {
  tags: ['vitest'],
}

Run them like any Vitest suite.

Interaction Tests โ€‹

The play function is where a story describes behavior: query the canvas, fire events, assert the result.

field.stories.gts
gts
import { expect, fn } from 'storybook/test';

import Field from './field.gts';

import type { Meta, StoryObj } from 'ember-storybook';

export default {
  title: 'Form/Field',
  component: Field
} satisfies Meta;

export const Default: StoryObj = {
  args: {
    submit: fn()
  },
  play: async ({ canvas, userEvent, args }) => {
    await userEvent.type(canvas.getByRole('textbox', { name: 'Title' }), 'abc');
    await expect(canvas.getByRole('textbox', { name: 'Slug' })).toHaveValue('abc');

    await userEvent.click(canvas.getByRole('button', { name: 'Catalog' }));
    await expect(args.submit).toBeCalled();
  }
};

A play function doubles as living documentation for the interaction โ€” anyone can watch it run in the browser.

Visual Regression โ€‹

Options that play well with this stack, from the slides' toolkit:

Continuous Integration โ€‹

As your storybook is injected as a project in vite.config.ts, you can run vitest from CLI and thus run it on your CI system:

sh
pnpm vitest
sh
npm vitest
sh
yarn vitest
sh
bun vitest

Limits Worth Knowing โ€‹

  • Route stories render in the canvas only; portable rendering of them is not supported (see Route Stories).
  • Switching between a component story and a route story remounts the Ember application โ€” state does not carry over.

Released under the MIT License.