8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

Next.jsのプロジェクトでStorybookのPlay functionをJestで再利用する

Last updated at Posted at 2023-07-11

この記事はニジボックスQiita記事投稿リレーの4日目の記事です🌈

StorybookのPlay functionを利用することでユーザーインタラクション後のコンポーネントの状態をStorybookで表示することができます。
各Storyに定義したPlay functionをJest側も利用することができ、これによってユーザーの挙動を模したシナリオをJestで実行し、その結果をassertできます。

最近、Next.jsを使っているプロジェクトにこのテストを導入したので、その方法を記事にしてみようと思います。

環境

  • Next.js v13.4.7
  • React v18.2.0
  • Storybook v7.0.26

前準備

Button というサンプルコンポーネントを用意しました。(サンプルコードなのであまり細かいところはお気になさらず...)

Button/index.tsx
import { useState } from "react";
import styles from "./index.module.css";

export function Button() {
  const [isClicked, setIsClicked] = useState(false);

  return (
    <>
      {isClicked && <div onClick={() => setIsClicked(false)}>Clicked!</div>}
      <button
        onClick={() => setIsClicked(true)}
        className={styles.button}
        role="button"
      >
        Click Me!
      </button>
    </>
  );
}

button 要素をクリックすると、Clicked!というテキストが表示され、そのテキストをクリックするとテキストは非表示になる、という振る舞いをします。

次にこのコンポーネントのStoriesファイルを用意します。

Button/index.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";

import { Button } from ".";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Normal: Story = {};

Storybookで表示できることが確認できました。
test.gif

Play functionを実装する

次に本題の Play function を実装していきます。
先ほど作成した index.stories.tsxにbutton要素をクリックするというPlay functionを持ったStoryを追加します。

Button/index.stories.tsx
export const ClickButton: Story = {};
ClickButton.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const button = canvas.getByRole("button");
  await userEvent.click(button);
};

これでクリック後の Button Componentの状態を描画できます。
スクリーンショット 2023-07-10 10.02.41.png

JestでPlay functionを再利用する

先ほど実装した Play function を使って、Jest側でassertをします。

少し本題から外れますが、Next.jsを使っているプロジェクトでは next/jest を使うと簡単にJestの設定ができます。

CSS in JSライブラリなどを利用していると、コンパイルのためにJestのconfigに設定が必要なのですが、Next.jsに設定してあれば、それを引き回して利用することができます。

JestでPlay functionを呼び出し、結果をassertする

話を戻して、 Play function をJestから呼び出して、その結果をassertするテストコードを書きます。

Button/index.test.tsx
import { composeStories } from "@storybook/react";
import { act, findByText, render } from "@testing-library/react";
import * as ButtonStories from "./index.stories";
import "@testing-library/jest-dom";

const { ClickButton } = composeStories(ButtonStories);

describe("Button component", () => {
  it("should display clicked text after clicked", async () => {
    const { container } = render(<ClickButton />);
    await act(async () => {
      await ClickButton.play({ canvasElement: container });
    });
    const clickedText = await findByText(container, "Clicked!");
    expect(clickedText).toBeVisible();
  });
});

コードそのままですが、@testing-library/react でrenderしたStoryComponentのHTMLElementをテスト対象のplay 関数の引数に渡して実行し、その結果(Clicked!というテキストが表示されていること)を expect(clickedText).toBeVisible();でassertしています。

とてもシンプルな例ですが、これでPlay functionをjestから呼び出すことができました。

最後に(業務で工夫した話)

これだけだと少し寂しいので、業務で少し工夫した点について書いて終わりにしたいと思います。

例えば、ボタンクリックしてから2秒後に要素を表示する、というように先ほどのComponentに仕様変更が入ったとします。
これをそのまま実行してしまうと、当たり前ですがJestでは2秒後の要素の描画を待たずにassertしてしまうので、テストは失敗します。
この例のようにシンプルであれば、2秒待つ、という処理をテスト側に仕込んでも良いかも知れませんが、もう少し複雑なComponentをテストするとなると、単純にn秒待つ、という処理ではテストがflakyになってしまうことを危惧しました。

幸いJestには useFakeTimers というsetTimeoutsetIntervalのタイマーをmockできる関数が有り、今回はこちらを利用しました。

index.test.tsx
describe("Button component", () => {
  beforeEach(() => {
    // 各テストケース実行前にタイマーをモックする
    jest.useFakeTimers();
  });

  afterEach(() => {
    // 各テストケース実行後にタイマーを通常動作に戻す
    jest.useRealTimers();
  });
  it("should display clicked text after clicked", async () => {
    const { container } = render(<ClickButton />);
    await act(async () => {
      await ClickButton.play({ canvasElement: container });
      jest.advanceTimersByTime(2000); // ここで2秒待っている
    });
    const clickedText = await findByText(container, "Clicked!");
    expect(clickedText).toBeVisible();
  });
});

以上、Next.jsのプロジェクトでStorybookのPlay functionをJest側で再利用する方法についてまとめてみました。

この記事はニジボックス投稿リレー企画、4日目の記事です。
次の投稿は@kalbeekatzさんです!

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?