🙆‍♀️

Remix, Storybookでフロントエンドのテストを試してみる

2024/10/18に公開

前提条件

node: v20.11.0

Remixセットアップ

プロジェクト作成

npx create-remix@latest

npx create-remix@latest
Need to install the following packages:
create-remix@2.13.1
Ok to proceed? (y) y


 remix   v2.13.1 💿 Let's build a better website...

   dir   Where should we create your new project?
         ./my-remix-app <- 好きなプロジェクト名 ... ①

      ◼  Using basic template See https://remix.run/guides/templates for more
      ✔  Template copied

   git   Initialize a new git repository?
         Yes

  deps   Install dependencies with npm?
         Yes

      ✔  Dependencies installed

      ✔  Git initialized

  done   That's it!

         Enter your project directory using cd ./my-remix-app
         Check out README.md for development and deploy instructions.

         Join the community at https://rmx.as/discord

npm notice
npm notice New minor version of npm available! 10.8.3 -> 10.9.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.0
npm notice To update run: npm install -g npm@10.9.0
npm notice

依存関係のインストール

cd my-remix-app/   ①のプロジェクト名
npm install

動作確認

npm run dev

npm run dev

> dev
> remix vite:dev

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

local: のURLにアクセスする

Storybookセットアップ

Storybookインストール

npx sb init
(エラーで落ちる)

❯ npx sb init
╭──────────────────────────────────────────────────────╮
│                                                      │
│   Adding Storybook version 8.3.6 to your project..   │
│                                                      │
╰──────────────────────────────────────────────────────╯
 • Detecting project type. ✓
Installing dependencies...


up to date, audited 815 packages in 759ms

253 packages are looking for funding
  run `npm fund` for details

7 low severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
 • Adding Storybook support to your "React" app • Detected Vite project. Setting builder to Vite. ✓

  ✔ Getting the correct version of 10 packages
    Configuring Storybook ESLint plugin at .eslintrc.cjs
  ✔ Installing Storybook dependencies
. ✓
Installing dependencies...


up to date, audited 997 packages in 795ms

313 packages are looking for funding
  run `npm fund` for details

7 low severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

attention => Storybook now collects completely anonymous telemetry regarding usage.
This information is used to shape Storybook's roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://storybook.js.org/telemetry

╭──────────────────────────────────────────────────────────────────────────────╮
│                                                                              │
│   Storybook was successfully installed in your project! 🎉                   │
│   To run Storybook manually, run npm run storybook. CTRL+C to stop.          │
│                                                                              │
│   Wanna know more about Storybook? Check out https://storybook.js.org/       │
│   Having trouble or want to chat? Join us at https://discord.gg/storybook/   │
│                                                                              │
╰──────────────────────────────────────────────────────────────────────────────╯

Running Storybook

> storybook
> storybook dev -p 6006 --initial-path=/onboarding --quiet

@storybook/core v8.3.6

=> Failed to build the preview
Error: The Remix Vite plugin requires the use of a Vite config file
    at configResolved (./node_modules/@remix-run/dev/dist/vite/plugin.js:724:15)
    at async Promise.all (index 2)
    at async resolveConfig (file://./node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:66404:3)
    at async getOptimizeDeps (./node_modules/@storybook/builder-vite/dist/index.js:58:2906)
    at async createViteServer (./node_modules/@storybook/builder-vite/dist/index.js:58:3531)
    at async Module.start (./node_modules/@storybook/builder-vite/dist/index.js:58:4345)
    at async storybookDevServer (./node_modules/@storybook/core/dist/core-server/index.cjs:47328:11)
    at async buildOrThrow (./node_modules/@storybook/core/dist/core-server/index.cjs:46581:12)
    at async buildDevStandalone (./node_modules/@storybook/core/dist/core-server/index.cjs:48518:78)
    at async withTelemetry (./node_modules/@storybook/core/dist/core-server/index.cjs:47080:12)

WARN Broken build, fix the error above.
WARN You may need to refresh the browser.

✔ Would you like to help improve Storybook by sending anonymous crash reports? … yes

vite設定

viteのコンフィグファイルを作成

touch vite-sb.config.ts

// vite-sb.config.ts
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    tsconfigPaths(),
  ],
});

.storybook/main.ts でviteのプラグインパスを指定

import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  stories: [
    "../stories/**/*.mdx",
    "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
  ],
  addons: [
    "@storybook/addon-onboarding",
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@chromatic-com/storybook",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {
      builder: {                                 // 追加
        viteConfigPath: './vite-sb.config.ts',   // 追加
      }                                          // 追加
    },
  },
};
export default config;

.storybook/preview.ts を修正してオンボーディング無効化

import type { Preview } from "@storybook/react";

const preview: Preview = {
  parameters: {
    previewTabs: {                             // 追加
      'storybook/docs/panel': { index: -1 },   // 追加
    },                                         // 追加
    options: {                                 // 追加
      showPanel: false,                        // 追加
    },                                         // 追加
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;

動作確認

Storybook起動

npm run storybook

http://localhost:6006 にアクセス

コンポーネント作成からミニマムなテストまで

コンポーネント追加

app/routes/Counter.tsx を新規作成

import { useState } from "react";

interface CounterProps {
  initialValue?: number;
}

export const Counter = ({ initialValue = 0 }: CounterProps) => {
  const [count, setCount] = useState(initialValue);

  const containerStyle = {
    display: "flex",
    alignItems: "center",
    gap: "10px",
    padding: "10px",
    backgroundColor: "#f0f4f8",
    borderRadius: "8px",
    width: "fit-content",
    margin: "20px auto",
  };

  const buttonStyle = {
    fontSize: "1.5rem",
    padding: "5px 10px",
    backgroundColor: "#4CAF50",
    color: "white",
    border: "none",
    borderRadius: "5px",
    cursor: "pointer",
  };

  const displayStyle = {
    fontSize: "1.5rem",
    padding: "5px 15px",
    backgroundColor: "#ffffff",
    color: "#333",
    border: "1px solid #ddd",
    borderRadius: "5px",
    minWidth: "50px",
    textAlign: "center" as const,
  };

  return (
    <div style={containerStyle}>
      <button
        type="button"
        id="plus"
        style={buttonStyle}
        onClick={() => setCount(count + 1)}
      >
        +
      </button>
      <span id="count" style={displayStyle}>{count}</span>
      <button
        type="button"
        id="minus"
        style={buttonStyle}
        onClick={() => setCount(count - 1)}
      >
        -
      </button>
    </div>
  );
};

StoryBookの設定に対象となるパスを追加

.storybook/main.ts

const config: StorybookConfig = {
  stories: [
    "../app/**/*.mdx",   // 追加
    "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)",  // 追加
    "../stories/**/*.mdx",
    "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
  ],

StoryBookのシナリオを追加

app/routes/Counter.stories.ts を追加

// 最低限必要
import type { Meta, StoryObj } from '@storybook/react';

import { Counter } from './Counter';

const meta = {
  title: 'Example/Counter',
  component: Counter,
} satisfies Meta<typeof Counter>;

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

export const Default: Story = {args: { initialValue: 0 }};

// こっからは追加
export const One: Story = {args: { initialValue: 1 }};

StoryBookでコンポーネントテスト実行

npm install

npm install -D @storybook/testing-library @storybook/jest

テストコード追加

app/routes/Counter.stories.ts にテストコードを追加

import * as testingLibrary from '@storybook/testing-library';   // 追加
const { userEvent, within } = testingLibrary;                   // 追加
import { expect } from "@storybook/jest";                       // 追加
import type { Meta, StoryObj } from '@storybook/react';

import { Counter } from './Counter';

const meta = {
  title: 'Example/Counter',
  component: Counter,
} satisfies Meta<typeof Counter>;

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

export const Default: Story = {args: { initialValue: 0 }};

export const One: Story = {args: { initialValue: 1 }};

// 以降を追加
export const Interactions: Story = {
  args: {
    initialValue: 5,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const plusButton = canvas.getByRole('button', { name: '+' });
    const minusButton = canvas.getByRole('button', { name: '-' });
    const countDisplay = canvas.getByText('5');

    // プラスボタンをクリックしてカウントが増えるか確認
    await userEvent.click(plusButton);
    expect(countDisplay.textContent).toBe('6');

    // マイナスボタンをクリックしてカウントが減るか確認
    await userEvent.click(minusButton);
    expect(countDisplay.textContent).toBe('5');
  },
};

テスト実行

StoryBookの画面で結果を確認できる

Discussion