Closed10

Storybookでページコンポーネントを描画するまでのメモ

ryo_kawamataryo_kawamata

Chrome Extensionの開発をしているが、起動までに諸条件があり、全体のUIを確認するために都度起動するのが面倒。なので、Storybook上で全体のUIも確認できるようにしたい。
そのために、描画時、操作時に実行されるAPIリクエストやブラウザ依存のAPIをMockする必要がある。

ryo_kawamataryo_kawamata

調べたら以下が出てきた。

https://storybook.js.org/docs/writing-stories/build-pages-with-storybook#mocking-api-services

ただ、install時に互換性エラーで失敗する。

どうやら、msw-storybook-addonはmswの1系のみ対応しているが、mswの1系はプロジェクトで使われているTypeScriptのバージョン5.3系と互換性がないようだ・・

mswの2系を使えるようにするためのPRもあるがまだwip。

https://github.com/mswjs/msw-storybook-addon/pull/122#event-11629858773

ryo_kawamataryo_kawamata

どうせならmswの2系を使いたいので msw-storybook-addon を使わないで素のmswでなんとかする方法を考える

https://mswjs.io/

を読んでみる。

とりあえず、Storybookを起動した時にmswのservice workerが起動するようにしたい

MSWのスクリプトをpublicディレクトリに配置する必要があるらしい。
なので、Storybookにpublicディレクトリの設定を追加して、そこを指定するようにしてみる

$ mkdir ./storybook/public
.storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
  // ...
  staticDirs: ["./public"],
  // ...
};
export default config;
$ npx msw init .storybook/public  
ryo_kawamataryo_kawamata

Storybookの起動時にmswのworkerのstartを実行したい

decoratorsでできそうだけど、Workerの起動をawaitしたいなー。。どうするのがよいのだろう。
decoratorsはpromiseを返せない気がする

ryo_kawamataryo_kawamata

無理やりだけど、mswのdecoratorを作って、こんな感じにしてみた。

.storybook/decorators/mswDecorator.tsx
import React from "react";
import { worker } from "../../src/mocks/browser";

export const mswDecorator = (Story) => {
  const [isWorkerLoaded, setIsWorkerLoaded] = React.useState(false);

  const loadWorker = async () => {
    if (isWorkerLoaded) return;
    await worker.start();
    setIsWorkerLoaded(true);
  };

  React.useEffect(() => {
    loadWorker();
  }, [loadWorker]);

  return isWorkerLoaded ? <Story /> : <div>Loading...</div>;
};
.storybook/preview.ts
import { mswDecorator } from "./decorators/mswDecorator";


const preview: Preview = {
  decorators: [
    mswDecorator
  ],
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
};

export default preview;
ryo_kawamataryo_kawamata

handlerとstoryを作って試してみる

src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/posts', () => {
    // Response resolver allows you to react to captured requests,
    // respond with mock responses or passthrough requests entirely.
    // For now, let's just print a message to the console.
    return HttpResponse.json({
      posts: [{
        id: 1,
        title: 'Mock Service Worker',
      }],
    })
  }),
]
App.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";

const App = () => {
  React.useEffect(() => {
    const fetchPosts = async () => {
      const result = await fetch("/posts")
      console.log("🚀 ~ mock result", result)
    }
    fetchPosts()
  }, [])
  return (
    <div>
      <h1>App</h1>
    </div>
  );

}

const meta: Meta = {
  title: "CSUI/App",
  component: App,
};

export default meta;

type Story = StoryObj<typeof App>;

export const Default: Story = {};

できた🎉

ryo_kawamataryo_kawamata

コンポーネント描画時にチラつくので、それをなんとかしたい。
decoratorじゃない方がいいのかな?

あと、Mswのmockがいらないコードでも起動するのがイマイチ。修正しよう。

ryo_kawamataryo_kawamata

Story単位でdecoratorsを設定できるようなので、そちらに修正する。

import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { mswDecorator } from "../../../.storybook/decorator/mswDecorator";

const App = () => {
  React.useEffect(() => {
    const fetchPosts = async () => {
      const result = await fetch("/posts")
      console.log("🚀 ~ mock result", result)
    }
    fetchPosts()
  }, [])
  return (
    <div>
      <h1>App</h1>
    </div>
  );

}

const meta: Meta = {
  title: "CSUI/App",
  component: App,
  decorators: [
    mswDecorator
  ]
};

export default meta;

type Story = StoryObj<typeof App>;

export const Default: Story = {};
ryo_kawamataryo_kawamata

これでOKと思ったらchormeのonMessageをコンポーネントで使っているの関係で参照エラーがでたので、それもmockしてみる。追加のdecoratorを作る。

export const browserApiDecorator = (Story) => {
  window.chrome = {
    runtime: {
      onMessage: {
        addListener: () => {},
        removeListener: () => {}
      },
    }
  } as any

  return <Story />
}
このスクラップは2024/02/24にクローズされました