storybook
まずはreactのチュートリアルをやって、それをベースにstorybookやっていく様子。
忘れてるしもっかいやっておく
素の状態でstartできなくて草。
react-scriptsが古い?5系にあげたらいけた。
ここにやっていく
storybook入れていく
npx storybook@latest init
既存プロジェクトに入れる場合は上記らしい。package.json見て最適なやつを入れてくれるらしいが…(こういうのうまくいったためしがない…)
案の定エラー
One of your dependencies, babel-preset-react-app, is importing the
"@babel/plugin-proposal-private-property-in-object" package without
declaring it in its dependencies. This is currently working because
"@babel/plugin-proposal-private-property-in-object" is already in your
node_modules folder for unrelated reasons, but it may break at any time.
babel-preset-react-app is part of the create-react-app project, which
is not maintianed anymore. It is thus unlikely that this bug will
ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to
your devDependencies to work around this error. This will make this message
go away.
ERROR in [eslint]
src/stories/Button.jsx
Line 1:8: 'React' is defined but never used no-unused-vars
src/stories/Header.jsx
Line 1:8: 'React' is defined but never used no-unused-vars
src/stories/Page.jsx
Line 35:13: `"` can be escaped with `"`, `“`, `"`, `”` react/no-unescaped-entities
Line 35:18: `"` can be escaped with `"`, `“`, `"`, `”` react/no-unescaped-entities
Search for the keywords to learn more about each error.
preview compiled with 1 error
=> Failed to build the preview
WARN Force closed preview build
SB_BUILDER-WEBPACK5_0003 (WebpackCompilationError): There were problems when compiling your code with Webpack.
大したエラーではなかった。修正したらビルドできた。
storiesファイルはstorybook/main.js
で指定すればどこに置いてても拾ってくれそう。
storyフォルダに入れ込むのと、各コンポーネントに隣接して置くのどっちがいいだろな…なんかCSS modulesとかにして、テストとかも全部隣接させるのがわかりやすそうな気もする。
src
├── hoge
├── HogeComponent.ts
├── HogeComponent.module.css
├── hoge.test.ts
└── hoge.stories.ts
こんな感じ。
一つのコンポーネントとかに関連するファイルが増えるからなあ…
1コンポーネントに対して1ファイル作らないといけない?
複数コンポーネントを同時に書く方法あるんだろうか。
書き方
The stories are written in Component Story Format (CSF)--an ES6 modules-based standard--for writing component examples.
import type { Meta, StoryObj } from '@storybook/react';
import { Button, ButtonProps } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
default exportでコンポーネント等のメタ情報を定義して、あとはテストオブジェクト?的なものを作ればOK.
CSFは新しい書き方なのね。
以前の Storybook は、storiesOf でコンポーネントパターンを定義していましたが、storiesOf 形式では当然 Storybook にロックインするため、せっかく定義した「コンポーネントの状態」を、他ツールへ応用しづらいのが現状です。
CSF は、Storybook v5.2 で新しく導入されたフォーマットです。そのコアコンセプトは、この storiesOf にポータビリティを持たせることです。Jest や Cypress など、他ツールとも連携できるように設計されています。
コンポーネントの引数はargsで定義。
story毎
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
export const PrimaryLongName: Story = {
args: {
...Primary.args, // こんな感じで使い回せる
label: 'Primary with a really long name',
},
};
ファイル = component毎
const meta: Meta<typeof Button> = {
component: Button,
//👇 Creates specific argTypes
argTypes: {
backgroundColor: { control: 'color' },
},
args: {
//👇 Now all Button stories will be primary.
primary: true, // default export内で定義すれば、全部に適応できる
},
};
export default meta;
全部の場合はpreview.tsに定義
// Replace your-renderer with the renderer you are using (e.g., react, vue3, angular, etc.)
import { Preview } from '@storybook/your-renderer';
const preview: Preview = {
// The default value of the theme arg for all stories
args: { theme: 'light' },
};
export default preview;
Args can be used to dynamically change props, slots, styles, inputs, etc.
Argsは引数以外にもいろいろ使えるぽい
argTypesでstorybook上でいじれるようになる。勝手にコンポから持ってきてくれないのかな…
parameters
storyの背景色とかもうちょっとメタな情報はparameterで定義するっぽい。
これもstory/component/globalレベルで設定可。というかほとんど全部それできそう。
storyの周り?にスタイルを追加するためのもの。
例えばボタンコンポーネントの周りにpaddingを設定したりとか。
あとは、
For example, if you're working with React's Styled Components and your components use themes, add a single global decorator to .storybook/preview.js to enable them. With Vue, extend Storybook's application and register your library. Or with Angular, add the package into your polyfills.ts and import it:
使用技術によってはdecoratorsを使って環境設定する感じ?
Play functions are small snippets of code executed after the story renders. Enabling you to interact with your components and test scenarios that otherwise required user intervention.
storyレンダリング後になにかしたい時。なにかってなに。
We recommend installing Storybook's addon-interactions before you start writing stories with the play function.
addon-interactionsを入れたほうがいいらしい
あーこれ、ユーザの行動の自動化(フォームへの入力とかクリックとか)とかか。つまりテスト用って感じ?
なんかいろいろ書いてあるが使う時に読む
たぶんtesting-libraryの書き方勉強する必要あり?
export const FilledForm: Story = {
play: async (引数) => {
// play functionの引数はこんな感じ
abortSignal
allArgs
applyLoaders
argTypes
args
argsByTarget
canvasElement
component
componentId
globals
hooks
id
initialArgs
kind
loaded
moduleExport
name
originalStoryFn
parameters
playFunction
step
story
subcomponents
tags
title
unboundStoryFn
undecoratedStoryFn
unmappedArgs
viewMode
By default, each interaction you write inside your play function will be executed starting from the top-level element of the Canvas. This is acceptable for smaller components (e.g., buttons, checkboxes, text inputs), but can be inefficient for complex components (e.g., forms, pages), or for multiple stories. To accommodate this, you can adjust your interactions to start execution from the component's root.
Applying these changes to your stories can provide a performance boost and improved error handling with addon-interactions.
export const ExampleStory: Story = {
play: async ({ canvasElement }) => {
// Assigns canvas to the component root element
const canvas = within(canvasElement);
// Starts querying from the component's root element
await userEvent.type(canvas.getByTestId('example-element'), 'something');
await userEvent.click(canvas.getByRole('another-element'));
},
};
よくわからんけどconst canvas = within(canvasElement);
って書き方が効率がいいってこと?
Loaders are asynchronous functions that load data for a story and its decorators. A story's loaders run before the story renders, and the loaded data injected into the story via its render context.
非同期データロード?
Loaders can be used to load any asset, lazy load components, or fetch data from a remote API. This feature was designed as a performance optimization to handle large story imports.
However, args is the recommended way to manage story data. We're building up an ecosystem of tools and techniques around Args that might not be compatible with loaded data.
大規模データとか特殊用途用で、基本はarguments使ってね、って感じか
Naming components and hierarchy
const meta: Meta<typeof Button> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/configure/#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'Design System/Atoms/Button',
component: Button,
};
タイトルを/
で区切ればstorybook上でフォルダ分け的な感じになる
フォルダ内でのソートもできるらしい
A major strength of Storybook are addons that extend Storybook’s UI and behavior. Storybook ships by default with a set of “essential” addons that add to the initial user experience.
The actions addon is used to display data received by event handler (callback) arguments in your stories.
const meta: Meta<typeof Button> = {
component: Button,
argTypes: { onClick: { action: 'clicked' } },
};
preview.jsにonXxx全部予めつけとくこともできる。
// Replace your-framework with the framework you are using (e.g., react, vue3)
import { Preview } from '@storybook/your-framework';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on.*' },
},
};
export default preview;
え、動かないんだけど…?
Exampleの方は動いている…全く同じコードにしても動かないのはなぜ
むむむ
// hoge.jsx
- export function Square({ value, onSquareClick }) {
+ export function Square({ value, onSquareClick, ...props }) {
return (
- <button className="square" onClick={onSquareClick} >
+ <button className="square" onClick={onSquareClick} {...props}>
{value}
</button>
);
}
// hoge.stories.js
export default {
...
argTypes: {
onClick: { action: "clicked" },
},
};
propsの展開+argTypesの定義でいけた。結構めんどいぞ。結局SquareにonClickを渡してるだけってことか。大事なことだと思うのだがなぜActionsのページに書かない…
あとは背景設定とかviewport設定とかいろいろあるっぽい
他にも追加できるAddonがいっぱい
基本play function?
- Test runner to automatically test your entire Storybook and catch broken stories.
- Visual tests capture a screenshot of every story then compare it against baselines to detect appearance and integration issues
- Accessibility tests catch usability issues related to visual, hearing, mobility, cognitive, speech, or neurological disabilities
- Interaction tests verify component functionality by simulating user behaviour, firing events, and ensuring that state is updated as expected
- Coverage tests to measure how much of your code is covered by your tests
- Snapshot tests detect changes in the rendered markup to surface rendering errors or warnings
- End-to-end tests for simulating real user scenarios
- Unit tests for functionality
Storybook test runner turns all of your stories into executable tests. It is powered by Jest and Playwright.
とりあえずレンダリングエラーだけ確認できるってことか。まあ確かに全部チェックとかしとれんもんな…warnとかも出たり、消したりできるんかな。
- play functionなし:Storyがエラーなくレンダリングされるか
- あり:play functionがエラー吐かないか、全アサーション(play functionってassertionあるの?)がパスしたか
やってみるか…
npm install @storybook/test-runner --save-dev
お、パスした。けど、レポートとか無いんかしら
Test Suites: 6 passed, 6 total
Tests: 22 passed, 22 total
Snapshots: 0 total
Time: 3.919 s
Ran all test suites.
一応やっといてもいいがって感じな気がしてきた(レンダリングエラーは気づきそう)…いや、まあでもいろんなパターンでレンダリングできるかどうかわかるのか。
chromaticがおすすめらしい。案件ではお金かかるからきついかもだが…個人レベルなら無料っぽいのでやってみる。
npm install --save-dev chromatic
npx chromatic --project-token=[トークン]
いろいろ自動ですごい!!って思ったけどスタイル変更検知してくれない…笑
チュートリアル的なものがおわらない
終わった。お、いい感じ。
chromaticをscriptsに登録
"scripts": {
"chromatic": "npx chromatic --project-token=[トークン]"
},
んん、言うだけあってめちゃいい感じすな…でもお高いんでしょう…?
githubとも連携してReviewとかマージとかいい感じにできるっぽい。よさそう。
でもこれなんかビルドごとに承認する感じなのか。1回変更してそれを戻したけどそれは検知してない。最新ビルドを承認しても過去のやつは別途やらなきゃ行けないのね。まあでもそりゃそうか。単純にスナップショット比較してるだけやもんね。いや、でも全Snapshot取ってたら最新OKならOKちゃうん。いや、ちゃうか。うーん…?
For a self-managed alternative to Chromatic, we offer test runner. It allows you to run visual tests on stories by integrating with Jest and Playwright. Here's an example recipe for visual testing stories.
test runnerでもできるらしいが、結構設定がいるのかな?案件では予算的にこっちになるのかなあ…?
合理的配慮の提供義務化もあるし、これはやっていったほうがよさそう。でも合理的配慮ってなんやねーん。調べないとな。
日本の法制に合わせたConfig設定が必要になる?グローバルに合わせておけば間違いない?
You start by writing a story to set up the component's initial state. Then simulate user behavior using the play function. Finally, use the test-runner to confirm that the component renders correctly and that your interaction tests with the play function pass.
play functionでユーザアクションを定義して、test-runnerでチェックって感じか。
お、ダブルクリックとかもあるのね。二重クリック防止のチェックとかにも使える?
export const Initial = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const squares = canvas.getAllByRole("button");
await userEvent.click(squares[0]);
await userEvent.click(squares[1]);
await userEvent.click(squares[2]);
await userEvent.click(squares[3]);
await userEvent.click(squares[4]);
await userEvent.click(squares[5]);
await userEvent.click(squares[6]);
// 👇 Assert DOM structure
await expect(canvas.getByText("Winner: X")).toBeInTheDocument();
},
};
書き方あってるかわからないが、↑でマルバツゲームを自動化できた↓。storybookをみれば結果が確認できる。
npm run test-storybook
で手動実行も可。これをhuskyに組み込んどけばいいのかな。CIが理想ではあるがGA結構お金かかるらしいしな…
"@storybook/jest": "^0.2.3",
"@storybook/testing-library": "^0.2.2",
storybookのjestとtesting-libraryまだ1系に乗ってないやん…大丈夫なんか笑
まあでもStorybookでdevDependenciesやしな…
チームでの共有前回も困ったな…結局デザイナさんにローカルでビルドしてもらうという力技にした…。この解説読んだ感じだとアクセスコントロールは特にできなさそう?GithubPagesってOrganizationにアクセス限るみたいな機能なかったっけ…
まとめ
- すごいいい感じ
- 案件で使った時結構環境設定で詰まった印象があったが、進化している?(シンプルなリポジトリだからってのもあるだろうが)
- フロントエンドのテストはStorybookで十分そうな印象
- chromaticよいかんじ
- すごいけど本気で使うってなるとお金が気になる
- chromaticとの連携は結構ありっぽいなあ…でも費用と相談って感じか(高いのかどうかもよくわかってない)
- 基本はstory書いて、重要な動作はplay function作って、test-runnerとアクセシビリティテスト回しつつ…これCIで回したら結構お金かかるのかなあ…
自動化
なるほど。
ディレクトリ構造はそうだなーと思ってたし、自動生成いいなやっぱり。試してみたい。
storyshots
テストの範疇だが大きくなりそうなので分離。
Storyhots is now officially deprecated, is no longer being maintained, and will be removed in the next major release of Storybook.
deprecatedやんw
専用のaddon作ったけど複雑やしデファクトスタンダードのjestに寄せればよくね、みたいな感じなんかな?
migration guide。test-runnerかportable storiesに移行せよとのこと。portable storiesってなんや。でもtest-runnerってレンダリングエラーやろ?前後の差分とかは比べられんやん。
including interaction testing with the play function, DOM snapshot, and accessibility testing.
俺がtest-runnerさんの可能性を引き出せてなかっただけか…
スナップショットテスト有効化
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
async postVisit(page, context) {
// the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
const elementHandler = await page.$('#storybook-root');
const innerHTML = await elementHandler.innerHTML();
expect(innerHTML).toMatchSnapshot();
},
};
export default config;
こんだけでいけるん…?
いけてる。すご。
ただちょっと魔法感あるな…トラブったら困りそうな予感。
storyと同じ階層に生成されてるのか。まとめられんのかな?
Portable Stories
Storybook provides a composeStories utility that helps convert stories from a story file into renderable elements that can be reused in your Node tests with JSDOM
??
解説記事もあんまないな。よくわからんから広まってない or 新しい機能?
Import all story files based on a glob pattern
Iterate over these files and use composeStories on each of their modules, resulting in a list of renderable components from each story
Cycle through the stories, render them, and snapshot them
結局やってることはtest-runnerと一緒?の割に設定多いから広まらんのか?何のためにある…?
名前的にテスト用ってよりは、他のテスティングフレームワークとかで再利用するための仕組み(can be reused in your Node tests with JSDOM
ってあるし)?なんもわからん。
設定コードを見るとゴリゴリにテストフローを記述してる感じ。ゴリゴリカスタマイズできる上級者向け機能って感じ?
そのままやるとスナップショットは1ファイルにまとまるけど、分けることもできるよとのこと。
ちょっと一旦スキップで
test-runnerに戻る
スナップショットファイルの内容
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tic-Tac-Toe/Square CheckActions smoke-test 1`] = `
<button class="square">
</button>
`;
exports[`Tic-Tac-Toe/Square X smoke-test 1`] = `
<button class="square">
X
</button>
`;
...
- 書式は謎だが、js?exportsというオブジェクトに
Tic-Tac-Toe/Square X smoke-test 1
をキーにしてHTMLを突っ込んでるのかな? -
Tic-Tac-Toe/Square
はtitle -
X
はstory名(export const X = {
のX) -
smoke-test
はなんだ?play function 使ってるやつはplay-test
になっていた。play function使ってないやつはsmoke-test?→スモークテスト -
1
はなんだろ…全部1になってる。何で変わるのか不明 - あとはレンダリングされたHTML
文言変えてみる
検出された。最後ranって綴りあってる…?→過去形か!笑そんなことも忘れているとはやばいな…
変更を承認するのどうやるの。
chromaticだと自動で承認するかどうかのボタンまで出たもんなーやっぱ便利やなあれ。一覧化してこれはOK、これはだめみたいな感じでやりたいよね。
いや、てかMigration guideに記載がないやん…しといて…笑まああくまでMigration用やしな…じゃあjestを見ればよい…?
› 2 snapshots failed from 1 test suite. Inspect your code changes or re-run jest with `-u` to update them.
ってエラー書いてあった。全部承認するならnpm run test-storybook -u
ってこと?
これ押した後ミスってたらキャンセルする方法とかあるんかいな。承認されたかどうかはどこで管理されている?というかそうか、スナップショットファイルが更新されるかどうかか。キャンセルはスナップショットファイルの差分を捨てればいい。
じゃあ1回やってみる。
え、ならないんですけど笑
さっきと同じ結果。
"test-storybook-u": "test-storybook -u",
っていうscriptをpackage.jsonに追加したらいけた。オプションって使えなかったっけか…
やはり差分を戻せば戻る。
これを解決するには、生成したスナップショットを更新する必要があります。 単純にスナップショットを再生成するように指示するフラグを付けてJestを実行するだけでできます。
jest --updateSnapshot
上記のコマンドを実行することで変更を受け入れることができます。 お好みで一文字の -uフラグでもスナップショットの再生成を行うことができます。
再生成されるスナップショットを限定したい場合は、 --testNamePatternフラグを追加して指定することでパターンにマッチするテストのみスナップショットを再生成することができます。
これか。
いや、どうやってそのモードに入るのか書いてや…笑
これや。--watch
i押しても
こんな感じになってEnter以外効かん。なんやねん。なんかやり方おかしい?バグ…?
うーん、なんかめんどい気がするけど、どうせ全部OKになってからしかやらんでしょ?みたいなこと?
引数渡すのこんなんやったんや…やったことなかった
ローカルでビジュアルリグレッションテスト
import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
const customSnapshotsDir = `${process.cwd()}/__snapshots__`;
const config: TestRunnerConfig = {
setup() {
expect.extend({ toMatchImageSnapshot });
},
async postVisit(page, context) {
// Waits for the page to be ready before taking a screenshot to ensure consistent results
await waitForPageReady(page);
// To capture a screenshot for for different browsers, add page.context().browser().browserType().name() to get the browser name to prefix the file name
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotsDir,
customSnapshotIdentifier: context.id,
});
},
};
export default config;
jest-image-snapshotがないとのこと。アメックス…?
[test-storybook] Cannot find module 'jest-image-snapshot'
-> npm i --save-dev jest-image-snapshot
…?全部タイムアウトした。
時間を増やせばいいってより根本的になにかおかしそう。。
ぬーん…やっぱ魔法やからデバッグがよくわからん。。
chromaticに誘導したいんだろうが公式が不親切過ぎんか…?
console.log("postVisit");
// Waits for the page to be ready before taking a screenshot to ensure consistent results
await waitForPageReady(page);
console.log("page ready");
page readyに到達してない。うーん…storybook実行してればいいんちゃうの…?
await waitForPageReady(page);
を外したらうまくいった… postVisit自体がレンダリング後のはずやからこれでも良い気がするが、いいのか…?画像自体はplay functionありのものも含めちゃんと取れてた。うーん…すっきりしない。。
文字を赤に変えたら検出された。差分画像が生成されてる(スナップショットフォルダ内にdiffフォルダが生成されてそのなに入っている)からそれをみて判断する感じか。でもChromaticのを見てるからそれと比べるとやっぱ見ずらい。
-uオプションで実行したら更新され、diff画像は削除された。