😊

@storybook/addon-interactionsがどのようにspyを設定するか確認したときのメモ

2022/01/26に公開

背景

@storybook/addon-interactionsには@storybook/addon-actionsと連携して、アクションにjest.fn()を挿入してくれる機能がある。これをcomposeStoryでも使いたいがどういう仕組になっているか確認する。

結論

雑に言うと、Storyのargsを読み取り、@storybook/addon-actionsによって追加されたプロパティがあった場合、そこにjest.fn()を挿入する。これは@storybook/addon-interactionsで行われる。
背景で書いた、composeStoryで利用する場合は、これらの近いことをする必要がある。なぜなら、composeStoryではaddonには全く依存しておらず、利用していないためである。つまり、actionsやinteractionsでやっていることと同様のことをする必要がある。

なお、難しくはないが、少し面倒ではある。コンポーネントにわたすプロパティのargTypesの自動生成と、argTypesを読み直してargsにjest.fn()を追加してやれば実現できるだろう。

もっと詳しく

    if (typeof val === 'function' && val.name === 'actionHandler') {
      Object.defineProperty(val, 'name', { value: key, writable: false });
      Object.defineProperty(val, '__storyId__', { value: id, writable: false });
      acc[key] = action(val);
      spies.push(acc[key]);
      return acc;
    }
    acc[key] = val;

https://github.com/storybookjs/storybook/blob/v6.4.14/addons/interactions/src/preset/argsEnhancers.ts#L19-L26

action(val)が実質jest.fn(val)である。instrumentはinteractionに必要な追加処理を加える高階関数と捉えて問題ないだろう。

const { action } = instrument({ action: fn }, { retain: true });

https://github.com/storybookjs/storybook/blob/v6.4.14/addons/interactions/src/preset/argsEnhancers.ts#L8

@storybook/addon-actionsによって追加されたプロパティは関数で、actionHandlerという名前である。
https://github.com/storybookjs/storybook/blob/v6.4.14/addons/actions/src/preview/action.ts#L47
これをどこで加えているかはaddArgsHelpers.tsで定義されている関数をみてみると、@storybook/addon-actionsに利用するargsTypesをループで回してnameを確認したり、parameters.actions.argTypesRegexを利用している様子が伺える。
https://github.com/storybookjs/storybook/blob/v6.4.14/addons/actions/src/preset/addArgsHelpers.ts

例えば、inferActionsFromArgTypesRegexではargTypesをentriesでループを回し、argTypesRegexにマッチするか確認し、action関数でactionHandlerを生成し、argsに加えている。

  const argTypesRegex = new RegExp(actions.argTypesRegex);
  const argTypesMatchingRegex = Object.entries(argTypes).filter(
    ([name]) => !!argTypesRegex.test(name)
  );
    return argTypesMatchingRegex.reduce((acc, [name, argType]) => {
    if (typeof initialArgs[name] === 'undefined') {
      acc[name] = action(name);
    }
    return acc;
  }, {} as Args);

argTypesは別途react-docgen-typescriptのようなライブラリなので追加されていると考えられる。

Discussion