Vitest, React Testing Library, MSWなどを使ってみてハマったポイントについて

この記事は、[Unipos Advent Calendar 2024](https://qiita.com/advent-calendar/2024/unipos) の3日目の記事です。

---

WebフロントエンドのテストをVitest, React Testing Library, MSWなどを使って書いていた時にハマったケースについてここにまとめておきます。

## 複数のSuspenseしているコンポーネントがある時に全てを待ちたい

例えば、テストしたいコンポーネントの中に、複数のSuspenseしているコンポーネントが含まれているとします。Suspenseのfallbackにて、role="status"が設定してあるローディングのコンポーネントもあるとします。この場合に全てのSuspenseが終了するまで待ちたいというケース。

```typescript
await waitForElementToBeRemoved(() => screen.getAllByRole('status'));
```

## userEvent.clickの完了で通信も一緒に終わってしまう場合を避けたい

例えば、あるボタンがあり、それをクリックすると通信が発生するとします。そのボタンのクリックをテストで書くと、`await userEvent.click`になりますが、一緒に通信も終わる扱いになってしまい、通信中になっているかどうかを判定できなくなるみたいなケース。

```typescript
it('userEvent.clickの完了で通信も一緒に終わってしまう場合を避けたい', async () => {
  expect.hasAssertions();

  vi.useFakeTimers({
    toFake: ['setTimeout'],
    shouldAdvanceTime: true,
  });
  server.use(
    http.get('/book/:bookId', async () => {
      // NOTE: userEvent.clickでチェックをつける時に、通信も一緒に実行されるのを防ぐため、遅延を入れる。
      // 秒数はfakeTimerを利用しているのでそれなりに遅延があればなんでも良い。
      await delay(10000);
      return new Response()
    }),
  );

  render(<Component />)

  // ボタンのクリックだけは終わらせる
  await userEvent.click(screen.getByRole('button'));

  expect(~~~); // 通信中の状態

  // タイマーを進めて通信を完了扱いにする
  await act(async () => {
    await vi.runOnlyPendingTimersAsync(); // ここは場合によって違うかも
  });

  expect(~~~); // 通信後の状態

  vi.useRealTimers();
});
```

## 実装の都合上、確定で`console.error`が出てしまう場合にログを出ないようにしたい

これで強制的に非表示にはできるが根本的にはよくなさそうです。

```typescript
vi.spyOn(console, 'error').mockImplementation(() => {});
```

## 文字列の配列があり、要素ごとに指定した文字列が文中にあるかどうかだけ確認したい場合

例えば、`~~~タイトル1~~~`みたいな感じでテキストを取得してきた場合に、`タイトルn(nに数値が入る)`という部分だけあれば良いんだがということを確認したいケース。

```typescript
expect(stringArray.map((longText) => longText.textContent)).toStrictEqual(
  expect.arrayContaining([
    expect.stringContaining('タイトル1'),
    expect.stringContaining('タイトル2'),
    expect.stringContaining('タイトル3'),
  ]),
);
```

## 現在の時刻をモックしたい

こういう感じに書けば良いことがわかりました。

```typescript
vi.spyOn(Date, 'now').mockReturnValue(new Date('2022-12-01T00:00:00').getTime());
```

下記の方法だと、モックできるっぽいんですが、テスト実行中に現在時刻がモックした時間から経過するようでした。

```typescript
vi.setSystemTime(new Date('2022-12-01T00:00:00').getTime());
```

## そもそもscreen使った方がいいのか

`render`の戻り値を使った方がいいのか、importしてきた`screen`を使った方が良いのか分からない時期がありました。

そういう形におすすめ下記の翻訳記事

https://qiita.com/kobanyan/items/126512ec3e8d76c538b3 


## setTimeoutの中にPromiseの関数があったとき

どうもタイマーが進まないと思ったら利用する関数が違うらしい。公式ドキュメントは大事。

```typescript
await act(async () => {
  await vi.advanceTimersByTimeAsync(1000);
});
```

https://vitest.dev/api/vi.html#vi-advancetimersbytimeasync

## React Testing Libraryの screen.debug() でたくさん行を表示したい

割と行数が短くてどんな状態か分からないことが結構あったんですが、debug関数の第二引数を変更することで対応できるようです。

```typescript
screen.debug(undefined, Infinity);
```

https://stackoverflow.com/questions/59763739/react-testing-library-some-portion-of-debugs-output-is-not-visible


## 事前にレンダリングしていたものが、何らかの通信後、消えたかどうか確かめたい

先に`waitForElementToBeRemoved`をしておいて、通信が発生する処理を跨いだ後に、`waitForElementToBeRemoved`をawaitしておけば良いみたいです。

```typescript
const isRemovedPromise = waitForElementToBeRemoved(screen.getByRole("dialog"));
await userEvent.click(screen.getByRole('button'); // 通信が発生する
await isRemovedPromise;
```

`waitForElementToBeRemoved(screen.getByRole("dialog"))`部分ですが、`waitForElementToBeRemoved(() => screen.getByRole("dialog"));`とした時に動作することあったので、どういう原理なのか確かめたいです。

参考にしたもの

https://github.com/testing-library/dom-testing-library/issues/1265#issuecomment-2027979370