42
19

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.

Storybookで完結!UIコンポーネントのインタラクション管理

Last updated at Posted at 2021-12-22

この記事はエイチーム引越し侍 / エイチームコネクトの社員による、Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021 22日目の記事です。

22日は、Storybookにどハマりしてひたすらコンポーネントの作成と管理をしてたら、
プロダクト側の事何もわからなくなった@diaが担当します。

はじめに

Component Story Format (CSF)最新の形式である「CFS3.0」がStorybook 6.4から利用可能となりました。

CSF3.0では新機能のPlay functionsが登場。
見た目だけでなく、コンポーネントのインタラクション管理も可能になっています。

今まで複雑な挙動のコンポーネントを新規で作成する際は、手動で操作して動作確認をしていましたが、CFS3.0ではその必要もなくなります。便利ですね。

動作パターンをコード上に残せるので俗人化の解消としても有用そうです。

更に、作成したストーリーはJestへの転用が可能です。

Play functionsのセットアップ

Create React Appしたプロジェクト + Storybook 6.4以上の環境を前提としています。

Storybook 6.4にするだけではPlay functionsは使えません。まずはアドオンを追加します。
基本的には公式に書いてある通りにやっているだけです。

1. Storybook Addon Interactionsの追加

yarn add -D @storybook/addon-interactions @storybook/jest @storybook/testing-library

2. main.jsの更新

.storybook/main.js
module.exports = {
  addons: ['@storybook/addon-interactions'],
  features: {
    interactionsDebugger: true,
  },
};

3. Storybook再起動

スクリーンショット 2021-12-22 15.40.03.png

addonの項目に「Interactions」が増えていればOKです。

ストーリーにインタラクション追加

簡単なフォームを題材にインタラクションを追加してみます。

sampleform.gif

SampleForm.stories.tsx
import React from "react";
import { ComponentStoryObj, ComponentMeta } from "@storybook/react";
import { within, userEvent, screen } from "@storybook/testing-library";
import { SampleForm } from "./SampleForm";

// CFS3.0ではMetaにtitleが不要になった。
// コンポーネントのディレクトリと同じ階層構造でStorybook側にも表示される。
export default { 
  component: SampleForm,
  render: () => <SampleForm />, // CFS3.0ではrender関数の中に表示したいStoryを記載する。
} as ComponentMeta<typeof SampleForm>;

// CFS3.0ではStoryが関数→オブジェクトに変更された
// オブジェクトになったことで、他Storyを継承して拡張するのが容易となった。
export const Success: ComponentStoryObj<typeof SampleForm> = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByTestId("name"), "田中 太郎", {
      delay: 300, // 文字が入力される間隔の指定
    });
    await userEvent.type(canvas.getByTestId("age"), "20", {
      delay: 300,
    });
    await userEvent.selectOptions(canvas.getByTestId("sex"), "1");
    await screen.findByText("登録");
  },
};

上記では、
"フォームを全て入力すると送信ボタンのテキストが「登録」に変化する"
というストーリーを作成ています。

Play functionsの実行タイミングはストーリーがレンダリングされた直後です。

await screen.findByText("登録");

一連の動作の結果、UI上どうあるべきか定義しておく事で、予期せぬデグレを防ぐこともできます。

スクリーンショット 2021-12-22 18.07.01.png

失敗すると「FAIL」となって教えてくれます。
正直この辺はJestのコンポーネントテスト側との使い分けがいまいち分かっていませんが・・・。

<input data-testid="name" />

getByTestIdの参照先はid属性ではなく、data-testidというカスタムデータ属性の方を参照してるので注意。
コンポーネント側にフックできるカスタムデータ属性やrole属性を追加しておきましょう。

Jestへの転用

先ほど作成したインタラクションをJestのコンポーネントテストに流用できます。

SampleForm.spec.tsx
import React from "react";
import { findByText } from "@testing-library/dom";
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/testing-react";
import * as stories from "./SampleForm.stories";

const { Success } = composeStories(stories);

describe("sampleForm", () => {
  test("全項目入力すると、ボタンのテキストが「登録」となる", async () => {
    const { container } = render(<Success />);
    await Success.play({ canvasElement: container });

    expect(await screen.findByText("登録")).toBeInTheDocument();
  });
});

Storybookで作成したストーリーをcomposeStories経由で取得、play()でPlay functionsの実行が可能です。
Jest側行なっていたフォーム操作の過程をまるごとカット出来ます。

UI管理系~インラタクションまでをStorybookに集約、それらをJestで用いテストをするというのは見通しが良くて良いですね。

補足:エラー系トラブルシューティング備忘録

テスト実行以前にエラーでかなり詰まったので備忘録として対応したことを記しておきます。
誰かの参考になれば幸いです。

Syntax Error

 FAIL  src/App.test.tsx
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

storybook initすると発生します。
これは以下の環境変数を追加することで治りました。

.env
SKIP_PREFLIGHT_CHECK=true

公式ドキュメントのチュートリアルに書いてありました。)

Watch plugin jest-watch-typeahead/filename cannot be found. Make sure the watchPlugins configuration option points to an existing node module.

yarn add --exact jest-watch-typeahead@0.6.5

とりあえず上記を追加したら治りました。
参考:issue

明日

Ateam Hikkoshi samurai Inc.× Ateam Connect Inc. Advent Calendar 2021 22日目の記事は、いかがでしたでしょうか。
明日は @dd511805がTerraform Providerについて書いてくれるようです。

42
19
1

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
42
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?