Docs
Storybook Docs

Vitest 中的可移植故事

可移植故事是 Storybook stories,可用于外部环境,例如 Vitest

¥Portable stories are Storybook stories which can be used in external environments, such as Vitest.

通常,Storybook 会自动编写一个故事及其 annotations,作为 故事管道 的一部分。在 Vitest 测试中使用故事时,你必须自己处理故事管道,这就是 composeStoriescomposeStory 函数所启用的功能。

¥Normally, Storybook composes a story and its annotations automatically, as part of the story pipeline. When using stories in Vitest tests, you must handle the story pipeline yourself, which is what the composeStories and composeStory functions enable.

此处指定的 API 在 Storybook 8.2.7 及更高版本中可用。如果你使用的是旧版本的 Storybook,则可以升级到最新版本(npx storybook@latest upgrade)以使用此 API。如果你无法升级,你可以使用以前的 API,它使用 .play() 方法而不是 .run(),但其他方面相同。

¥The API specified here is available in Storybook 8.2.7 and up. If you're using an older version of Storybook, you can upgrade to the latest version (npx storybook@latest upgrade) to use this API. If you're unable to upgrade, you can use previous API, which uses the .play() method instead of .run(), but is otherwise identical.

使用 Next.js?你可以通过安装和设置重新导出 vite-plugin-storybook-nextjs 包的 @storybook/experimental-nextjs-vite 来使用 Vitest 测试你的 Next.js 故事。

¥Using Next.js? You can test your Next.js stories with Vitest by installing and setting up the @storybook/experimental-nextjs-vite which re-exports vite-plugin-storybook-nextjs package.

composeStories

composeStories 将处理你指定的组件故事,使用必要的 annotations 编写每个故事,并返回包含编写的故事的对象。

¥composeStories will process the component's stories you specify, compose each of them with the necessary annotations, and return an object containing the composed stories.

默认情况下,组合故事将使用故事中定义的 args 渲染组件。你还可以将任何属性传递给测试中的组件,这些属性将覆盖故事参数中传递的值。

¥By default, the composed story will render the component with the args that are defined in the story. You can also pass any props to the component in your test and those props will override the values passed in the story's args.

Button.test.tsx
import { test, expect } from 'vitest';
import { screen } from '@testing-library/react';
// 👉 Using Next.js? Import from @storybook/nextjs instead
import { composeStories } from '@storybook/react';
 
// 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 play 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>

参数

¥Parameters

csfExports

(必需)

¥(Required)

类型:CSF 文件导出

¥Type: CSF file exports

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

¥Specifies which component's stories you want to compose. Pass the full set of exports from the CSF file (not the default export!). E.g. import * as stories from './Button.stories'

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

¥Type: ProjectAnnotation | ProjectAnnotation[]

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

¥Specifies the project annotations to be applied to the composed stories.

提供此参数是为了方便。你很可能应该改用 setProjectAnnotations。有关 ProjectAnnotation 类型的详细信息可在该函数的 projectAnnotations 参数中找到。

¥This parameter is provided for convenience. You should likely use setProjectAnnotations instead. Details about the ProjectAnnotation type can be found in that function's projectAnnotations parameter.

此参数可用于 override 通过 setProjectAnnotations 应用的项目注释。

¥This parameter can be used to override the project annotations applied via setProjectAnnotations.

返回

¥Return

类型:Record<string, ComposedStoryFn>

¥Type: Record<string, ComposedStoryFn>

对象中的键是故事的名称,值是组合的故事。

¥An object where the keys are the names of the stories and the values are the composed stories.

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

¥Additionally, the composed story will have the following properties:

属性类型描述
argsRecord<string, any>故事的 args
argTypesArgType故事的 argTypes
idstring故事的 ID
参数Record<string, any>故事的 参数
play(context) => Promise<void> | undefined执行给定故事的播放函数
run(context) => Promise<void> | undefined给定故事的 挂载并执行播放函数
storyNamestring故事的名称
tagsstring[]故事的 tags

composeStory

如果你希望为组件编写单个故事,则可以使用 composeStory

¥You can use composeStory if you wish to compose a single story for a component.

Button.test.tsx
import { vi, test, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { composeStory } from '@storybook/react';
 
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

参数

¥Parameters

story

(必需)

¥(Required)

类型:Story export

¥Type: Story export

指定要创作哪个故事。

¥Specifies which story you want to compose.

componentAnnotations

(必需)

¥(Required)

类型:Meta

¥Type: Meta

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

¥The default export from the stories file containing the story.

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

¥Type: ProjectAnnotation | ProjectAnnotation[]

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

¥Specifies the project annotations to be applied to the composed story.

提供此参数是为了方便。你很可能应该改用 setProjectAnnotations。有关 ProjectAnnotation 类型的详细信息可在该函数的 projectAnnotations 参数中找到。

¥This parameter is provided for convenience. You should likely use setProjectAnnotations instead. Details about the ProjectAnnotation type can be found in that function's projectAnnotations parameter.

此参数可用于 override 通过 setProjectAnnotations 应用的项目注释。

¥This parameter can be used to override the project annotations applied via setProjectAnnotations.

exportsName

类型:string

¥Type: string

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

¥You probably don't need this. Because composeStory accepts a single story, it does not have access to the name of that story's export in the file (like composeStories does). If you must ensure unique story names in your tests and you cannot use composeStories, you can pass the name of the story's export here.

返回

¥Return

类型:ComposedStoryFn

¥Type: ComposedStoryFn

单个 编写的故事

¥A single composed story.

setProjectAnnotations

在测试运行之前,应在 安装文件 中调用此 API 一次。这将确保在调用 composeStoriescomposeStory 时,项目注释也会被考虑在内。

¥This API should be called once, before the tests run, typically in a setup file. This will make sure that whenever composeStories or composeStory are called, the project annotations are taken into account as well.

这些是安装文件中所需的配置:

¥These are the configurations needed in the setup file:

  • 预览注释:.storybook/preview.ts 中定义的参数

    ¥preview annotations: those defined in .storybook/preview.ts

  • 插件注释(可选):插件导出的参数

    ¥addon annotations (optional): those exported by addons

  • beforeAll:在所有测试(更多信息)之前运行的代码

    ¥beforeAll: code that runs before all tests (more info)

setupTest.ts
import { beforeAll } from 'vitest';
// 👇 If you're using Next.js, import from @storybook/nextjs
//   If you're using Next.js with Vite, import from @storybook/experimental-nextjs-vite
import { setProjectAnnotations } from '@storybook/react';
// 👇 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 才能正确渲染。例如,插件可以应用装饰器,将你的故事封装在必要的路由上下文中。在这种情况下,你必须在项目注释集中包含该插件的 preview 导出。请参阅上面示例中的 addonAnnotations

¥Sometimes a story can require an addon's decorator or loader to render properly. For example, an addon can apply a decorator that wraps your story in the necessary router context. In this case, you must include that addon's preview export in the project annotations set. See addonAnnotations in the example above.

注意:如果插件没有自动应用装饰器或加载器本身,而是将它们导出以供你在 .storybook/preview.js|ts 中手动应用(例如,从 @storybook/addon-themes 使用 withThemeFromJSXProvider),那么你不需要做任何其他事情。它们已经包含在上面示例的 previewAnnotations 中。

¥Note: If the addon doesn't automatically apply the decorator or loader itself, but instead exports them for you to apply manually in .storybook/preview.js|ts (e.g. using withThemeFromJSXProvider from @storybook/addon-themes), then you do not need to do anything else. They are already included in the previewAnnotations in the example above.

如果你需要配置测试库的 render 或使用不同的渲染函数,请在 此讨论 中告诉我们,以便我们进一步了解你的需求。

¥If you need to configure Testing Library's render or use a different render function, please let us know in this discussion so we can learn more about your needs.

类型

¥Type

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

参数

¥Parameters

projectAnnotations

(必需)

¥(Required)

类型:ProjectAnnotation | ProjectAnnotation[]

¥Type: ProjectAnnotation | ProjectAnnotation[]

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

¥A set of project annotations (those defined in .storybook/preview.js|ts) or an array of sets of project annotations, which will be applied to all composed stories.

注释

¥Annotations

注释是应用于故事的元数据,如 argsdecoratorsloaders播放函数。它们可以为特定故事、组件的所有故事或项目中的所有故事定义。

¥Annotations are the metadata applied to a story, like args, decorators, loaders, and play functions. They can be defined for a specific story, all stories for a component, or all stories in the project.

故事管道

¥Story pipeline

要在 Storybook 中预览你的故事,Storybook 会运行一个故事管道,其中包括应用项目注释、加载数据、渲染故事和播放交互。这是管道的简化版本:

¥To preview your stories in Storybook, Storybook runs a story pipeline, which includes applying project annotations, loading data, rendering the story, and playing interactions. This is a simplified version of the pipeline:

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 为你提供了在外部环境中重新创建该故事管道的机制:

¥When you want to reuse a story in a different environment, however, it's crucial to understand that all these steps make a story. The portable stories API provides you with the mechanism to recreate that story pipeline in your external environment:

1. 应用项目级注释

¥ Apply project-level annotations

注释 来自故事本身、故事的组件和项目。项目级注释是在你的 .storybook/preview.js 文件中和你正在使用的插件中定义的注释。在可移植故事中,这些注释不会自动应用 - 你必须自己应用它们。

¥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.

👉 为此,你可以使用 setProjectAnnotations API。

¥👉 For this, you use the setProjectAnnotations API.

2. 编写

¥ Compose

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

¥The story is prepared by running composeStories or composeStory. The outcome is a renderable component that represents the render function of the story.

3. 运行

¥ Run

最后,故事可以在渲染之前通过定义 loadersbeforeEach 或在使用 mount 时将所有故事代码放在播放函数中来准备所需的数据(例如,设置一些模拟或获取数据)。在可移植故事中,当你调用组合故事的 run 方法时,所有这些步骤都将执行。

¥Finally, stories can prepare data they need (e.g. setting up some mocks or fetching data) before rendering by defining loaders, beforeEach or by having all the story code in the play function when using the mount. In portable stories, all of these steps will be executed when you call the run method of the composed story.

👉 为此,你可以使用 composeStoriescomposeStory API。组合的故事将返回一个要调用的 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';
import { composeStories } from '@storybook/react';
 
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();
});

如果你的播放函数包含断言(例如 expect 调用),则当这些断言失败时,你的测试将失败。

¥If your play function contains assertions (e.g. expect calls), your test will fail when those assertions fail.

覆盖全局变量

¥Overriding globals

如果你的故事基于 globals 的行为不同(例如以英语或西班牙语渲染文本),你可以在编写故事时通过覆盖项目注释来在可移植故事中定义这些全局值:

¥If your stories behave differently based on globals (e.g. rendering text in English or Spanish), you can define those global values in portable stories by overriding project annotations when composing a story:

Button.test.tsx
import { test } from 'vitest';
import { render } from '@testing-library/react';
import { composeStory } from '@storybook/react';
 
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();
});