⚠️ 注意!
如果您使用的是 Storybook 7,則需要閱讀此章節。否則,請隨意跳過。
@storybook/testing-react
已被提升為 Storybook 的一級功能。這表示 您不再需要這個套件。相反地,您可以從 @storybook/react
套件匯入相同的工具。此外,composeStories
和 composeStory
的內部機制已進行改進,因此 story 的組成方式更加準確。@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 testing library。在您的 Storybook story 中,您已經定義了元件的各種情境。您也設定了必要的裝飾器(主題、路由、狀態管理等),以確保它們都能正確渲染。當您編寫測試時,您也最終會定義元件的情境,並設定必要的裝飾器。重複執行相同的操作兩次,您會覺得自己花費了太多精力,使得編寫和維護 story/測試變得不像是一種樂趣,而更像是一種負擔。
解決方案
@storybook/testing-react
是一個在您的 React 測試中重複使用 Storybook story 的解決方案。透過在您的測試中重複使用 story,您將擁有一個隨時可以測試的元件情境目錄。來自您的 args 和 裝飾器、story 及其 meta,以及 全域裝飾器,都將由此程式庫組成並以簡單元件的形式返回給您。這樣,在您的單元測試中,您只需選擇要渲染的 story,所有必要的設定都將為您完成。這是實現測試編寫和 Storybook story 編寫之間更好共享性和維護性的關鍵。
安裝
此程式庫應作為您專案的 devDependencies
之一安裝
透過 npm
npm install --save-dev @storybook/testing-react
或透過 yarn
yarn add --dev @storybook/testing-react
設定
Storybook 6 和元件 Story 格式
此程式庫要求您使用 Storybook 6 版本、元件 Story 格式 (CSF) 和 提升的 CSF 註解,這是自 Storybook 6 以來推薦的 story 編寫方式。
基本上,如果您使用 Storybook 6 且您的 story 看起來與此類似,那麼您就準備就緒了!
// 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,
};
全域配置
這是一個可選步驟。如果您沒有 全域裝飾器,則無需執行此操作。但是,如果您有,這是您的全域裝飾器必須套用的步驟。
如果您有全域裝飾器/參數/等,並且希望它們在測試時套用至您的 story,您首先需要設定它。您可以透過新增到或建立 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 接收到設定檔,您需要在您的測試命令中將其作為選項傳遞給 jest
// package.json
{
"test": "react-scripts test --setupFiles ./setupFile.js"
}
使用方式
composeStories
composeStories
將處理您指定的元件中的所有 story,在它們中組合 args/裝飾器,並傳回一個包含組合 story 的物件。
如果您使用組合的 story (例如 PrimaryButton),元件將使用 story 中傳遞的 args 進行渲染。但是,您可以自由地在元件之上傳遞任何 props,這些 props 將覆蓋 story 的 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
如果您希望將其套用於單個 story 而不是所有 story,則可以使用 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: {...})
重複使用 story 屬性
由 composeStories
或 composeStory
傳回的元件不僅可以渲染為 React 元件,而且還具有來自 story、meta 和全域配置的組合屬性。這表示,如果您想存取 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,其中 story 也可以是一個物件。@storybook/testing-react
中支援此功能,但您必須符合其中一個要求
1 - 您的 story 有 render
方法 2 - 或您的 meta 有 render
方法 3 - 或您的 meta 包含 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
的新函數,您可以在其中編寫 story 的自動互動。
在 @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!');
});
批次測試檔案中的所有 story
您也可以使用 test.each 和 composeStories
的組合來執行自動化測試,而不是手動指定逐個測試。以下是從檔案中對所有 story 執行快照測試的範例
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,並提供自動完成功能以輕鬆偵測元件的所有 story
它還提供元件的 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
// ...
}
// ...
}
免責聲明
為了自動接收類型,您的 story 必須是類型化的。請參閱範例
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',
};