Play 函式
觀看影片教學
Play
函式是在故事渲染後執行的小程式碼片段。讓您可以與元件互動,並測試原本需要使用者介入的情境。
設定互動擴充功能
我們建議您先安裝 Storybook 的 addon-interactions
,再開始使用 play
函式撰寫故事。它是完美的補充,包括一組方便的 UI 控制項,讓您可以控制執行流程。您可以隨時暫停、繼續、倒回和逐步執行每個互動。同時也為您提供一個易於使用的偵錯工具,以解決潛在的問題。
執行以下命令以安裝擴充功能和必要的相依性。
npm install @storybook/test @storybook/addon-interactions --save-dev
更新您的 Storybook 設定 (在 .storybook/main.js|ts
中) 以包含互動擴充功能。
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
// Other Storybook addons
'@storybook/addon-interactions', // 👈 Register the addon
],
};
export default config;
使用 Play 函式撰寫故事
Storybook 的 play
函式是在故事完成渲染後執行的小程式碼片段。在 addon-interactions
的輔助下,它可以讓您建立元件互動,並測試在沒有使用者介入的情況下不可能實現的情境。例如,如果您正在處理註冊表單並想要驗證它,您可以使用 play
函式撰寫以下故事
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { RegistrationForm } from './RegistrationForm';
const meta: Meta<typeof RegistrationForm> = {
component: RegistrationForm,
};
export default meta;
type Story = StoryObj<typeof RegistrationForm>;
/*
* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emailInput = canvas.getByLabelText('email', {
selector: 'input',
});
await userEvent.type(emailInput, 'example-email@email.com', {
delay: 100,
});
const passwordInput = canvas.getByLabelText('password', {
selector: 'input',
});
await userEvent.type(passwordInput, 'ExamplePassword', {
delay: 100,
});
// See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
const submitButton = canvas.getByRole('button');
await userEvent.click(submitButton);
},
};
請參閱元件測試文件,以了解可用的 API 事件概述。
當 Storybook 完成故事渲染時,它會執行 play
函式中定義的步驟,與元件互動並填寫表單的資訊。所有這些都不需要使用者介入。如果您檢查 Interactions
面板,您會看到逐步的流程。
組成故事
由於採用了以 ES6 模組為基礎的檔案格式 元件故事格式,您也可以結合您的 play
函式,類似於其他現有的 Storybook 功能 (例如,args)。例如,如果您想要驗證元件的特定工作流程,您可以撰寫以下故事
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/*
* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FirstStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('an-element'), 'example-value');
},
};
export const SecondStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('other-element'), 'another value');
},
};
export const CombinedStories: Story = {
play: async ({ context, canvasElement }) => {
const canvas = within(canvasElement);
// Runs the FirstStory and Second story play function before running this story's play function
await FirstStory.play(context);
await SecondStory.play(context);
await userEvent.type(canvas.getByTestId('another-element'), 'random value');
},
};
透過結合這些故事,您正在重新建立整個元件工作流程,並且可以在減少需要撰寫的樣板程式碼的同時,找出潛在的問題。
使用事件
大多數現代使用者介面的建構都著重於互動 (例如,按一下按鈕、選取選項、勾選核取方塊),為終端使用者提供豐富的體驗。透過 play
函式,您可以將相同層級的互動納入您的故事中。
常見的元件互動類型是按鈕點擊。如果您需要在故事中重現它,您可以將故事的 play
函式定義如下
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { fireEvent, userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ClickExample: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'));
},
};
export const FireEventExample: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await fireEvent.click(canvas.getByTestId('data-testid'));
},
};
當 Storybook 載入故事並執行函式時,它會與元件互動並觸發按鈕點擊,類似於使用者會執行的動作。
除了點擊事件外,您也可以使用 play
函式編寫其他事件的腳本。例如,如果您的元件包含具有各種選項的選取器,您可以撰寫以下故事並測試每個情境
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
// Function to emulate pausing between interactions
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleChangeEvent: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const select = canvas.getByRole('listbox');
await userEvent.selectOptions(select, ['One Item']);
await sleep(2000);
await userEvent.selectOptions(select, ['Another Item']);
await sleep(2000);
await userEvent.selectOptions(select, ['Yet another item']);
},
};
除了事件之外,您還可以根據其他類型的非同步方法,使用 play
函式建立互動。例如,假設您正在使用一個實作了驗證邏輯的元件 (例如,電子郵件驗證、密碼強度)。在這種情況下,您可以在 play
函式中引入延遲,以模擬使用者互動,並斷言提供的值是否有效
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const DelayedStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const exampleElement = canvas.getByLabelText('example-element');
// The delay option sets the amount of milliseconds between characters being typed
await userEvent.type(exampleElement, 'random string', {
delay: 100,
});
const AnotherExampleElement = canvas.getByLabelText('another-example-element');
await userEvent.type(AnotherExampleElement, 'another random string', {
delay: 100,
});
},
};
當 Storybook 載入故事時,它會與元件互動、填寫其輸入,並觸發任何已定義的驗證邏輯。
您也可以使用 play
函式來驗證基於特定互動的元素是否存在。舉例來說,如果您正在開發一個元件,並想檢查使用者輸入錯誤資訊時會發生什麼情況。在這種情況下,您可以編寫如下的故事:
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, waitFor, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleAsyncStory: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const Input = canvas.getByLabelText('Username', {
selector: 'input',
});
await userEvent.type(Input, 'WrongInput', {
delay: 100,
});
// See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
const Submit = canvas.getByRole('button');
await userEvent.click(Submit);
await waitFor(async () => {
await userEvent.hover(canvas.getByTestId('error'));
});
},
};
查詢元素
如果您需要,您也可以調整您的 play
函式,以根據查詢(例如,角色、文字內容)查找元素。例如:
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const ExampleWithRole: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// See https://storybook.dev.org.tw/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button', { name: / button label/i }));
},
};
您可以在 Testing Library 文件中閱讀更多關於查詢元素的資訊。
當 Storybook 加載故事時,play
函式會開始執行,並查詢 DOM 樹,期望在故事渲染時該元素可用。如果您的測試失敗,您將能夠快速驗證其根本原因。
否則,如果元件並非立即可用,例如,由於您在 play
函式中定義的前一個步驟或某些非同步行為,您可以調整您的故事並等待 DOM 樹的變更發生,然後再查詢元素。例如:
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
/* See https://storybook.dev.org.tw/docs/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const AsyncExample: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Other steps
// Waits for the component to be rendered before querying the element
await canvas.findByRole('button', { name: / button label/i });
},
};
使用畫布 (Canvas)
預設情況下,您在 play
函式中寫入的每個互動都會從畫布的頂層元素開始執行。這對於較小的元件(例如,按鈕、核取方塊、文字輸入框)是可以接受的,但對於複雜的元件(例如,表單、頁面)或多個故事來說,可能會效率不彰。為了容納這種情況,您可以調整您的互動,使其從元件的根部開始執行。例如:
// Replace your-framework with the name of your framework
import type { Meta, StoryObj } from '@storybook/your-framework';
import { userEvent, within } from '@storybook/test';
import { MyComponent } from './MyComponent';
const meta: Meta<typeof MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const ExampleStory: Story = {
play: async ({ canvasElement }) => {
// Assigns canvas to the component root element
const canvas = within(canvasElement);
// Starts querying from the component's root element
await userEvent.type(canvas.getByTestId('example-element'), 'something');
await userEvent.click(canvas.getByRole('another-element'));
},
};
將這些變更應用到您的故事中,可以透過 addon-interactions
提供效能提升和改進的錯誤處理。