Material-UI を普通に使うと Storyshots がうまく使えない
以前の記事で Storyshots の導入を行いました。そこでは触れられませんでしたが、 CSS in JS のライブラリによっては story の snapshot に依存関係が生まれて一つのコンポーネントを修正すると以降の story のスナップショットテストがすべて落ちてしまうことがあります。
今回は Material-UI を例に対処方法をまとめました。
原因と対処の概要
先に何が起きているのかと、その対処方法をまとめます。
- 原因: Storyshots 全体で className に連番が振られており、1箇所 className の数が変わると以降のコンポーネントの className がずれる
- 対処方法: 連番を story 単位でリセットする。(もしくは連番をしない)
そもそもなぜ連番が振られるかという話ですが、多くの CSS in JS ライブラリはコンポーネントの一意性を高めるためにコンポーネントごとの className に連番を振ったり一意なハッシュを付与することで、意図せずグローバルなスタイルがあたってしまったりコンポーネント間でスタイルの衝突が起きたりしないようにしています。
その仕組みを維持するために今回は連番を止めず、 Story 単位で完結する形で対応します。
現象の確認
前準備
まずは現象が起こる環境を用意しました。
今回のデモは下記PRにまとめています。コードの詳細など気になる点はこちらを御覧ください。
準備の概要
- Material-UI を Storybook 上で使用する準備をし
https://github.com/Gaji-Labo/demo-storybook-with-next-typescript/pull/9/commits/f36de53 - 複数の Story を追加
https://github.com/Gaji-Labo/demo-storybook-with-next-typescript/pull/9/commits/962e7d- ※ story 内でコンポーネントを定義してしまっているのもありますが本来はちゃんと作成したコンポーネントを import すべきです
- makeStyles でいくつかの Story のスタイルを上書き
https://github.com/Gaji-Labo/demo-storybook-with-next-typescript/pull/9/commits/a6a6afc- ※ ThemeProvider を使用してテーマで上書きすべきなスタイリングですが、 makeStyles が重要なのでそこは目をつぶってください
スナップショット作成
この状態でまずはスナップショットの更新を行います。
当然ですが通常通り問題なくスナップショットが更新できます。
Story を足すと…
この状態であらたに makeStyles でスタイルの上書きをしている Breadcrumbs.story.tsx を追加すると下記のように storyshots のスナップショットテストが落ちてしまいます。
本来であれば、Breadcrumbs story を追加しても影響を受けるはずがない下記のスナップショットテストが失敗しています。
- Material-UI Button Default
- Material-UI CircularProgress Default
- Material-UI Tabs Default
これらの Story はすべて準備段階で makeStyles を使っていたコンポーネントです。
テストを見てみると下記のように className の連番がずれています。
ひとつの story で className が増減したり story を追加するだけで以降の makeStyles を使用しているスナップショットテストがすべて落ちてしまいます。
Snapshot name: `Storyshots Material-UI Button Default 1`
- Snapshot - 1
+ Received + 1
@@ -18,11 +18,11 @@
<span
className="MuiButton-label"
>
Hello
<span
- className="makeStyles-labelHighlight-1"
+ className="makeStyles-labelHighlight-2"
>
World
</span>
</span>
</button>
対処
ということは、各 story ごとで連番をリセットすれば story 同士の依存関係が解消されます。
下記コミットのように Storybook に StylesProvider
をかまし、自前の generateClassName
を渡すことで className の制御ができます。
import { addParameters, addDecorator } from "@storybook/react";
import { ThemeProvider, createMuiTheme } from "@material-ui/core/styles";
import { StylesProvider } from "@material-ui/core/styles";
import CssBaseline from "@material-ui/core/CssBaseline";
// NOTE: Story 単位で makeStyles の className 連番をリセットしたい
const createGenerateId = () => {
let counter = 0;
return (rule, styleSheet) =>
`${styleSheet.options.classNamePrefix}-${rule.key}-${counter++}`;
};
const theme = createMuiTheme({
// 【中略】
});
addDecorator((story) => (
<StylesProvider generateClassName={createGenerateId()}>
<ThemeProvider theme={theme}>
<CssBaseline />
{story()}
</ThemeProvider>
</StylesProvider>
));
// 【後略】
これで Story 単位の連番が付与されるようになり平穏がおとずれました 🎉
連番がリセットされている様子がわかる snapshot の diff は下記です。
※ Material-UI のコンポーネントが持つ className にも採番されるようになってしまいますが、 Storybook や Storyshots を利用する上で問題はないので今回はよしとしています。
ちょっと余談
Material-UI では CSS in JS ライブラリに JSS を使用しています。
JSS には createGenerateId
, generateId
というAPIが提供されていて className
を操作できます。
今回はこの仕組みを利用しました。
ただ、JSS v9.x まではこれらの API は下記のような名前でした。
createGenerateId
→createGenerateClassName
generateId
→generateClassName
しかし執筆時点の最新である @material-ui/core@4.11.0
の StylesProvider
では古い API の名前のままなので、諸々調べるのに少しハマるかもしれません。
経緯を知っていればサクッと解決できるのですが、ヒントを見つけるまで苦戦した記憶があるのでここに残しておきます。
また JSS 以外でもスナップショットテストで似たような現象が起きたら同様のアプローチが取れないか検討してみると良いかもしれません。
開発のお悩み、フロントエンドから解決しませんか?
あなたのチームのお悩みはなんですか?
「バックエンドエンジニアにフロントエンドまで任せてしまっている」
「デザイナーに主業務以外も任せてしまっている」
「すべての手が足りず細かいことまで手が回らない」
役割や領域を適切に捉えてカバーし、チーム全体の生産性と品質をアップするお手伝いをします。
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にお声がけください。
オンラインでのヒアリングとフルリモートでのプロセス支援に対応しています。
リモートワーク対応のパートナーをお探しの場合もぜひ弊社にお問い合わせください!
お悩み相談はこちらから!