🤸‍♀️

Storybook Play function とは

2024/03/10に公開

この記事では、Storybook の Play function について話をします。

Play function とは

https://storybook.js.org/docs/writing-stories/play-function

Play functions are small snippets of code executed after the story renders. Enabling you to interact with your components and test scenarios that otherwise required user intervention.
https://storybook.js.org/docs/writing-stories/play-function

Play function は ユーザ操作を含む Story の定義を可能とする機能 と言えると思います。

Story は何かというと、

A story captures the rendered state of a UI component. Developers write multiple stories per component that describe all the “interesting” states a component can support.
https://storybook.js.org/docs/get-started/whats-a-story

コンポーネントがサポートできる ”興味深い状態” のキャプチャです。

これらを合わせて整理すると、Play function は
「コンポーネントがサポートしている、ユーザ操作を含む興味深い状態 の定義」を可能にする機能です。

Only Available という Story にて Play function が使われています。
ここでは、「ユーザがチェックボックスをクリックした後の、ストックのある商品が表示された状態」を Story として定義しています。

A story with play function
(画像のキャプチャ元: https://www.youtube.com/clip/Ugkx9Yn1tl4Nm33eAD6kTD2svqYLq8QA0YP7 3:46時点)

テストで再利用

Play function は、ユーザ操作を含む興味深い状態 を定義できる機能だとすると、
Play function によって定義される状態は、テストにて動作と結果が保証されるべき状態であり、テストケースとしての再利用 に適したものであると言えると思います。

テストの実装上でも、Story を簡単に再利用する仕組みが用意されています。
以下の例では、 React のテストにて Play function を再利用しています。

// テキストボックスに入力をする Story を定義
export const InputFieldFilled: Story<InputFieldProps> = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByRole('textbox'), 'Hello world!');
  },
};
const { InputFieldFilled } = composeStories(stories);

test('renders with play function', async () => {
  // 👇 上記 Story を再利用
  const { container } = render(<InputFieldFilled />);
  await InputFieldFilled.play({ canvasElement: container });

  const input = screen.getByRole('textbox') as HTMLInputElement;
  expect(input.value).toEqual('Hello world!');
});

Play function のテストでの再利用のメリットとしては、視覚的なフィードバックを受けながらテストを書く事が可能になる ことが挙げられます。通常、testing-library 等で記述するテストは、視覚的なフィードバックなしに、トライ & エラーを繰り返しながら実装していく大変な作業です。

実装の詳細は、以下をご参照ください。(上記コードも以下リンク先より引用しています)
https://github.com/storybookjs/testing-react

まとめ

  • Play function は ユーザ操作を含む興味深い状態 を Story として定義することを可能にした
  • テストに再利用可能 であり、Storybook でも、テストでもメリットを享受できる機能
  • 実装は簡単

この記事を読んでいただきありがとうございました!
Happy coding! 🚀

おまけ

Play function を試す中ででつまずいた点を共有します。

Interactions タブに interactions が表示されない

// 🙅🏻: `@testing-library/react` の `within` と `userEvent` を使っていた
import { userEvent, within } from '@testing-library/react';
// 🙆: `@storybook/testing-library` を使う
import { userEvent, within } from '@storybook/testing-library';

参考:
https://stackoverflow.com/questions/75475488/storybook-play-function-doesnt-show-interactions-in-the-bottom-panel

Cannot invoke an object which is possibly 'undefined'.

export const CombinedStories: Story = {
  play: async (context) => {
    // 🙅🏻: Error: Cannot invoke an object which is possibly 'undefined'.
    await FirstStory.play(context);
    await SecondStory.play(context);
    // 🙆
    await FirstStory.play?.(context);
    await SecondStory.play?.(context);
  },
};

参考:
https://github.com/storybookjs/storybook/issues/21969#issuecomment-1647287480

Pass a context when invoking play function of another story

export const CombinedStories: Story = {
  // 🙅🏻
  play: async ({canvasElement}) => {
    // Error: `Pass a context when invoking play function of another story`
    await FirstStory.play?.(canvasElement);
  }
  // 🙆: context をそのまま渡す必要あり
  play: async (context) => {
    await FirstStory.play?.(context);
  }
};

参考:
https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/context-in-play-function.md


If you want to read this article in English, here is the link: https://dev.to/takuyakikuchi/what-is-storybook-play-function-2bmb

GitHubで編集を提案

Discussion