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

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

調べたら以下が出てきた。
ただ、install時に互換性エラーで失敗する。
どうやら、msw-storybook-addonはmswの1系のみ対応しているが、mswの1系はプロジェクトで使われているTypeScriptのバージョン5.3系と互換性がないようだ・・
mswの2系を使えるようにするためのPRもあるがまだwip。

どうせならmswの2系を使いたいので msw-storybook-addon を使わないで素のmswでなんとかする方法を考える
を読んでみる。
とりあえず、Storybookを起動した時にmswのservice workerが起動するようにしたい
MSWのスクリプトをpublicディレクトリに配置する必要があるらしい。
なので、Storybookにpublicディレクトリの設定を追加して、そこを指定するようにしてみる
$ mkdir ./storybook/public
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
// ...
staticDirs: ["./public"],
// ...
};
export default config;
$ npx msw init .storybook/public

Storybookの起動時にmswのworkerのstartを実行したい
decoratorsでできそうだけど、Workerの起動をawaitしたいなー。。どうするのがよいのだろう。
decoratorsはpromiseを返せない気がする

無理やりだけど、mswのdecoratorを作って、こんな感じにしてみた。
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>;
};
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;

handlerとstoryを作って試してみる
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',
}],
})
}),
]
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 = {};
できた🎉

コンポーネント描画時にチラつくので、それをなんとかしたい。
decoratorじゃない方がいいのかな?
あと、Mswのmockがいらないコードでも起動するのがイマイチ。修正しよう。

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 = {};

これでOKと思ったらchormeのonMessageをコンポーネントで使っているの関係で参照エラーがでたので、それもmockしてみる。追加のdecoratorを作る。
export const browserApiDecorator = (Story) => {
window.chrome = {
runtime: {
onMessage: {
addListener: () => {},
removeListener: () => {}
},
}
} as any
return <Story />
}

browser apiのモックやplasmo固有のメソッドのモックは、以下を使う方がいいのかもしれない。