Jest 中的可移植故事
如果您使用实验性的 CSF 工厂格式,则无需使用可移植故事 API。相反,您可以直接导入和使用您的故事。
可移植故事是 Storybook 的故事,可在外部环境中使用,例如Jest。
通常,Storybook 会自动组合故事及其注解,作为故事管道的一部分。在 Jest 测试中使用故事时,必须自行处理故事管道,这正是composeStories
和composeStory
函数所实现的功能。
此处指定的 API 在 Storybook 8.2.7
及更高版本中可用。如果您使用的是旧版本的 Storybook,可以升级到最新版本 (npx storybook@latest upgrade
) 以使用此 API。如果您无法升级,可以使用以前的 API,它使用 .play()
方法,而不是 .run()
,但在其他方面是相同的。
使用 Next.js
? 在 Next.js 项目中使用 Jest 进行可移植故事测试时,您需要做三件不同的事情
- 配置
next/jest.js
转换器,它将为您处理所有必要的 Next.js 配置。 - 从
@storybook/nextjs
包导入composeStories
或composeStory
(例如import { composeStories } from '@storybook/nextjs'
)。 - 设置内部模块别名以确保框架配置正常工作,并能够模拟和断言它们。
composeStories
composeStories
将处理您指定的组件故事,使用必要的注解组合它们,并返回一个包含组合后故事的对象。
默认情况下,组合后的故事将使用故事中定义的args渲染组件。您也可以在测试中向组件传递任何 props,这些 props 将覆盖故事 args 中传递的值。
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>
一个对象,其键是故事的名称,值是组合后的故事。
此外,组合后的故事将具有以下属性
属性 | 类型 | 描述 |
---|---|---|
args | Record<string, any> | 故事的args |
argTypes | ArgType | 故事的argTypes |
id | string | 故事的 id |
parameters | Record<string, any> | 故事的parameters |
play | (context) => Promise<void> | undefined | 执行给定故事的 play 函数 |
run | (context) => Promise<void> | undefined | 挂载并执行 play 函数的给定故事 |
storyName | string | 故事名称 |
tags | string[] | 故事的tags |
composeStory
如果您希望为组件组合单个故事,可以使用 composeStory
。
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 文件中。这将确保无论何时调用 composeStories
或 composeStory
,也会考虑项目注解。
setup 文件中需要以下配置
- 预览注解:在
.storybook/preview.ts
中定义的那些 - 插件注解(可选):由插件导出的那些
- beforeAll:在所有测试之前运行的代码(更多信息)
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);
有时故事可能需要插件的decorator 或loader 才能正确渲染。例如,插件可以应用一个 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
中)或项目注解集合的数组,这些注解将应用于所有组合的故事。
注解
注解是应用于故事的元数据,例如args、decorators、loaders,以及play 函数。它们可以定义在特定故事、组件的所有故事或项目的所有故事中。
故事管道
为了在 Storybook 中预览故事,Storybook 会运行故事管道,其中包括应用项目注解、加载数据、渲染故事以及播放交互。这是一个简化的管道版本
然而,当您想在不同的环境中重用一个故事时,理解所有这些步骤构成了故事至关重要。可移植故事 API 提供了在您的外部环境中重现该故事管道的机制
1. 应用项目级注解
注解来自故事本身、故事的组件以及项目。项目级注解是定义在您的 .storybook/preview.js
文件中以及您正在使用的插件中的那些。在可移植故事中,这些注解不会自动应用——您必须自行应用它们。
👉 为此,您使用setProjectAnnotations
API。
2. 组合
通过运行composeStories
或composeStory
来准备故事。结果是一个可渲染的组件,它代表了故事的 render 函数。
3. 运行
最后,故事可以在渲染之前通过定义loaders、beforeEach 或者在使用mount 时将所有故事代码放在 play 函数中,来准备所需数据(例如设置一些模拟或获取数据)。在可移植故事中,所有这些步骤将在您调用组合后故事的 run
方法时执行。
👉 为此,您使用composeStories
或composeStory
API。组合后的故事将返回一个待调用的 run
方法。
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
调用),当这些断言失败时,您的测试将失败。
覆盖全局变量
如果您的故事根据全局变量表现不同(例如以英语或西班牙语渲染文本),您可以通过在组合故事时覆盖项目注解,在可移植故事中定义这些全局值
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();
});