NITS TIPS

~微塵集合知巧~

StorybookのInteractionテストでcreatePortalした要素にアクセスできない時の対処法

最終更新日: 2024-04-08記事投稿日: 2024-04-08

nobumitsu-1995のgithubトップ

Frontend developer

nobumitsu-1995


概要

storybookのplay関数を使用したり

  • @storybook/addon-interaction
  • @storybook/test-runner
  • @storybook/test

などのaddonを導入することで、実際のブラウザ上での挙動のテストを実行することができます。(このセットアップや運用方法については、本記事では解説しません。)

今回は以下のようなテストを書いていました。

import { within, userEvent, expect } from '@storybook/test'

export default {
  title: 'Component/Review',
  component: Review
}

export const Default: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    await delay(1000)
    await userEvent.click(canvas.getByRole('button', { name: 'すべてのレビュー' })) // この操作でレビューが見れるモーダルがオープンする。
    await delay(1000)
    await userEvent.click(canvas.getByRole('button', { name: '不正なレビューを報告する' })) // ここで押下するボタンはモーダル内に存在する。今回はこの操作がうまく動かなかった。
  }
}

原因

今回の原因は、userEvent.click(canvas.getByRole('button', { name: 'すべてのレビュー' }))この処理で開かれるモーダルがReactのcreatePortalを使用していたことでした。

とはいえ、createPortalを使用していると必ず、この不具合が起きるのかというとそういうわけではありません。

この不具合が起きる条件を理解するにはまず、storyがどのように描画されているのかを知る必要があります。Storyは以下のようにiframeを利用して描画されています。その中にhtml, head, bodyがあり、<div id='storybook-root'>の中に設定したStoryの内容が描画されている形になります。

次に、const canvas = within(canvasElement)この部分に注目してみます。この部分が何をしているのかというと、#storybook-rootの要素を取得しています。つまり、canvasに入ってくるのは<div id='storybook-root'>になり、これに対してgetByRoleをしたり、userEvent.clickをしていることになります。

お気づきの方もいらっしゃるかもですが、createPortal(<Modal />, document.body)で描画されるModalはこのcanvasの外の要素になります。それゆえにモーダル内の要素にアクセスできなかったようです👀

つまり<div id='storybook-root'>外の要素にはアクセスできないというのが正しそうです。

解決方法

この問題はすでにissueが上がっているようでした。(2024年4月8日現在未解決)

その中のコメントにある方法を使用して以下のように修正したところ正常にPASSするテストになりました!

import { within, userEvent, expect }

export default {
  title: 'Component/Review',
  component: Review
}

export const Default: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    await delay(1000)
    await userEvent.click(canvas.getByRole('button', { name: 'すべてのレビュー' }))

    const body = within(canvasElement.ownerDocument.body)
    await delay(1000)
    await userEvent.click(body.getByRole('button', { name: '不正なレビューを報告する' }))
  }
}

参考文献


関連記事

  • 最終更新日: 2024-08-06記事投稿日: 2024-08-06

    ARIA属性を活用したアクセシブルなモーダル実装

    アクセシブルなmodalを実装するために必要なaria属性が意外と多い。参考となるコードをメモとして残しておきたい。

  • 最終更新日: 2024-07-27記事投稿日: 2024-07-27

    swiper/reactで実装したカルーセルをTAB操作した時、画面に描画されていないスライドにフォーカスが当たってしまう

    swiper/reactで実装したカルーセルをTABキーで操作した時、画面に描画されていないスライドにもフォーカスが当たってしまう問題があり、それの修正対応をしたので備忘録として残しておく。

  • 最終更新日: 2024-07-09記事投稿日: 2024-07-09

    PayloadとArgumentの使い分け方

    typescriptで型を宣言するとき〇〇Payloadとか××Argumentとか名付けることが多いがその違いについて真面目に考えたことがなかったので調べてみた。

  • 最終更新日: 2024-05-06記事投稿日: 2024-05-06

    初回レンダリング時のみ、useEffect内の処理を実行しないようにする方法

    コンポーネントの初回レンダリング時のみ、useEffect内の処理を実行したくないという場面で、どのような処理を加えれば実現できるかを紹介します。