⚠️ 注意!
如果您使用的是 Storybook 7,则需要阅读本节。否则,可以跳过。
@storybook/testing-react
已提升为 Storybook 的一等功能。这意味着**您不再需要此包**。相反,您可以从 @storybook/react
包中导入相同的实用程序。此外,composeStories
和 composeStory
的内部结构已进行了改进,因此故事的组合方式更加准确。@storybook/testing-react
包将被弃用,因此我们建议您迁移。
请执行以下操作
- 卸载此包
- 更新您的导入
- import { composeStories } from '@storybook/testing-react';
+ import { composeStories } from '@storybook/react';
// OR
- import { setProjectAnnotations } from '@storybook/testing-react';
+ import { setProjectAnnotations } from '@storybook/react';
问题
您正在使用 Storybook 来开发组件,并使用 jest 为其编写测试,很可能还会结合使用 Enzyme 或 React 测试库。在您的 Storybook 故事中,您已经定义了组件的场景。您还设置了必要的装饰器(主题、路由、状态管理等),以使它们都能正确渲染。在编写测试时,您也会最终定义组件的场景,并设置必要的装饰器。通过重复执行相同操作,您会感觉自己花费了太多精力,使编写和维护故事/测试变得不再有趣,反而更像是一种负担。
解决方案
@storybook/testing-react
是一种在 React 测试中重用 Storybook 故事的解决方案。通过在测试中重用您的故事,您可以获得一个可供测试的组件场景目录。此库将组合来自您的 args 和 decorators,以及您的 story 及其 meta,以及 全局装饰器,并在一个简单的组件中返回给您。这样,在您的单元测试中,您只需选择要渲染的故事,所有必要的设置都将为您完成。这是实现测试编写和 Storybook 故事编写之间更好共享和维护的缺失部分。
安装
此库应作为项目的 devDependencies
之一进行安装
通过 npm
npm install --save-dev @storybook/testing-react
或通过 yarn
yarn add --dev @storybook/testing-react
设置
Storybook 6 和组件故事格式
此库要求您使用 Storybook 版本 6、组件故事格式 (CSF) 和 提升的 CSF 注解,这是从 Storybook 6 开始推荐的编写故事的方式。
从本质上讲,如果您使用 Storybook 6 并且您的故事看起来与此类似,那么您就可以开始了!
// CSF: default export (meta) + named exports (stories)
export default {
title: 'Example/Button',
component: Button,
};
const Primary = args => <Button {...args} />; // or with Template.bind({})
Primary.args = {
primary: true,
};
全局配置
这是一个可选步骤。如果您没有 全局装饰器,则无需执行此操作。但是,如果您有,则这是应用全局装饰器的必要步骤。
如果您有全局装饰器/参数等,并希望在测试它们时将其应用于您的故事,则首先需要进行设置。您可以通过添加到或创建 jest 设置文件 来实现此目的。
// setupFile.js <-- this will run before the tests in jest.
import { setProjectAnnotations } from '@storybook/testing-react';
import * as globalStorybookConfig from './.storybook/preview'; // path of your preview.js file
setProjectAnnotations(globalStorybookConfig);
为了使设置文件被拾取,您需要在测试命令中将其作为选项传递给 jest。
// package.json
{
"test": "react-scripts test --setupFiles ./setupFile.js"
}
用法
composeStories
composeStories
将处理您指定的组件中的所有故事,组合其中的所有 args/decorators,并返回一个包含已组合故事的对象。
如果您使用组合的故事(例如 PrimaryButton),则组件将使用故事中传递的 args 进行渲染。但是,您可以自由地在组件之上传递任何 props,这些 props 将覆盖故事的 args 中传递的默认值。
import { render, screen } from '@testing-library/react';
import { composeStories } from '@storybook/testing-react';
import * as stories from './Button.stories'; // import all stories from the stories file
// Every component that is returned maps 1:1 with the stories, but they already contain all decorators from story level, meta level and global level.
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!/i
);
expect(buttonElement).not.toBeNull();
});
test('renders primary button with overriden props', () => {
render(<Primary>Hello world</Primary>); // you can override props and they will get merged with values from the Story's args
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
composeStory
如果您希望将其应用于单个故事而不是所有故事,则可以使用 composeStory
。您需要将 meta(默认导出)也传递进去。
import { render, screen } from '@testing-library/react';
import { composeStory } from '@storybook/testing-react';
import Meta, { Primary as PrimaryStory } from './Button.stories';
// Returns a component that already contain all decorators from story level, meta level and global level.
const Primary = composeStory(PrimaryStory, Meta);
test('onclick handler is called', () => {
const onClickSpy = jest.fn();
render(<Primary onClick={onClickSpy} />);
const buttonElement = screen.getByRole('button');
buttonElement.click();
expect(onClickSpy).toHaveBeenCalled();
});
将项目注释设置为 composeStory
或 composeStories
setProjectAnnotations
用于应用在 .storybook/preview.js
文件中定义的所有全局配置。这意味着如果您的 preview.js 导入某些模拟或您实际上不想在测试文件中执行的其他内容,则可能会产生意外的副作用。如果是这种情况,并且您仍然需要提供一些通常来自 preview.js 的注释覆盖(装饰器、参数等),则可以将其作为 composeStories
和 composeStory
函数的可选最后一个参数直接传递。
composeStories:
import * as stories from './Button.stories'
// default behavior: uses overrides from setProjectAnnotations
const { Primary } = composeStories(stories)
// custom behavior: uses overrides defined locally
const { Primary } = composeStories(stories, { decorators: [...], globalTypes: {...}, parameters: {...})
composeStory:
import * as stories from './Button.stories'
// default behavior: uses overrides from setProjectAnnotations
const Primary = composeStory(stories.Primary, stories.default)
// custom behavior: uses overrides defined locally
const Primary = composeStory(stories.Primary, stories.default, { decorators: [...], globalTypes: {...}, parameters: {...})
重用故事属性
composeStories
或 composeStory
返回的组件不仅可以作为 React 组件进行渲染,还可以包含来自故事、元数据和全局配置的组合属性。这意味着,例如,如果您想访问 args
或 parameters
,则可以这样做。
import { render, screen } from '@testing-library/react';
import { composeStory } from '@storybook/testing-react';
import * as stories from './Button.stories';
const { Primary } = composeStories(stories);
test('reuses args from composed story', () => {
render(<Primary />);
const buttonElement = screen.getByRole('button');
// Testing against values coming from the story itself! No need for duplication
expect(buttonElement.textContent).toEqual(Primary.args.children);
});
CSF3
Storybook 6.4 发布了 CSF 的新版本,其中故事也可以是一个对象。这在 @storybook/testing-react
中受支持,但您必须满足以下条件之一
1 - 您的**故事**具有 render
方法 2 - 或您的**元数据**具有 render
方法 3 - 或您的**元数据**包含 component
属性
// Example 1: Meta with component property
export default {
title: 'Button',
component: Button, // <-- This is strictly necessary
};
// Example 2: Meta with render method:
export default {
title: 'Button',
render: args => <Button {...args} />,
};
// Example 3: Story with render method:
export const Primary = {
render: args => <Button {...args} />,
};
与 play 函数的交互
Storybook 6.4 还引入了一个名为 play
的新函数,您可以在其中编写故事的自动化交互。
在 @storybook/testing-react
中,play
函数不会自动为您运行,而是包含在返回的组件中,您可以根据需要执行它。
请考虑以下示例
export const InputFieldFilled: Story<InputFieldProps> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByRole('textbox'), 'Hello world!');
},
};
您可以像这样使用 play 函数
const { InputFieldFilled } = composeStories(stories);
test('renders with play function', async () => {
const { container } = render(<InputFieldFilled />);
// pass container as canvasElement and play an interaction that fills the input
await InputFieldFilled.play({ canvasElement: container });
const input = screen.getByRole('textbox') as HTMLInputElement;
expect(input.value).toEqual('Hello world!');
});
批量测试来自一个文件的所有故事
您可以通过结合使用 composeStories
和 test.each 来运行自动化测试,而不是手动逐个指定测试。以下是如何对来自一个文件的所有故事进行快照测试的示例
import * as stories from './Button.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [
// The ! is necessary in Typescript only, as the property is part of a partial type
Story.storyName!,
Story,
]);
// Batch snapshot testing
test.each(testCases)('Renders %s story', async (_storyName, Story) => {
const tree = await render(<Story />);
expect(tree.baseElement).toMatchSnapshot();
});
TypeScript
@storybook/testing-react
已准备好用于 TypeScript,并提供自动完成功能,以便轻松检测组件的所有故事。
它还提供组件的 props,就像您在测试中直接使用它们时所期望的那样。
类型推断仅在 tsconfig.json
文件中将 strict
或 strictBindApplyCall
模式设置为 true
的项目中才有可能。您还需要 4.0.0 以上版本的 TypeScript。如果您没有正确的类型推断,这可能是原因。
// tsconfig.json
{
"compilerOptions": {
// ...
"strict": true, // You need either this option
"strictBindCallApply": true // or this option
// ...
}
// ...
}
免责声明
为了自动拾取类型,您的故事必须是类型化的。请参阅示例
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { Button, ButtonProps } from './Button';
export default {
title: 'Components/Button',
component: Button,
} as Meta;
// Story<Props> is the key piece needed for typescript validation
const Template: Story<ButtonProps> = args => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
children: 'foo',
size: 'large',
};