文档
Storybook Docs

Vitest 中的可移植故事

Storybook 现在推荐使用 Vitest 插件 在 Vitest 中测试您的故事,该插件会自动将故事转换为真实的 Vitest 测试(底层使用此 API)。

对于那些更喜欢直接使用可移植故事的用户,此 API 仍然可用,但我们建议使用 Vitest 插件以获得更简化的测试体验。

可移植故事是 Storybook 故事,可以在外部环境中使用,例如 Vitest

通常,Storybook 会自动组合一个故事及其 注解,作为 故事流程 的一部分。当在 Vitest 测试中使用故事时,您必须自己处理故事流程,这就是 composeStoriescomposeStory 函数实现的功能。

正在使用 Next.js 您可以通过安装和设置 @storybook/nextjs-vite 来测试您的 Next.js 故事,该插件会重新导出 vite-plugin-storybook-nextjs 包。

composeStories

composeStories 将处理您指定的组件故事,将每个故事与其必要的 注解 进行组合,并返回一个包含组合后故事的对象。

默认情况下,组合后的故事将使用故事中定义的 args 来渲染组件。您也可以在测试中向组件传递任何 props,这些 props 将覆盖故事 args 中传递的值。

Button.test.tsx
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { composeStories } from '@storybook/your-framework';
 
// Import all stories and the component annotations from the stories file
import * as stories from './Button.stories';
 
// Every component that is returned maps 1:1 with the stories,
// but they already contain all annotations from story, meta, and project levels
const { Primary, Secondary } = composeStories(stories);
 
test('renders primary button with default args', async () => {
  await Primary.run();
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', async () => {
  // You can override props by passing them in the context argument of the run function
  await Primary.run({ args: { ...Primary.args, children: 'Hello world' } });
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

Type

(
  csfExports: CSF file exports,
  projectAnnotations?: ProjectAnnotations
) => Record<string, ComposedStoryFn>

参数

csfExports

(必需)

类型: CSF 文件导出

指定您要组合哪个组件的故事。传递 CSF 文件的 **完整导出集**(不是默认导出!)。例如:import * as stories from './Button.stories'

projectAnnotations

Type: ProjectAnnotation | ProjectAnnotation[]

指定要应用于组合后故事的项目注解。

此参数是为方便起见提供的。您应该更倾向于使用 setProjectAnnotations。有关 ProjectAnnotation 类型的详细信息,请参阅该函数 projectAnnotations 参数。

此参数可用于 覆盖 通过 setProjectAnnotations 应用的项目注解。

Return

Type: Record<string, ComposedStoryFn>

一个对象,其键是故事的名称,值是组合后的故事。

此外,组合后的故事将具有以下属性

Property类型描述
argsRecord<string, any>故事的 args
argTypesArgType故事的 argTypes
idstring故事的 id
parametersRecord<string, any>故事的 parameters
play(context) => Promise<void> | undefined执行给定故事的 play 函数
run(context) => Promise<void> | undefined挂载并执行 play 函数 给定故事
storyNamestring故事的名称
tagsstring[]故事的 tags

composeStory

如果您想为组件组合单个故事,可以使用 composeStory

Button.test.tsx
import { vi, test, expect } from 'vitest';
import { screen } from '@testing-library/react';
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { composeStory } from '@storybook/your-framework';
 
import meta, { Primary as PrimaryStory } from './Button.stories';
 
// Returns a story which already contains all annotations from story, meta and global levels
const Primary = composeStory(PrimaryStory, meta);
 
test('renders primary button with default args', async () => {
  await Primary.run();
 
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', async () => {
  await Primary.run({ args: { ...Primary.args, label: 'Hello world' } });
 
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

Type

(
  story: Story export,
  componentAnnotations: Meta,
  projectAnnotations?: ProjectAnnotations,
  exportsName?: string
) => ComposedStoryFn

参数

story

(必需)

Type: Story export

指定您要组合的故事。

componentAnnotations

(必需)

Type: Meta

故事文件中的默认导出,包含 story

projectAnnotations

Type: ProjectAnnotation | ProjectAnnotation[]

指定要应用于组合后故事的项目注解。

此参数是为方便起见提供的。您应该更倾向于使用 setProjectAnnotations。有关 ProjectAnnotation 类型的详细信息,请参阅该函数 projectAnnotations 参数。

此参数可用于 覆盖 通过 setProjectAnnotations 应用的项目注解。

exportsName

类型:string

您可能不需要这个。因为 composeStory 接受单个故事,所以它无法访问该故事在文件中的导出名称(就像 composeStories 那样)。如果您必须确保测试中的故事名称唯一且无法使用 composeStories,则可以在此处传递故事导出的名称。

Return

Type: ComposedStoryFn

单个 组合后故事

setProjectAnnotations

此 API 应在测试运行之前调用一次,通常在 setup 文件 中。这将确保在调用 composeStoriescomposeStory 时,也会考虑项目注解。

这些是 setup 文件中需要的配置

  • preview 注解:在 .storybook/preview.ts 中定义的注解
  • addon 注解(可选):由 addons 导出的注解
  • beforeAll:在所有测试之前运行的代码(更多信息
setupTest.ts
import { beforeAll } from 'vitest';
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { setProjectAnnotations } from '@storybook/your-framework';
// 👇 Import the exported annotations, if any, from the addons you're using; otherwise remove this
import * as addonAnnotations from 'my-addon/preview';
import * as previewAnnotations from './.storybook/preview';
 
const annotations = setProjectAnnotations([previewAnnotations, addonAnnotations]);
 
// Run Storybook's beforeAll hook
beforeAll(annotations.beforeAll);

有时故事可能需要插件的 decoratorloader 才能正确渲染。例如,插件可以应用一个 decorator,该 decorator 将您的故事包装在必要的路由上下文中。在这种情况下,您必须在项目注解中包含该插件的 preview 导出。请参阅上面示例中的 addonAnnotations

注意:如果插件不自动应用 decorator 或 loader 本身,而是将其导出供您在 .storybook/preview.js|ts 中手动应用(例如,使用来自 @storybook/addon-themeswithThemeFromJSXProvider),那么您无需执行任何其他操作。它们已包含在上面示例的 previewAnnotations 中。

如果您需要配置 Testing Library 的 render 或使用不同的渲染函数,请在 此讨论 中告知我们,以便我们了解您的需求。

Type

(projectAnnotations: ProjectAnnotation | ProjectAnnotation[]) => ProjectAnnotation

参数

projectAnnotations

(必需)

Type: ProjectAnnotation | ProjectAnnotation[]

一组项目 注解(在 .storybook/preview.js|ts 中定义的)或一组项目注解的数组,它们将应用于所有组合后的故事。

Annotations

注解是应用于故事的元数据,例如 argsdecoratorsloadersplay functions。它们可以为特定故事、组件的所有故事或项目中的所有故事定义。

Story pipeline

要预览 Storybook 中的故事,Storybook 会运行一个故事流程,其中包括应用项目注解、加载数据、渲染故事以及播放交互。这是一个简化的流程版本。

A flow diagram of the story pipeline. First, set project annotations. Collect annotations (decorators, args, etc) which are exported by addons and the preview file. Second, compose story. Create renderable elements based on the stories passed onto the API. Third, run. Mount the component and execute all the story lifecycle hooks, including the play function.

但是,当您想在不同的环境中重用故事时,重要的是要理解所有这些步骤构成了一个故事。可移植故事 API 为您提供了在外部环境中重现该故事流程的机制。

1. 应用项目级注解

Annotations come from the story itself, that story's component, and the project. The project-level annotations are those defined in your .storybook/preview.js file and by addons you're using. In portable stories, these annotations are not applied automatically — you must apply them yourself.

👉 For this, you use the setProjectAnnotations API.

2. Compose

通过运行 composeStoriescomposeStory 来准备故事。结果是一个可渲染的组件,它代表了故事的渲染函数。

3. Run

最后,故事可以通过定义 loadersbeforeEach 或在使用 mount 时将所有故事代码放在 play 函数中来准备它们需要的数据(例如,设置一些 mock 或获取数据)。在可移植故事中,所有这些步骤都将在您调用组合后故事的 run 方法时执行。

👉 For this, you use the composeStories or composeStory API. The composed story will return a run method to be called.

Button.test.tsx
import { test } from 'vitest';
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { composeStories } from '@storybook/your-framework';
 
import * as stories from './Button.stories';
 
const { Primary } = composeStories(stories);
 
test('renders and executes the play function', async () => {
  // Mount story and run interactions
  await Primary.run();
});

如果您的 play 函数包含断言(例如 expect 调用),您的测试将在这些断言失败时失败。

Overriding globals

如果您的故事行为因 globals(例如,将文本渲染为英语或西班牙语)而异,您可以通过在组合故事时覆盖项目注解来在可移植故事中定义这些全局值。

Button.test.tsx
import { test } from 'vitest';
import { render } from '@testing-library/react';
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { composeStory } from '@storybook/your-framework';
 
import meta, { Primary as PrimaryStory } from './Button.stories';
 
test('renders in English', async () => {
  const Primary = composeStory(
    PrimaryStory,
    meta,
    { globals: { locale: 'en' } }, // 👈 Project annotations to override the locale
  );
 
  await Primary.run();
});
 
test('renders in Spanish', async () => {
  const Primary = composeStory(PrimaryStory, meta, { globals: { locale: 'es' } });
 
  await Primary.run();
});