Vue3 での Storybook v6.3.13 から v6.5.14 にアップデートする
表題通り v6.3.13 から Play function を使用したく、アップデートを試みようとして悪戦苦闘したログを残す。
ディレクトリ構成は以下のようなイメージ
.
├── /components
│ ├── button.stories.js
│ └── button.vue
├── /__tests__
│ ├── /__snapshots__
│ │ └── button.stories.storyshot
│ └── dom.test.js
├── main.js
└── package.json
まずは愚直にバージョンを上げてみる
- "@storybook/addon-actions": "~6.3.13",
+ "@storybook/addon-actions": "~6.5.14",
- "@storybook/addon-controls": "~6.3.13",
+ "@storybook/addon-controls": "~6.5.14",
- "@storybook/addon-storyshots": "~6.3.13",
+ "@storybook/addon-storyshots": "~6.5.14",
- "@storybook/source-loader": "~6.3.13",
+ "@storybook/source-loader": "~6.5.14",
- "@storybook/vue3": "~6.3.13",
+ "@storybook/vue3": "~6.5.14",
このまま起動・ビルドをすると失敗する
core builder の webpack を v5 にすると成功するようになるので設定する。
"@storybook/builder-webpack5": "~6.5.14",
"@storybook/manager-webpack5": "~6.5.14",
// Stroybook 6.4 以上にしたときにビルド失敗してしまう
// webpack5 の builder を用いることで成功するので設定
core: {
builder: 'webpack5',
},
併せて @babel/core
のバージョンも最新版(アップデート時点)の v7.20.5 にしておく
Storybook によるスナップショットテストを導入しているがいくつか問題があったのでその対応
スナップショットテストの位置がズレてしまう問題
スナップショットは src/__tests__
配下に生成されるようにしていたのだが、アップデートしたことによってルートディレクトリ直下に生成されるようになった。
この事象がアップデートによってできる破壊的変更かの情報がリリースノートには見つけられなかった。
アップデート前の src/__tests__
配下に生成するように設定を変える必要がある。
addons/storyshots/storyshots-core/src/test-bodies.ts の実装を参考に、カスタマイズした multiSnapshotWithOptions に変更する
const isFunction = (obj) => !!(obj && obj.constructor && obj.call && obj.apply);
const optionsOrCallOptions = (opts, story) => (isFunction(opts) ? opts(story) : opts);
function snapshotWithOptions(options) {
return async ({ story, context, renderTree, snapshotFileName }) => {
const result = renderTree(story, context, optionsOrCallOptions(options, story));
function match(tree) {
let target = tree;
const isReact = story.parameters.framework === 'react';
if (isReact && typeof tree.childAt === 'function') {
target = tree.childAt(0);
}
if (isReact && Array.isArray(tree.children)) {
[target] = tree.children;
}
if (snapshotFileName) {
expect(target).toMatchSpecificSnapshot(snapshotFileName);
} else {
expect(target).toMatchSnapshot();
}
if (typeof tree.unmount === 'function') {
tree.unmount();
}
}
if (typeof result.then === 'function') {
return result.then(match);
}
return match(result);
};
}
function multiSnapshotWithOptions(options = {}) {
return async ({ story, context, done, renderTree, stories2snapsConverter }) => {
const snapshotFileName = stories2snapsConverter.getSnapshotFileName(context);
// snapshots の生成位置がルートディレクトリ直下にできてしてしまう
// そのため生成位置を src/__tests__ 配下になるようにする
// ref: https://github.com/storybookjs/storybook/issues/18628
const newSnapshotFileName = `${__dirname}/${snapshotFileName.substring(
'../../components/'.length,
)}`;
await snapshotWithOptions(options)({ story, context, renderTree, snapshotFileName: newSnapshotFileName });
done();
};
}
const test = multiSnapshotWithOptions();
initStoryshots({
test
});
Vue コンポーネントのスナップショットが別のスナップショットに転移する問題がある
対応方法として @vue/test-utils
で mount させる方法があり、そちらを導入してみる
https://github.com/storybookjs/storybook/issues/17005#issuecomment-1013161632
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots'
import { mount } from '@vue/test-utils'
import { resolve } from 'path'
initStoryshots({
test: ({ story, context, stories2snapsConverter }) => {
const snapshotFileName = resolve(
__dirname,
stories2snapsConverter.getSnapshotFileName(context)
)
const storyElement = story.render()
const wrapper = mount(storyElement)
expect(wrapper.element).toMatchSpecificSnapshot(snapshotFileName)
},
})
<Transition>
や <transition-group>
がある場合は stub が生成されないように設定しておく必要がある。
const wrapper = mount(storyElement, {
global: {
// スナップショット内に transition などの stub を生成しないように設定
stubs: {
transition: false,
'transition-group': false,
},
},
});
これによりスナップショットがほかのスナップショットにずれる問題はなくなったが、 axios といった Promise 処理を含むスナップショットが反映されなくなった。
@vue/test-utils
には flushPromises
というAPIがある。API 通信後の DOM 結果を反映させるために待機させるために使用した。
flushPromises
flushes all resolved promise handlers. This helps make sure async operations such as promises or DOM updates have happened before asserting against them.Check out Making HTTP requests to see an example of
flushPromises
in action.
const storyElement = story.render()
const wrapper = mount(storyElement)
+ await flushPromises()
expect(wrapper.element).toMatchSpecificSnapshot(snapshotFileName)
これによる Promise 処理部分のスナップショットが正常に反映されるようになった。
しかし連続する Promise 処理が挟まる場合にDOMの結果がうまく反映されなくなったり、<Trainstion>
の効果が反映され fade-xxx-xxxx
といった CSS class がスナップショットに追加されることがあった。
そこで flushPromises ではなく独自で Promise 処理を解決させる関数を作成する。
function sleep(milliSecond) {
return new Promise((resolve) => {
setTimeout(() => resolve(), milliSecond);
});
}
// 連続する Promise を処理したい。
// 100に意味はないが無限に Promise を積む場合に終わらなくなるので一定の上限は必要。
// Vue.nextTick() では MSW の終了を待てない。
// flushPromises() を行うべきでも本来は十分であるが、複数回の通信の結果を待ちたいため、過分に待機する
for (let i = 0; i < 100; i++) {
await sleep(0);
}
// await flushPromises();
これにより、上述した一部うまく反映されない問題が解消された