Closed16

Vue3 での Storybook v6.3.13 から v6.5.14 にアップデートする

yamanokuyamanoku

表題通り v6.3.13 から Play function を使用したく、アップデートを試みようとして悪戦苦闘したログを残す。

yamanokuyamanoku

ディレクトリ構成は以下のようなイメージ

.
├── /components
│   ├── button.stories.js
│   └── button.vue
├── /__tests__
│   ├── /__snapshots__
│   │   └── button.stories.storyshot
│   └── dom.test.js
├── main.js
└── package.json
yamanokuyamanoku

まずは愚直にバージョンを上げてみる

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",

このまま起動・ビルドをすると失敗する

yamanokuyamanoku

core builder の webpack を v5 にすると成功するようになるので設定する。

package.json
    "@storybook/builder-webpack5": "~6.5.14",
    "@storybook/manager-webpack5": "~6.5.14",
main.js
  // Stroybook 6.4 以上にしたときにビルド失敗してしまう
  // webpack5 の builder を用いることで成功するので設定
  core: {
    builder: 'webpack5',
  },
yamanokuyamanoku

併せて @babel/core のバージョンも最新版(アップデート時点)の v7.20.5 にしておく

yamanokuyamanoku

Storybook によるスナップショットテストを導入しているがいくつか問題があったのでその対応

yamanokuyamanoku

スナップショットテストの位置がズレてしまう問題

https://github.com/storybookjs/storybook/issues/18628

yamanokuyamanoku

スナップショットは src/__tests__ 配下に生成されるようにしていたのだが、アップデートしたことによってルートディレクトリ直下に生成されるようになった。

yamanokuyamanoku

この事象がアップデートによってできる破壊的変更かの情報がリリースノートには見つけられなかった。

アップデート前の src/__tests__ 配下に生成するように設定を変える必要がある。

yamanokuyamanoku

addons/storyshots/storyshots-core/src/test-bodies.ts の実装を参考に、カスタマイズした multiSnapshotWithOptions に変更する

src/__tests__/dom.test.js
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
});
yamanokuyamanoku

Vue コンポーネントのスナップショットが別のスナップショットに転移する問題がある

https://github.com/storybookjs/storybook/issues/17005

yamanokuyamanoku

対応方法として @vue/test-utils で mount させる方法があり、そちらを導入してみる

https://github.com/storybookjs/storybook/issues/17005#issuecomment-1013161632

src/__tests__/dom.js
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 が生成されないように設定しておく必要がある。

src/__tests__/dom.js
const wrapper = mount(storyElement, {
  global: {
  // スナップショット内に transition などの stub を生成しないように設定
    stubs: {
      transition: false,
      'transition-group': false,
    },
  },
});
yamanokuyamanoku

これによりスナップショットがほかのスナップショットにずれる問題はなくなったが、 axios といった Promise 処理を含むスナップショットが反映されなくなった。

yamanokuyamanoku

@vue/test-utils には flushPromises というAPIがある。API 通信後の DOM 結果を反映させるために待機させるために使用した。

https://test-utils.vuejs.org/api/#flushpromises

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.

src/__tests__/dom.js
    const storyElement = story.render()
    const wrapper = mount(storyElement)
+   await flushPromises()
    expect(wrapper.element).toMatchSpecificSnapshot(snapshotFileName)

これによる Promise 処理部分のスナップショットが正常に反映されるようになった。

yamanokuyamanoku

しかし連続する Promise 処理が挟まる場合にDOMの結果がうまく反映されなくなったり、<Trainstion> の効果が反映され fade-xxx-xxxx といった CSS class がスナップショットに追加されることがあった。

yamanokuyamanoku

そこで flushPromises ではなく独自で Promise 処理を解決させる関数を作成する。

function sleep(milliSecond) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), milliSecond);
  });
}
src/__tests__/dom.js
// 連続する Promise を処理したい。
// 100に意味はないが無限に Promise を積む場合に終わらなくなるので一定の上限は必要。
// Vue.nextTick() では MSW の終了を待てない。
// flushPromises() を行うべきでも本来は十分であるが、複数回の通信の結果を待ちたいため、過分に待機する
for (let i = 0; i < 100; i++) {
  await sleep(0);
}
// await flushPromises();

これにより、上述した一部うまく反映されない問題が解消された

このスクラップは2022/12/26にクローズされました