【Playwright】一つのテスト内で2つの連続するスクリーンショットを比較・差分検出する

# はじめに
E2Eテストのテスティングフレームワークとして、Playwrightを2023年に入ってから実務に導入しています。

https://playwright.dev/

Locator周りのヘルパー関数から、Auto-wait、トレース、並行処理にuiモード、至れり尽せりのPlaywrightは、Visual Comparison、すなわちスクリーンショットの比較もout-of-the-boxでできます。

...できますが、実際にやってみると、豊富な機能の森に迷って、簡単なことで意外に手こずることもあります。

--- 

先日、本番環境とステージング環境[^1]で修正の戻りがないか、すなわちVisual Regressionテストを行おうとしました。

[^1]:ここでは本番と同じDBを参照している開発環境、を指します。

実装したい手順は、

    1. ステージング環境にアクセス
    2. ステージング環境のスクリーンショット
    3. 本番環境にアクセス
    4. 本番環境のスクリーンショット
    5. アサーション

と明瞭です。

が、この実装に詰まりました。

ということで、Playwrightを使って一つのテスト内で2つの連続するスクリーンショットを比較する方法を、NG例を添えて記録します。

# 結論
:::note
- 1回目の`page.screenshot`のpathに`testInfo.snapshotPath`を用いて保存場所を指定する。
- Matcherの`toMatchSnapshot`に引数をつける。

```ts:コード例
test("ページAとページBのスクリーンショット比較", async ({ page }, testInfo) => {
  await page.goto("<ページAのURL>")
  await page.screenshot({ path: `${testInfo.snapshotPath("result.png")}`, fullPage: true })

  await page.goto("<ページBのURL>")
  expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ name: "result.png" })
})
```
:::

# ポイント
## testInfoで保存先のPathを指定する
今回の実装の要は、1枚目と2枚目のスクリーンショットを同一のファイル名にすること。

単純に

```ts
await page.screenshot({ path: "result.png", fullPage: true })

expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ name: "result.png" })
```

とすると失敗します。`screenshot`と`toMatchSnapshot`では引数に与えたパスの扱いが異なるからです。

つまり上記の例だと、`page.screenshot`はプロジェクトルートに画像を作成するのに対し、`toMatchSnapshot`はデフォルトで、`example.spec.ts-snapshot/testName-chromium-darwin`と、`<テストファイル名>-snapshot/<テスト名>-<ブラウザ>-<環境名>`を参照します。

ここで使えるのが、「**testInfo**」です。

https://playwright.dev/docs/api/class-testinfo

**testInfo**の`snapshotPath`プロパティを使うと、Matcherで参照しているパスが取得できます。

なお別解として、Matcherが参照するパスは`playwright.config.ts`の`snapshotPathTemplate`で指定できるので、こちらを編集することも考えられます。

https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template

## Matcher
アサーションのMatcherには`toMatchSnapshot`を使いましたが、`toHaveScreenshot`で代置可能です。ただし、引数と同期非同期が異なります。

### toMatchSnapshotの場合
1枚目のスクリーンショットのpathに合わせて、`name`プロパティを引数として渡します。
```ts
expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ name: "result.png" })
```

### toHaveScreenshotの場合
こちらでも可能です。引数の形が異なるので注意してください。`Page`クラスに対するアサーションになります。
また、`toHaveScreenshot`はPromiseを返すweb-specific matcherなので、expectの前にawaitが必要です。

```ts
await expect(page).toHaveScreenshot("result.png", { fullPage: true });
```

以下、試してみてダメだったNG例を供養します。なむなむ。


## NG集
### toMatchSnapshot / toHaveScreenshotを引数なしで2回実行

```ts
await expect(page).toHaveScreenshot()
// ページ遷移
await expect(page).toHaveScreenshot()
```
or
```ts
expect(await page.screenshot()).toMatchSnapshot()
// ページ遷移
expect(await page.screenshot()).toMatchSnapshot()
```

これら二つのMatcherは同一テスト内で連続して引数なしで実行されると、
- `<テストファイル名>-snapshot/<テスト名>_1-<ブラウザ>-<環境名>`
- `<テストファイル名>-snapshot/<テスト名>_2-<ブラウザ>-<環境名>`

と序数がついて、別ファイルとして保存され、比較はされません。

### toMatchSnapshot / toHaveScreenshotを引数ありでパス指定して2回実行

```ts
await expect(page).toHaveScreenshot("result.png")
// ページ遷移
await expect(page).toHaveScreenshot("result.png")
```
or
```ts
expect(await page.screenshot()).toMatchSnapshot({ name: "result.png" })
// ページ遷移
expect(await page.screenshot()).toMatchSnapshot({ name: "result.png" })
```

引数で画像名を指定することで、連続するスクリーンショットを比較対象にすることができます。

ただし、2つのMatcherは初回実行など、画像が存在しない場合は画像を作成しますが、すでに存在している場合は、最初のexpectで比較が行われてしまいます。

したがって、beforeEach等で画像の削除をしたり、都度`--update-snapshots`フラグをつけたりする必要がありそうでNGとなりました。

最も正しそうなやり方に見えるので、何らかスマートな設定方法があるかもしれません。

### screenshot(画像名指定) => toMatchSnapshot / toHaveScreenshot
```ts
await page.screenshot({ path: "result.png" })
// ページ遷移
expect(await page.screenshot()).toMatchSnapshot({ name: "result.png" })
```

自然に見えますが、前述の通り、`page.screenshot`と`page.toHaveScreenshot`でデフォルトのパスが異なるのでNGです。1回目と2回目で別のパスを参照してしまいます。

### BufferをtoEqualで比較
`page.screenshot`はBuffer（正確にはPromise<Buffer>）を返します。これを比較するのも一案。

```ts
const production:Buffer = await page.screenshot()
// ページ推移
const staging = await page.screenshot()
expect(staging).toEqual(production)
```

試してみましたが、一致する場合は問題なくテストできましたが、一致しない場合にフリーズしてしまいました。

下記IssueのコメントによるとBufferの差分が出るとのことですが、いずれにせよ差分の場所がわからないので、実用に堪えません。

https://github.com/microsoft/playwright/issues/9696#issuecomment-1060064791


# 参考文献
## Playwrightドキュメント
https://playwright.dev/docs/api/class-testinfo

https://playwright.dev/docs/screenshots

https://playwright.dev/docs/test-snapshots

## Issue
https://github.com/microsoft/playwright/issues/9696