Open5

React Testing Library では fireEvent よりも userEvent を使ったほうがいいらしい

かしかし

ここに書いてあった

https://www.robinwieruch.de/react-testing-library/#react-testing-library-user-event

Whenever possible, use userEvent over fireEvent when using React Testing Library. At the time of writing this, userEvent doesn't include all the features of fireEvent, however, this may change in the future.

React Testing Library を使う際には、可能な限り fireEvent よりも userEvent の方を使ったほうがいいよ。
ただし、現時点において fireEvent にできて userEvent にできないこともあるよ(今後の開発に期待してね)。

みたいなことを書いてる。

詳しく調べてみる。

環境

@testing-library/react: v13.4.0
@testing-library/user-event: v14.4.3

かしかし

fireEvent

https://testing-library.com/docs/dom-testing-library/api-events

冒頭に @testing-library/user-event 使ったほうがええぞって書いてる。

Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.

fireEvent という API が提供されていて、これを使ってイベントを発火させる。

fireEvent[eventName](node: HTMLElement, eventProperties: Object)

DOM のイベントを発火させるメソッド。
第一引数に対象となるノード、第二引数にイベントのプロパティを指定する。

ここに列挙されたイベントを使える。
createEvent によって、それぞれのイベントを表現するオブジェクトと fireEvent の第二引数で指定した値からイベントが生成される。

イベントの発火の処理は dispatchEvent を使ってる。

参考
https://github.com/testing-library/dom-testing-library/blob/91b9dc3b6f5deea88028e97aab15b3b9f3289a2a/src/events.js#L5-L19

かしかし

fireEvent (続き)

createEvent という API も提供されていて、好きなようにカスタマイズしたイベントを生成できる。
https://testing-library.com/docs/dom-testing-library/api-events#createeventeventname

サンプルにあるように、複数の DOM に対して同じイベントを走らせたいときにはいいかもしれない

const simulateImageInput = (inpout) => {
  fireEvent(
    input,
    createEvent('input', input, {
      target: {files: inputFiles},
      ...init,
    }),
  );
};

とはいえこれは次のように書いても実現できるので createEvent って結局いつ使うの?という気持ちになった。

const simulateImageInput = (inpout) => {
  fireEvent.upload(
    input,
    {
      target: {files: inputFiles},
      ...init,
    },
  );
};
かしかし

user-event

ブラウザでインタラクションによって生じるイベント発火させてシミュレーションをするための Testing Library 付属のライブラリ。

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

fireEvent との差分

以下のようなことが書かれている。
fireEvent は DOM のイベントを発火させるだけの軽量なラッパーで、ブラウザの低レイヤの API である dispatchEvent を使ってイベントの発火を実現している。
一方、ブラウザは一つのインタラクションに対して2つ以上のイベントを発火させることもある。(たとえばテキストボックスに文字を入力する場合、 input の DOM にフォーカスをして、キーボードと input のイベントが発火して、値がタイプした通りに変化する)。

user-event を使うと、具体的なイベントではなくユーザインタラクションそのものを記述できる。
しかもそのインタラクションの途中で、その DOM のブラウザでの表示状態やインタラクションの状態の判断もやってくれる。

つまり単一のイベントを発火させる fireEvent を使ってインタラクションに応じていくつものイベントを扱うブラウザの挙動を再現するのが難しいというのが問題点らしい。
そして、 より実際のブラウザに近い挙動を再現できる user-event の利用が推奨されている。

ただし、いくつかはまだ挙動が再現しきれていないので、そういう場合には fireEvent を使って頑張ってくれってことらしい。

fireEvent と user-event で差が出る例

ここで言及されている。

https://ph-fritsche.github.io/blog/post/why-userevent

onKeyDown のイベントが preventDefault によってキャンセルされて input に伝わらなくしたような場合においても、 fireEvent で input に直接イベントを発火させる方法なら入力させられる。

test("KeyDown させないラッパーで囲っても、テキストフィールドに fireEvent で値を入れるとノードの value に反映される", async () => {
  render(<MyTextField canType={false} />);

  const input = screen.getByRole("textbox");
  fireEvent.change(input, { target: { value: "bar" } });

  expect(input).toHaveValue("bar");
});

一方、userEvent を使って input にフォーカスしてからキーボードをタイプするという実際のブラウザを使ったユーザインタラクションに近い挙動でテストすると、 input に値を入力できないことも再現できている。

test("KeyDown させないラッパーで囲っていると、 input をクリックしてテキストを入力してもノードの value に反映されない", async () => {
  render(<MyTextField canType={false} />);

  const user = userEvent.setup();
  const input = screen.getByRole("textbox");

  await user.tripleClick(input);
  await user.keyboard("bar");

  expect(input).toHaveValue("foo");
});

https://codesandbox.io/s/fireevent-vs-user-event-gltv08?file=/src/MyTextField.test.tsx

かしかし

use-event の中身を見てみると実際に要素に対してインタラクションを発生させてた場合を再現しているのがわかる。

たとえば、ダブルクリックのインタラクション(dblClick)では「対象を element とし、マウスの左クリックを2回実行」している。

https://github.com/testing-library/user-event/blob/1aa2027e5ec445ab413808556efa7763b65053d3/src/convenience/click.ts#L14-L19

また、文字列の入力のインタラクション(keyboard)では「text を適切に字句ごとに分割して、一つずつキーボードで入力」していることも読み取れる。

https://github.com/testing-library/user-event/blob/1aa2027e5ec445ab413808556efa7763b65053d3/src/keyboard/index.ts#L13-L21

use-event は再現できていないインタラクションもあるとあったけど、うまく再現するのが難しいパターンに関してはやってないということかなあと思った。
user-event で実装されてないインタラクションとして、 Issue に次のようなものがあった(2022/9/18時点)

  • Shit + 矢印のキーボード入力で範囲選択できない (#966)
  • touch がない (#880)
  • type="range" の選択ができない(#871

など

出展
https://github.com/testing-library/user-event/issues?q=is%3Aopen+label%3Aaccuracy%2Cenhancement