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

Jest 中的可移植故事

如果您使用实验性的 CSF 工厂格式,则无需使用可移植故事 API。相反,您可以直接导入和使用您的故事

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

通常,Storybook 会自动组合故事及其注解,作为故事管道的一部分。在 Jest 测试中使用故事时,必须自行处理故事管道,这正是composeStoriescomposeStory 函数所实现的功能。

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

使用 Next.js 在 Next.js 项目中使用 Jest 进行可移植故事测试时,您需要做三件不同的事情

composeStories

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

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

Button.test.tsx
import { test, expect } from '@jest/globals';
import { render, 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', () => {
  render(<Primary />);
  const buttonElement = screen.getByText('Text coming from args in stories file!');
  expect(buttonElement).not.toBeNull();
});
 
test('renders primary button with overridden props', () => {
  // You can override props and they will get merged with values from the story's args
  render(<Primary>Hello world</Primary>);
  const buttonElement = screen.getByText(/Hello world/i);
  expect(buttonElement).not.toBeNull();
});

类型

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

Parameters

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 { jest, test, expect } from '@jest/globals';
import { render, 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';
 
test('onclick handler is called', () => {
  // Returns a story which already contains all annotations from story, meta and global levels
  const Primary = composeStory(PrimaryStory, meta);
 
  const onClickSpy = jest.fn();
  await Primary.run({ args: { ...Primary.args, onClick: onClickSpy } });
 
  const buttonElement = screen.getByRole('button');
  buttonElement.click();
  expect(onClickSpy).toHaveBeenCalled();
});

类型

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

Parameters

story

(必需)

类型:Story export

指定要组合哪个故事。

componentAnnotations

(必需)

类型:Meta

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

projectAnnotations

类型:ProjectAnnotation | ProjectAnnotation[]

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

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

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

exportsName

类型:string

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

返回值

类型:ComposedStoryFn

一个组合后的故事

setProjectAnnotations

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

setup 文件中需要以下配置

  • 预览注解:在 .storybook/preview.ts 中定义的那些
  • 插件注解(可选):由插件导出的那些
  • beforeAll:在所有测试之前运行的代码(更多信息
setupTest.ts
import { beforeAll } from '@jest/globals';
// 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]);
 
// Supports beforeAll hook from Storybook
beforeAll(annotations.beforeAll);

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

注意:如果插件本身不自动应用 decorator 或 loader,而是导出它们供您在 .storybook/preview.js|ts 中手动应用(例如使用 withThemeFromJSXProvider@storybook/addon-themes),那么您无需再做任何事情。它们已经包含在上面的示例中的 previewAnnotations 中。

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

类型

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

Parameters

projectAnnotations

(必需)

类型:ProjectAnnotation | ProjectAnnotation[]

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

注解

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

故事管道

为了在 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 来准备故事。结果是一个可渲染的组件,它代表了故事的 render 函数。

3. 运行

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

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

Button.test.tsx
import { test } from '@jest/globals';
// 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 调用),当这些断言失败时,您的测试将失败。

覆盖全局变量

如果您的故事根据全局变量表现不同(例如以英语或西班牙语渲染文本),您可以通过在组合故事时覆盖项目注解,在可移植故事中定义这些全局值

Button.test.tsx
import { test } from '@jest/globals';
// 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();
});