【React】Testing Libraryでポップアップの表示テスト

Testing Library学習中ということで、いろんなテストケースを記事にまとめています。

今回はマウスオーバーでポップアップが表示されるケースについてまとめました。
テスト実装中に遭遇したWarningの解消にかなり手こずったので、同様の問題にぶつかっている方がいたら記載した解消法が参考になるかと思います。

https://qiita.com/suzuki0430/items/656213ca221ad75aaee4

https://qiita.com/suzuki0430/items/dfec9532327f6d247a5c

#実装
Terms and Conditionの部分をマウスオーバーしたときに、`No ice cream will actually be delivered`というポップアップが表示されるケースを考えます。
![スクリーンショット 2021-06-05 11.07.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/d87c8478-7d86-9dbf-bacb-ca5e7a6713f8.png)

テストするファイルのコンポーネント`SummaryForm`の中身は以下のようになっています。
スタイルには`react-bootstrap`を使用しています。

```jsx:SummaryForm.jsx
export default function SummaryForm() {
  const [tcChecked, setTcChecked] = useState(false);

  const popover = (
    <Popover id="termsandconditions-popover">
      <Popover.Content>No ice cream will actually be delivered</Popover.Content>
    </Popover>
  );

  const checkboxLabel = (
    <span>
      I agree to
      <OverlayTrigger placement="right" overlay={popover}>
        <span style={{ color: 'blue' }}> Terms and Conditions</span>
      </OverlayTrigger>
    </span>
  );

  return (
    <Form>
      <Form.Group controlId="terms-and-conditions">
        <Form.Check
          type="checkbox"
          checked={tcChecked}
          onChange={(e) => setTcChecked(e.target.checked)}
          label={checkboxLabel}
        />
      </Form.Group>
      <Button variant="primary" type="submit" disabled={!tcChecked}>
        Confirm order
      </Button>
    </Form>
  );
}
```

同じ階層にtestフォルダを作成し、その中に`SummaryForm.test.jsx`を作成します。

```jsx:SummaryForm.test.jsx
test('popover responds to hover', async () => {
  // SummaryFormの仮想DOMにアクセス
  render(<SummaryForm />);

  // ポップアップが初期状態で隠れていれば成功
  const nullPopover = screen.queryByText(
    /no ice cream will actually be delivered/i
  );
  expect(nullPopover).not.toBeInTheDocument();

  // マウスオーバーを模擬
  const termsAndConditions = screen.getByText(/terms and conditions/i);
  userEvent.hover(termsAndConditions);

  // ポップアップが表示されていれば成功
  const popover = screen.getByText(/no ice cream will actually be delivered/i);
  expect(popover).toBeInTheDocument();

  // マウスオーバーの解除を模擬
  userEvent.unhover(termsAndConditions);
  await waitForElementToBeRemoved(() =>
    screen.queryByText(/no ice cream will actually be delivered/i)
  );
});
```

要素へのマウスオーバーのイベント模擬には、userEventを使います。
userEventを使うことで細かいブラウザ操作の模擬を行うことができるため、fireEventよりもテストでの使用に適しています。

https://testing-library.com/docs/ecosystem-user-event/

また、テストファイル内では`queryByText` `getByText`というクエリメソッドを使い分けています。
`query` `get`にあたる部分をコマンド、`Text`にあたる部分をクエリタイプと呼ぶのですが、それぞれの部分で以下のようなことを指定しています。

コマンド

- get: DOMに要素があることを期待しており、要素がなければエラーを返す
- query: DOMに要素がないことを期待しており、要素がなければnullを返す
- find: 要素が非同期で現れることを期待しており、Promiseを返す

クエリタイプ

- Role: すべての要素
- AltText: imgやinputなどのText以外の要素
- Text: divやspanなどのForm外の要素

クエリタイプの使い分けについては、公式Docsに記載されている優先度に従います。

https://testing-library.com/docs/queries/about/

最後に、`await waitForElementToBeRemoved`の記述についての補足です。
アサーションについては`expect(popover).toBeInTheDocument()`で終わっているのですが、`userEvent.unhover(termsAndConditions)`以降の記述がないと以下のWarningに遭遇します。

![スクリーンショット 2021-06-05 9.48.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5df0fbd2-1e3b-3459-6d84-35a40df9ae9d.png)

このWarningはテスト内でDOMの更新を行った場合に発生します。
ただ、Warningに従ってact()でのラッピングを行う必要はなく(Testing Libraryですでに行っているため)、代わりにテスト終了後の状態を決めるような操作を行ってあげる必要があります。

`userEvent.unhover(termsAndConditions)`でマウスオーバーを解除したときのイベントを発生させ、`await waitForElementToBeRemoved(...)`で要素が消えた後のコールバックを非同期で実行しています。

#参考資料

https://www.udemy.com/share/1040ZSCEEZd11VRno=/