文档
Storybook 文档

Vitest 中的可移植故事

可移植故事是 Storybook 的故事,可以在外部环境(如 Vitest)中使用。

通常,Storybook 会将故事及其注解作为故事管道的一部分自动组合。当在 Vitest 测试中使用故事时,您必须自己处理故事管道,这就是composeStoriescomposeStory 函数起作用的地方。

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

使用 Next.js您可以使用 Vitest 测试您的 Next.js 故事,方法是安装并设置 @storybook/experimental-nextjs-vite,它会重新导出 vite-plugin-storybook-nextjs 包。

composeStories

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

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

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

类型

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

参数

csfExports

(**必填**)

类型:CSF 文件导出

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

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

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

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

返回值

类型:Record<string, ComposedStoryFn>

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

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

属性类型描述
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';
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();
});

类型

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

参数

story

(**必填**)

类型:Story export

指定您要组合的故事。

componentAnnotations

(**必填**)

类型:Meta

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

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

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

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

exportsName

类型:string

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

返回值

类型:ComposedStoryFn

单个 组合的故事

setProjectAnnotations

此 API 应该只调用一次,在测试运行之前,通常在 设置文件 中。这将确保无论何时调用 composeStoriescomposeStory,项目注释也会被考虑在内。

这些是在设置文件中所需的配置

  • 预览注释:在 .storybook/preview.ts 中定义的那些
  • 附加组件注释(可选):附加组件导出的那些
  • beforeAll:在所有测试之前运行的代码(更多信息
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);

有时,一个故事可能需要附加组件的 装饰器加载器 才能正确渲染。例如,一个附加组件可以应用一个装饰器,将您的故事包装在必要的路由器上下文中。在这种情况下,您必须在设置的项目注释中包含该附加组件的 preview 导出。请参见上面示例中的 addonAnnotations

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

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

类型

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

参数

projectAnnotations

(**必填**)

类型:ProjectAnnotation | ProjectAnnotation[]

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

注释

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

故事管道

为了在 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. 应用项目级注释

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

👉 为此,您使用 setProjectAnnotations API。

2. 组合

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

3. 运行

最后,故事可以通过定义 加载器beforeEach 或在使用 挂载 时将所有故事代码放在 play 函数中,来准备它们所需的数据(例如,设置一些模拟或获取数据)。在可移植的故事中,当你调用组合故事的 run 方法时,所有这些步骤都将被执行。

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

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

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

覆盖全局变量

如果您的故事基于 全局变量(例如,以英语或西班牙语渲染文本)的行为有所不同,则可以通过在组合故事时覆盖项目注释来在可移植的故事中定义这些全局值。

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