@storybook/addon-storyshots を導入する
storybook の tutorial をもとに、React + ts な project に @storybook/addon-storyshots を導入しようとしたところ、エラーが出現。解決までの道のりをメモっていく。
参考にした tutorial はこちら
遭遇したエラーはこちら↓
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
何度か遭遇したことあるやつ。おそらく parse がうまくいってない箇所があるな。
=> 今回は .storybook/preview.js
内で import している箇所で parse に失敗している様子。
ts-jest で ESM support
parse に失敗している箇所が、ESM な js file (.storybook/preview.js
) であったため、ts-jest の力を借りて ESM syntax を CommonJS syntax に変換することで対応する。
ESM support を有効化するために、jest.config.js
に以下を追加。
preset: 'ts-jest/presets/js-with-ts',
globals: {
'ts-jest': {
useESM: true,
},
},
こちらの example で出てくる moduleNameMapper
は import される特定の file を mock file にすげ替えるなど、あるモジュールを別のモジュールに map するのに使われるものなのでここでは指定しない。
ts-jest/presets/js-with-ts
preset
preset で指定できるものはここで確認できる↓
今回は、ts, js file を CommonJs に変換したいので ts-jest/presets/js-with-ts
を採用。
You'll need to set allowJs to true in your tsconfig.json file.
allowJs を true にしなさいとのことなので、tsconfig.json も更新。
また、.storybook/preview.js
も変換対象に含めたいので、dev 環境用の tsconfig.json を以下のように編集。
{
"extends": "../tsconfig.json",
"include": ["src", ".storybook"],
"compilerOptions": {
"allowJs": true
}
}
一旦、これで .storybook/preview.js
のエラーは解消!
ただ、他のエラー出たので続く。
.storybook/preview.js
内の 'typeface-roboto' (MUI で使用する font) を import しているとこで エラー起きてる。
SyntaxError: Invalid or unexpected token
3 | import React from 'react';
4 | // material-ui
> 5 | import 'typeface-roboto';
| ^
css は mock file を参照するように
typeface-roboto/index.css を parse しようとしちゃってるので、css は mock file を参照するようにしてみる。
__mocks__/style-mock.ts
を作成して、css はそちらを参照するよう設定
export default {};
+ // test 実行時に import している css file は mock して、parse されないように
+ moduleNameMapper: {
+ '\\.css$': '<rootDir>/__mocks__/style-mock.ts',
+ },
だめだ...あい変わらず同じエラーでる...
typeface-robot を mock
力技感あるが、typeface-robot 自体を mock して対応してみる。
// test 実行時に import している css file は mock して、parse されないように
moduleNameMapper: {
'\\.css$': '<rootDir>/__mocks__/style-mock.ts',
+ 'typeface-roboto': '<rootDir>/__mocks__/style-mock.ts',
},
解決!これで parse 関連のエラーは全てクリア。
しかし、テスト実行時に別のエラー出たので続く。
@storybook/addon-storyshots
を実際に呼び出している file でエラー出た。
FAIL src/storybook.test.ts
● Test suite failed to run
TypeError: Cannot read properties of undefined (reading '__STORYBOOK_STORY_STORE__')
1 | import initStoryshots from '@storybook/addon-storyshots';
> 2 | initStoryshots();
| ^
3 |
調べてもそれらしいのが出てこない...
ソースコード見てみるか
version は 6.4.13
@storybook/addon-storyshots@6.4.13
type error がでてるのはここ だな
globalWindow.__STORYBOOK_STORY_STORE__.initializationPromise.then(() => {
この globalWindow が原因っぽいな...
import global from 'global';
const { describe, window: globalWindow } = global;
global
て package から import してる window object だな。
global
を見にいくか
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof global !== "undefined") {
win = global;
} else if (typeof self !== "undefined"){
win = self;
} else {
win = {};
}
module.exports = win;
見たところ、node の global object を win として export してるだけだな...
ってことは、@storybook/addon-storyshots を実行している環境の global object に __STORYBOOK_STORY_STORE__
が生えていなきゃいけない訳だな
console.log((global as any).__STORYBOOK_STORY_STORE__);
// undefined
console.log してみたけど、そんなもの生えてないぞ....
どこで __STORYBOOK_STORY_STORE__
生やしてるか調べるか
ここ だな
export function start<TFramework extends AnyFramework>(
/*~~ 省略 ~~*/
if (globalWindow) {
globalWindow.__STORYBOOK_CLIENT_API__ = clientApi;
globalWindow.__STORYBOOK_ADDONS_CHANNEL__ = channel;
// eslint-disable-next-line no-underscore-dangle
globalWindow.__STORYBOOK_PREVIEW__ = preview;
globalWindow.__STORYBOOK_STORY_STORE__ = preview.storyStore;
}
/*~~ 省略 ~~*/
@storybook/core-client@6.4.13
の start()
function 内で __STORYBOOK_STORY_STORE__
を global に生やしてる。
そんで、この start()
を呼び出してるので @storybook/react@6.4.13
の ここか
import { start } from '@storybook/core/client';
/*~~ 省略 ~~*/
const api = start(renderToDOM, { render });
@storybook/core
は @storybook/core-client
を re-export してるだけ。
ここまでのまとめ
-
@storybook/addon-storyshots
は@storybook/core-client
に依存-
@storybook/core-client
のstart()
が呼び出されることを前提にしている (= global に__STORYBOOK_STORY_STORE__
がセットされている前提)
-
-
@storybook/core-client
のstat()
は@storybook/react
で呼び出される
依存している package が正しいバージョンではない疑惑...?
調べる
まずは storyshots
yarn list --pattern @storybook/addon-storyshots
└─ @storybook/addon-storyshots@6.4.13
version は 6.4.13
。
念の為、 @storybook/addon-storyshots@6.4.13
の package.json みる。
{
"name": "@storybook/addon-storyshots",
"version": "6.4.13",
"dependencies": {
"@jest/transform": "^26.6.2",
"@storybook/addons": "6.4.13",
"@storybook/babel-plugin-require-context-hook": "1.0.1",
"@storybook/client-api": "6.4.13",
"@storybook/core": "6.4.13",
"@storybook/core-client": "6.4.13",
"@storybook/core-common": "6.4.13",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@types/glob": "^7.1.3",
"@types/jest": "^26.0.16",
"@types/jest-specific-snapshot": "^0.5.3",
"core-js": "^3.8.2",
"glob": "^7.1.6",
"global": "^4.4.0",
"jest-specific-snapshot": "^4.0.0",
"preact-render-to-string": "^5.1.19",
"pretty-format": "^26.6.2",
"react-test-renderer": "^16.8.0 || ^17.0.0",
"read-pkg-up": "^7.0.1",
"regenerator-runtime": "^0.13.7",
"ts-dedent": "^2.0.0"
},
"@storybook/core": "6.4.13"
なので、@storybook/core
の 6.4.13
であるべき。
さぁ確認。
yarn list --pattern @storybook/core-client
└─ @storybook/core-client@6.3.7
うおぉぉぉぉぉぉぉぉい
やっぱり version 違いでしたか
@storybook/** 全て 6.4.13
に揃える。
"@storybook/addon-actions": "6.4.13",
"@storybook/addon-essentials": "6.4.13",
"@storybook/addon-links": "6.4.13",
"@storybook/addon-storyshots": "6.4.13",
"@storybook/react": "6.4.13",
よし、テスト実行
エラー出た...
TypeError: Cannot convert undefined or null to object
とりあえず @babel/core
, babel-loader
を最新に上げたら、上記エラーは消えた。
"@babel/core": "^7.16.10",
"babel-loader": "^8.2.3",
が、storybook 自体が動かなくなったぞ...
ERR! TypeError: (0 , _coreCommon.getStorybookBabelConfig) is not a function
issue あがってるみたいだけど、結論は現状(2022/1/20)出てない
どうやら、@storybook の依存 module が正しくない version でインストールされちゃってることが原因ぽい
my suggestion is delete the root node_module and install it again from the root.
”node_module を消して再インストール" を提案されてたので、やってみたら Storybook 動くようになった...
釈然としないが先に進む
test 実行したら別のエラー出た
TypeError: Cannot read properties of undefined (reading 'addEventListener')
ソースコード読む限り、このエラーも global window が undefined になっているエラー
試しに、console.log してみる
import initStoryshots from '@storybook/addon-storyshots';
console.log('window', typeof window);
// window undefined
console.log('global', typeof global);
// global object
initStoryshots();
やはり window が存在してない
既存 prroject への導入中断
埒が明かないので、一旦作業中断。
そもそも最新の version で問題なく動くのか検証したいので、tutorial 通り (最新の version 使う) に進めていく。
storyshots の導入まで行ったがうまく動いてるな
package.json と install されている @storybook 関連の module 貼っとく。
{
"name": "intro-storybook-react-template",
"version": "0.1.0",
"description": "Starter template to get up and running quickly with React and Storybook",
"author": "Chromatic <https://chromatic.com/>",
"repository": {
"type": "git",
"url": "https://github.com/chromaui/intro-storybook-react-template"
},
"bugs": {
"url": "https://github.com/chromaui/intro-storybook-react-template/issues"
},
"license": "MIT",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@storybook/addon-actions": "^6.4.8",
"@storybook/addon-essentials": "^6.4.8",
"@storybook/addon-interactions": "^6.4.8",
"@storybook/addon-links": "^6.4.8",
"@storybook/addon-storyshots": "^6.4.13",
"@storybook/node-logger": "^6.4.8",
"@storybook/preset-create-react-app": "^3.2.0",
"@storybook/react": "^6.4.8",
"@storybook/testing-library": "^0.0.7",
"@storybook/testing-react": "^1.2.2",
"react-test-renderer": "^17.0.2"
}
}
$ yarn list --pattern @storybook
yarn list v1.22.4
├─ @storybook/addon-actions@6.4.13
├─ @storybook/addon-backgrounds@6.4.13
├─ @storybook/addon-controls@6.4.13
├─ @storybook/addon-docs@6.4.13
├─ @storybook/addon-essentials@6.4.13
├─ @storybook/addon-interactions@6.4.13
│ └─ @storybook/instrumenter@6.4.13
├─ @storybook/addon-links@6.4.13
├─ @storybook/addon-measure@6.4.13
├─ @storybook/addon-outline@6.4.13
├─ @storybook/addon-storyshots@6.4.13
├─ @storybook/addon-toolbars@6.4.13
├─ @storybook/addon-viewport@6.4.13
├─ @storybook/addons@6.4.13
├─ @storybook/api@6.4.13
├─ @storybook/babel-plugin-require-context-hook@1.0.1
├─ @storybook/builder-webpack4@6.4.13
├─ @storybook/channel-postmessage@6.4.13
├─ @storybook/channel-websocket@6.4.13
├─ @storybook/channels@6.4.13
├─ @storybook/client-api@6.4.13
├─ @storybook/client-logger@6.4.13
├─ @storybook/components@6.4.13
├─ @storybook/core-client@6.4.13
├─ @storybook/core-common@6.4.13
├─ @storybook/core-events@6.4.13
├─ @storybook/core-server@6.4.13
├─ @storybook/core@6.4.13
├─ @storybook/csf-tools@6.4.13
├─ @storybook/csf@0.0.2--canary.87bc651.0
├─ @storybook/instrumenter@6.4.0-rc.5
│ ├─ @storybook/addons@6.4.0-rc.5
│ ├─ @storybook/api@6.4.0-rc.5
│ ├─ @storybook/channels@6.4.0-rc.5
│ ├─ @storybook/client-logger@6.4.0-rc.5
│ ├─ @storybook/core-events@6.4.0-rc.5
│ ├─ @storybook/router@6.4.0-rc.5
│ └─ @storybook/theming@6.4.0-rc.5
├─ @storybook/manager-webpack4@6.4.13
├─ @storybook/node-logger@6.4.13
├─ @storybook/postinstall@6.4.13
├─ @storybook/preset-create-react-app@3.2.0
├─ @storybook/preview-web@6.4.13
├─ @storybook/react-docgen-typescript-plugin@1.0.2-canary.253f8c1.0
├─ @storybook/react@6.4.13
├─ @storybook/router@6.4.13
├─ @storybook/semver@7.3.2
├─ @storybook/source-loader@6.4.13
├─ @storybook/store@6.4.13
├─ @storybook/testing-library@0.0.7
│ └─ @storybook/client-logger@6.4.0-rc.5
├─ @storybook/testing-react@1.2.3
├─ @storybook/theming@6.4.13
└─ @storybook/ui@6.4.13
こちらでも window, global を console.log してみる
import initStoryshots from '@storybook/addon-storyshots';
console.log('window', typeof window);
// window object
console.log('global', typeof global);
// global object
initStoryshots();
window が存在する!
やはり、window の有無が鍵だな。jest 実行時に window が存在していれば上述したエラーは解消されるはず...
なぜこちらでは window が存在しているのに、自分の project では window が存在しないんだ...?
jest の version 確認 => 26.6.0
└─ jest@26.6.0
エラーが起きている側の jest の version も確認 => 27.4.7
└─ jest@27.4.7
v26 から v27 で何かしらの breaking change があったのか...?
Since Jest 27 "jsdom" isn't the default anymore but "node", see github.com/facebook/jest/pull/9874 and here jestjs.io/blog/2021/05/25/jest-27#flipping-defaults.So you have to set "jsdom" explicitly now.
(StackOverflow)
Jest v27 から "testEnvironment" の default 値変わったらしい! "jsdom" から "node" になったと!
これが原因で window が undefined になっていた可能性高いぞ!
testEnvironment
を jsdom
に設定
jest config の module.exports = {
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/?(*.)+(spec|test).+(ts|tsx|js)',
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
preset: 'ts-jest/presets/js-with-ts',
globals: {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: {
'\\.css$': '<rootDir>/__mocks__/style-mock.ts',
'typeface-roboto': '<rootDir>/__mocks__/style-mock.ts',
},
+ testEnvironment: 'jsdom',
};
いけた!!!!!!!
testEnvironment を jsdom に設定して jest 実行環境で window object にアクセスできるようになったおかげでこのエラー消えた!↓
TypeError: Cannot read properties of undefined (reading 'addEventListener')
無事 snapshot testing 実行されたので完了!
対応まとめ
- ts-jest で ESM support する
- css など parse エラーになるものは jest config の
moduleNameMapper
を使用して mock -
@storybook
関連の package は version を揃える -
@babel/core
,babel-loader
を最新の version にあげる - jest config の
testEnvironment
をjsdom
に設定する
- styled-components によって生成される class name が毎回変更され差分が大きくなってしまう問題
- 全ての story の snapshot が一つの file に出力される問題
があったので対応
最終的にはこう↓
import initStoryshots, {
multiSnapshotWithOptions,
} from '@storybook/addon-storyshots';
import { styleSheetSerializer } from 'jest-styled-components';
initStoryshots({
snapshotSerializers: [styleSheetSerializer],
integrityOptions: { cwd: __dirname }, // it will start searching from the current directory
test: multiSnapshotWithOptions(),
});
styled-components によって生成される class name が毎回変更され差分が大きくなってしまう問題
jest-styled-components の serializer を snapshotserializers
option に追加して対応
全ての story の snapshot が一つの file に出力される問題
test option に multisnapshotwithoptionsoptions
を指定