🎉

Material-UIのIconコンポーネントに対してgetByRole()すると失敗したので調べてみた話

2022/12/09に公開

この記事はコネヒトアドベントカレンダー9日目の記事です。

はじめに

Material-UI を使った Icon コンポーネントに対して、React Testing LibrarygetByRole() を使ったテストに失敗したので調べてみました。

事象

Material-UI の Icon コンポーネントを意味的に識別するために role="img" を使用して

SampleIcon.tsx
import ChatIcon from '@material-ui/icons/Chat'
import React from 'react'

export const SampleIcon: React.FC = () => {
  return <ChatIcon role="img" />
}

次のようなテストコードを実行すると、

SampleIcon.test.tsx
import { render, screen } from '@testing-library/react'
import { SampleIcon } from 'components/atoms/SampleIcon'
import React from 'react'

describe('<SampleIcon />', () => {
  test('should render component', async () => {
    render(<SampleIcon />)
    expect(screen.getByRole('img')).toBeInTheDocument() // ❌ 要素の取得に失敗する 
  })
})

role を設定しているのにも関わらずテストに失敗します。エラー内容は以下の通りで、role が見つからないとエラーが発生しています。

 FAIL  client/src/components/atoms/SampleIcon/SampleIcon.test.tsx
  <SampleIcon />
    ✕ should render component (46 ms)

  ● <SampleIcon /> › should render component

    TestingLibraryElementError: Unable to find an accessible element with the role "img"

    There are no accessible roles. But there might be some inaccessible roles. If you wish to access them, then set the `hidden` option to `true`. Learn more about this here: https://testing-library.com/docs/dom-testing-library/api-queries#byrole

    Ignored nodes: comments, script, style
    <body>
      <div>
        <svg
          aria-hidden="true"
          class="MuiSvgIcon-root"
          focusable="false"
          role="img"
          viewBox="0 0 24 24"
        >
          <path
            d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"
          />
        </svg>
      </div>
    </body>

       6 |   test('should render component', async () => {
       7 |     render(<SampleIcon />)
    >  8 |     expect(screen.getByRole('img')).toBeInTheDocument()
         |                   ^
       9 |   })
      10 | })
      11 |

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:40:19)
      at node_modules/@testing-library/dom/dist/query-helpers.js:90:38
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at node_modules/@testing-library/dom/dist/query-helpers.js:111:19
      at Object.getByRole (client/src/components/atoms/SampleIcon/SampleIcon.test.tsx:8:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.743 s, estimated 8 s

詳しくエラー内容を見てみると、role="img" はついており、さらにaria-hidden="true" もついていることがわかります。aria-hidden="true" が付与されることで、スクリーンリーダーに読み上げられないように要素が隠されていることがわかります。

<body>
  <div>
    <svg
      aria-hidden="true"
      class="MuiSvgIcon-root"
      focusable="false"
      role="img"
      viewBox="0 0 24 24"
    >
    <path
      d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"
    />
    </svg>
  </div>
</body>

どうして aria-hidden="true" が付与されるのか

Material-UI Icon コンポーネントのアクセシビリティにて言及されています。
https://mui.com/material-ui/icons/#accessibility

Material-UI の Icon コンポーネントには2種類あり、1つはブランドイメージのような視覚的なアイコン。もう1つはボタン、フォームなどを意味を示すアイコン。視覚的なアイコンを表すものに関しては、aria-hidden="true" が追加されるようになっています。

実際に試してみると、IconButton のような意味を示す Icon コンポーネントはテストが通ります。

SampleIcon.tsx
export const SampleIcon: React.FC = () => {
  return <IconButton role="img" />
}
SampleIcon.test.tsx
describe('<SampleIcon />', () => {
  test('should render component', async () => {
    render(<SampleIcon />)
    expect(screen.getByRole('img')).toBeInTheDocument() // ✅ OK
  })
})

一方で、視覚的な ChatIcon のような Icon コンポーネントはテストが通りませんでした。

SampleIcon.tsx
export const SampleIcon: React.FC = () => {
  return <ChatIcon role="img" />
}
SampleIcon.test.tsx
describe('<SampleIcon />', () => {
  test('should render component', async () => {
    render(<SampleIcon />)
    expect(screen.getByRole('img')).toBeInTheDocument() // ❌ NG
  })
})

ではどうやって、getByRole()できるようにするか

視覚的な Icon に対しては、スクリーンリーダーでアイコン部分を読み上げられないように、Material-UI 側でaria-hidden="true"がつけられていたと思われますので、利用する Icon に意味があるのであれば、aria-hidden="false"することで解決できそうです。

SampleIcon.tsx
export const SampleIcon: React.FC = () => {
  return (
    <ChatIcon role="img" aria-hidden="false" />
  )
}
SampleIcon.test.tsx
describe('<SampleIcon />', () => {
  test('should render component', async () => {
    render(<SampleIcon />)
    expect(screen.getByRole('img')).toBeInTheDocument() // ✅ OK
  })
})

終わりに

この記事では、Material-UIのIconコンポーネントに対してgetByRole()すると失敗したので、なぜ失敗するのか調べてみました。この機会にアクセシビリティについてもっと理解を深めていこうと思います。

明日の担当は @aboy さんです!投稿を楽しみにしております。

Discussion