文档
Storybook 文档

Vitest 中的可移植 Story

如果您正在使用实验性的 CSF Factories 格式,则无需使用可移植 story API。相反,您可以直接导入和使用您的 story

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

通常,Storybook 会自动组合 story 及其注释,作为 story 流程的一部分。在 Vitest 测试中使用 story 时,您必须自己处理 story 流程,composeStoriescomposeStory 函数就是为此而设计的。

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

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

composeStories

composeStories 将处理您指定的组件 story,将它们与必要的注释组合起来,并返回一个包含组合 story 的对象。

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

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

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

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

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

返回值

类型:Record<string, ComposedStoryFn>

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

此外,组合 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';
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

指定要组合的 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 文件中。这将确保在调用 composeStoriescomposeStory 时,也会考虑项目注释。

以下是在 setup 文件中需要的配置

  • 预览注释:在 .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);

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

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

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

类型

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

参数

projectAnnotations

(必需)

类型:ProjectAnnotation | ProjectAnnotation[]

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

注释

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

Story 流程

为了在 Storybook 中预览您的 story,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。可移植 story API 为您提供了在外部环境中重新创建该 story 流程的机制

1. 应用项目级注释

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

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

2. 组合

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

3. 运行

最后,story 可以在渲染之前准备它们需要的数据(例如,设置一些模拟或获取数据),方法是定义 加载器beforeEach 或在使用 mount 时将所有 story 代码放在 play 函数中。在可移植 story 中,当您调用组合 story 的 run 方法时,将执行所有这些步骤。

👉 为此,您可以使用 composeStoriescomposeStory API。组合 story 将返回一个要调用的 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 调用),当这些断言失败时,您的测试将失败。

覆盖全局变量

如果您的 story 根据 全局变量的行为有所不同(例如,以英语或西班牙语渲染文本),您可以通过在组合 story 时覆盖项目注释,在可移植 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();
});