【React】GitHub Actionsで通っているテストがローカルで通らなくなった

# はじめに
ローカルで通ったテストがデプロイ環境では通らない、ということはよくあるようで、検索するとQiita記事もよく出てきます。しかし、その逆はあまりないようで、検索してもあまりHitしません。

今回、[この記事](https://qiita.com/IchidaU/items/9b9abdd88e098192ee94 "初めて作ったアプリを改良してみた")を書くにあたってそのエラーに遭遇して解決に苦慮したため、備忘録を兼ねて記事にします。

# 事象
- デプロイした学習記録アプリの`index.html`のtitleを変更したところ、一度パスしたはずの記録追加テストがローカル環境下で通らなくなっていた
- エラーメッセージは下記の通り
```
TestingLibraryElementError: Unable to find an element by: [data-testid="log"] 
```
- `index.html`の変更を元に戻しても解消せず、エラー内容的にも関係なさそう

<details>
<summary>該当コード</summary>

```react:addRecord.test.jsx
import '@testing-library/jest-dom';
import { screen, fireEvent, render, waitFor } from '@testing-library/react';
import App from '../App';
import { TotalTimeProvider } from '../Providers/TotalTimeProvider';
import { addRecord, getAllLogs } from '../utils/supabaseFunctions';

jest.setTimeout(20000);

const mockLogs = [{ id: 1, title: 'test', time: 1 }];

jest.mock('../utils/supabaseFunctions', () => ({
  getAllLogs: jest.fn(() => Promise.resolve(mockLogs)),
  addRecord: jest.fn((title, time) => {
    const newLog = { id: mockLogs.length + 1, title, time };
    mockLogs.push(newLog);
    return Promise.resolve(newLog);
  }),
}));

describe('動作テスト', () => {
  it('記録追加', async () => {
    render(
      <TotalTimeProvider>
        <App />
      </TotalTimeProvider>
    );

    await waitFor(
      () => {
        expect(screen.getByTestId('log')).toHaveTextContent('test 1時間');
      },
      { timeout: 5000 }
    );

    fireEvent.change(screen.getByLabelText('学習内容'), {
      target: { value: 'test2' },
    });
    fireEvent.change(screen.getByLabelText('学習時間'), {
      target: { value: '2' },
    });
    fireEvent.click(screen.getByText('登録'));

    await waitFor(
      () => {
        const logs = screen.getAllByTestId('log');
        expect(logs).toHaveLength(2);
      },
      { timeout: 5000 }
    );

    const logs = screen.getAllByTestId('log');
    expect(logs[1]).toHaveTextContent('test2 2時間');

    expect(addRecord).toHaveBeenCalledWith('test2', 2);
    expect(getAllLogs).toHaveBeenCalledTimes(2);
  });
});

```

</details>

# 解決策
- Loading画面が消えるまで次の処理を待つようにした
- `render`と`fireEvent`を`act`で囲んだ

```react:addRecord.test.jsx
import {
  screen,
  fireEvent,
  render,
  waitFor,
  act,
} from '@testing-library/react';

（中略）

    await act(async () => {
      render(
        <TotalTimeProvider>
          <App />
        </TotalTimeProvider>
      );
    });

    await waitFor(
      () => {
        expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
      },
      { timeout: 5000 }
    );
    
（中略）

    await act(async () => {
      fireEvent.change(screen.getByLabelText('学習内容'), {
        target: { value: 'test2' },
      });
      fireEvent.change(screen.getByLabelText('学習時間'), {
        target: { value: '2' },
      });
      fireEvent.click(screen.getByText('登録'));
    });

（以下略）
```

## エラーは解消したが……
- `render`とか`fireEvent`って`act`で囲む必要ないんじゃなかったっけ？

https://bufferings.hatenablog.com/entry/2021/11/18/015809

- `timeout`多すぎやろ
- もっと良い方法あるよね

というわけでこれをさらに改善することにしました。

## 改善
- そもそも`fireEvent`よりは`userEvent`使ったほうがいいらしい、ということなのでそちらに変更

https://zenn.dev/k_log24/articles/4c1cd37ff0ca50

https://zenn.dev/tnyo43/scraps/6d15ee29867b7e

- これでテストが通ったので、`timeout`処理もなるべく削除

<details><summary>改善後のコード全体</summary>

```react:addRecord.test.jsx
import '@testing-library/jest-dom';
import { screen, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../App';
import { TotalTimeProvider } from '../Providers/TotalTimeProvider';
import { addRecord, getAllLogs } from '../utils/supabaseFunctions';

const mockLogs = [{ id: 1, title: 'test', time: 1 }];

jest.mock('../utils/supabaseFunctions', () => ({
  getAllLogs: jest.fn(() => Promise.resolve(mockLogs)),
  addRecord: jest.fn((title, time) => {
    const newLog = { id: mockLogs.length + 1, title, time };
    mockLogs.push(newLog);
    return Promise.resolve(newLog);
  }),
}));

describe('動作テスト', () => {
  it('記録追加', async () => {
    const user = userEvent.setup();

    render(
      <TotalTimeProvider>
        <App />
      </TotalTimeProvider>
    );

    await waitFor(() => {
      expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
    });

    await waitFor(() => {
      expect(screen.getByTestId('log')).toHaveTextContent('test 1時間');
    });

    await user.type(screen.getByLabelText('学習内容'), 'test2');
    await user.type(screen.getByLabelText('学習時間'), '2');
    await user.click(screen.getByText('登録'));

    await waitFor(() => {
      const logs = screen.getAllByTestId('log');
      expect(logs).toHaveLength(2);
    });

    const logs = screen.getAllByTestId('log');
    expect(logs[1]).toHaveTextContent('test2 2時間');

    expect(addRecord).toHaveBeenCalledWith('test2', 2);
    expect(getAllLogs).toHaveBeenCalledTimes(2);
  });
});

```

</details>

# おわりに
これで一応エラーも解消し、ついでにコードもすっきりしてめでたしめでたし……と言いたいところなのですが、結局一度通過したテストがエラー吐いた理由がはっきりせず、その点はもやもやしています。