ReactアプリをPlaywrightでテストする〜Page編〜

playwrightで、どうやるのか悩んだものや、便利な機能だなと思ったものをピックアップしてご紹介したいと思います。

## ルーティング機能で通信部分をモック化する

[page.route](https://playwright.dev/docs/api/class-page#page-route) は、指定したURLにマッチングしたリクエストを変更する機能です。
fronendがbackendなど他外部サイトとHTTP通信している部分をモック化できます。
この機能を使って、backendと通信せずに色々なレスポンスパターンのテストを実施できました。

```ts
async callMock() {
  await page.route('/hoge/fuga', async () => {
    await route.fullfill({
      headers: {
      },
      status: 200,
      body: '{"list": []}',
    });
  });
};
```

backendのベースURLをconfigに指定している場合は、そのbaseURLを除いたパスを指定することができます。その場合、backend以外の外部接続先であれば、フルパスでURLを書くことになります。

異常レスポンスを返したい場合は、abort()を呼び出せば良いようです。

```ts
async callMock() {
  await page.route('/hoge/fuga', async () => {
    await route.abort();
  });
};
```


## ファイル選択、ドラッグ＆ドロップ

type=fileのinput部品でファイル選択させる方法です。
[FileChooser](https://playwright.dev/docs/api/class-filechooser)を使ってできます。
ファイル選択ダイアログを表示するボタンをクリックしたら'a,b,c'の中身を持つ`test.csv`というファイル名を選択した状態にしています。

```ts
const [fileChooser] = await Promise.all([
  page.waitForEvent('filechooser'),
  page.getByTestId('select-button').click(),
]);
await fileChooser.setFiles({
  name: 'test.csv',
  mimeType: 'text/csv',
  buffer: Buffer.from('a,b,c'),
});
```


ドラッグ＆ドロップでファイル選択させる方法です。
ドラッグ＆ドロップ操作時にデータ格納で使われる[DataTransfer](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer)を使ってデータを格納し、dropイベントにdispatchするという手順になるようです。

```ts
const filename = 'test.csv';

const dataTransfer = await page.evaluateHandle((filename) => {
  const dt = new DataTransfer();
  const file = new File(['a,b,c'], filename, {type: 'text/csv'});
  dt.items.add(file);

  return dt;
}, filename);

await page.dispatchEvent('[data-testid="file"]', 'drop', {
  dataTransfer,
});
```

## スクリーンショットを撮る
検証結果のレポートに、証跡として画面のスクリーンショットを撮れます。
撮ったスクリーンショットは `testInfo.attach()` で添付できるようになってます。

スクリーンショット撮る際のオプション指定もできて、私は主に fullPage と animations を使いました。
CSSにアニメーション効果がある場合に中途半端な画像が撮れてしまう可能性が高く、animation効果をOFFっておく指定としました。また、画像が切れないようフルサイズ指定します。

画像添付する際、同じテストの中でいくつか画像を撮った時にどのタイミングで撮った画像なのかわかりにくいため、attachする際の名前を区別してつけることをお勧めします。

```ts
const screenshot = page.screenshot({
  fullPage: true,
  animations: 'disabled',
});

testInfo.attach('XXXした時', {
  body: screenshot,
  contentType: 'image/png',
});
```

## 検証系ロジックはPage側に記載する

検証系ロジックはPageクラスにまとめて記載すると多少実装がすっきりします。１画面=1Pageクラス単位で作ると良いですが、３画面で１機能になってるような場合は３画面分まとめて1Pageクラスでも良いかと思います。

例えば、ページ遷移でURLやページタイトルが想定通りかといったものは、次へボタン押したときも、次のページへ遷移後に戻るボタンで戻った時にも確認したい、というケースがあるとします。

テスト側にそれをそれぞれ実装するのは手間なので、以下のような感じでPageにまとめて実装してしまいます。

```ts
/** TestPageという名前のPageクラスとする **/
// URLの検証
async assertUrl() {
  await test.step('URLが"hoge/fuga"であること', async () => {
    await expect(this.page).toHaveURL('hoge/fuga');
  });
}
// タイトルの検証
async assertTitle() {
  await test.step('タイトルが"HOGEFUGA画面"であること', async () => {
    await title = this.page.getByTestId('title');
    await expect(title).toHaveText('HOGEFUGA画面');
  });
}
```

テスト側はこれらを呼び出すだけですみますし、メソッド名から何を検証しているのかも分かりやすいかと思います。

```ts

test('hogefuga画面のテスト', async ({page}, testInfo) => {
  // TopPageというページからHogeFugaページにアクセスするとテスト対象ページのインスタンスを戻すようにしておく
  const testPage = await topPage.goHogeFugaPage();
  // 検証
  await testPage.assertUrl();
  await testPage.assertTitle();
  ...
}
```

## リンク

Playwright Page https://playwright.dev/docs/api/class-page
Playwright Mock API https://playwright.dev/docs/mock
Playwright FileChooser https://playwright.dev/docs/api/class-filechooser
Playwright Screenshot https://playwright.dev/docs/screenshots