加入直播:周四,美国东部时间上午 11 点,Storybook 9 发布及 AMA
文档
Storybook Docs

在 Vitest 中使用便携式 stories

如果您正在使用实验性的 CSF 工厂格式 (experimental CSF Factories format),则无需使用便携式 stories API。相反,您可以直接导入和使用您的 stories

便携式 stories 是 Storybook stories,可用于外部环境,例如 Vitest

通常,Storybook 会自动组合一个 story 及其注解 (annotations),作为story 管道 (story pipeline)的一部分。在 Vitest 测试中使用 stories 时,您必须自行处理 story 管道,这就是 composeStoriescomposeStory 函数的作用。

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

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

composeStories

composeStories 将处理您指定的组件 stories,将它们各自与必要的注解 (annotations)组合,并返回一个包含这些组合后 stories 的对象。

默认情况下,组合后的 story 将使用 story 中定义的args来渲染组件。您也可以在测试中将任何 props 传递给组件,这些 props 将覆盖 story 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();
});

类型

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

参数

csfExports

(必需)

类型:CSF 文件导出

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

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

提供此参数是为了方便。您可能应该使用setProjectAnnotations 代替。有关 ProjectAnnotation 类型的详细信息,请参见该函数的projectAnnotations 参数。

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

返回值

类型:Record<string, ComposedStoryFn>

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

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

属性类型描述
argsRecord<string, any>Story 的args
argTypesArgTypeStory 的argTypes
idstringStory 的 id
parametersRecord<string, any>Story 的parameters
play(context) => Promise<void> | undefined执行给定 story 的 play 函数
run(context) => Promise<void> | undefined挂载并执行给定 story 的 play 函数
storyNamestringStory 的名称
tagsstring[]Story 的标签 (tags)

composeStory

如果您希望为一个组件组合单个 story,可以使用 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();
});

类型

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

参数

story

(必需)

类型:Story 导出

指定要组合哪个 story。

componentAnnotations

(必需)

类型:Meta

包含story 的 stories 文件的默认导出。

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

提供此参数是为了方便。您可能应该使用setProjectAnnotations 代替。有关 ProjectAnnotation 类型的详细信息,请参见该函数的projectAnnotations 参数。

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

exportsName

类型:string

您可能不需要此参数。composeStory 只接受单个 story,因此无法访问该 story 在文件中的导出名称(不像 composeStories)。如果您的测试中必须确保 story 名称唯一且无法使用 composeStories,则可以在此处传递 story 的导出名称。

返回值

类型:ComposedStoryFn

一个单独的组合后 story

setProjectAnnotations

此 API 应在测试运行前调用一次,通常在设置文件 (setup file)中。这将确保无论何时调用 composeStoriescomposeStory,都会同时考虑项目注解。

设置文件中需要的配置如下

  • 预览注解:那些定义在 .storybook/preview.ts 中的
  • 插件注解(可选):由插件导出的那些
  • 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);

有时,story 可能需要插件的装饰器 (decorator)加载器 (loader)才能正确渲染。例如,插件可以应用一个将您的 story 包装在必要的路由器上下文中的装饰器。在这种情况下,您必须将该插件的预览导出包含在项目注解集中。请参阅上面示例中的 addonAnnotations

注意:如果插件没有自动应用装饰器或加载器,而是将它们导出供您在 .storybook/preview.js|ts 中手动应用(例如使用 @storybook/addon-themes 中的 withThemeFromJSXProvider),则您无需执行其他任何操作。它们已包含在上面示例的 previewAnnotations 中。

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

类型

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

参数

projectAnnotations

(必需)

类型:ProjectAnnotation | ProjectAnnotation[]

一组项目注解 (annotations)(那些定义在 .storybook/preview.js|ts 中的)或一组项目注解集合的数组,这些注解将应用于所有组合后的 stories。

注解 (Annotations)

注解是应用于 story 的元数据,例如 args装饰器 (decorators)加载器 (loaders)play 函数。它们可以针对特定 story、组件的所有 stories 或项目中的所有 stories 进行定义。

Story 管道 (Story pipeline)

要在 Storybook 中预览您的 stories,Storybook 会运行一个 story 管道,其中包括应用项目注解、加载数据、渲染 story 和执行交互。这是管道的简化版本

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.

然而,当您想在不同的环境中重用 story 时,重要的是要理解所有这些步骤共同构成了一个 story。便携式 stories API 提供了一种机制,让您可以在外部环境中重现该 story 管道

1. 应用项目级别注解

注解来自 story 本身、story 对应的组件以及项目。项目级别的注解是那些定义在 .storybook/preview.js 文件中以及您正在使用的插件中的注解。在便携式 stories 中,这些注解不会自动应用——您必须自己应用它们。

👉 为此,您使用 setProjectAnnotations API。

2. 组合

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

3. 运行

最后,stories 可以在渲染之前准备它们需要的数据(例如设置一些模拟或获取数据),这可以通过定义加载器 (loaders)beforeEach,或者在使用挂载 (mount)时将所有 story 代码放在 play 函数中来实现。在便携式 stories 中,当您调用组合后 story 的 run 方法时,所有这些步骤都将被执行。

👉 为此,您使用 composeStoriescomposeStory API。组合后的 story 将返回一个 run 方法供调用。

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 调用),当这些断言失败时,您的测试将失败。

覆盖全局变量

如果您的 stories 根据全局变量 (globals) 的不同表现不同(例如渲染英文或西班牙文文本),您可以通过在组合 story 时覆盖项目注解来在便携式 stories 中定义这些全局值

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();
});