はじめに
E2Eテストのテスティングフレームワークとして、Playwrightを2023年に入ってから実務に導入しています。
Locator周りのヘルパー関数から、Auto-wait、トレース、並行処理にuiモード、至れり尽せりのPlaywrightは、Visual Comparison、すなわちスクリーンショットの比較もout-of-the-boxでできます。
...できますが、実際にやってみると、豊富な機能の森に迷って、簡単なことで意外に手こずることもあります。
先日、本番環境とステージング環境1で修正の戻りがないか、すなわちVisual Regressionテストを行おうとしました。
実装したい手順は、
1. ステージング環境にアクセス
2. ステージング環境のスクリーンショット
3. 本番環境にアクセス
4. 本番環境のスクリーンショット
5. アサーション
と明瞭です。
が、この実装に詰まりました。
ということで、Playwrightを使って一つのテスト内で2つの連続するスクリーンショットを比較する方法を、NG例を添えて記録します。
結論
- 1回目の
page.screenshot
のpathにtestInfo.snapshotPath
を用いて保存場所を指定する。 - Matcherの
toMatchSnapshot
に引数をつける。
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枚目のスクリーンショットを同一のファイル名にすること。
単純に
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」です。
testInfoのsnapshotPath
プロパティを使うと、Matcherで参照しているパスが取得できます。
なお別解として、Matcherが参照するパスはplaywright.config.ts
のsnapshotPathTemplate
で指定できるので、こちらを編集することも考えられます。
Matcher
アサーションのMatcherにはtoMatchSnapshot
を使いましたが、toHaveScreenshot
で代置可能です。ただし、引数と同期非同期が異なります。
toMatchSnapshotの場合
1枚目のスクリーンショットのpathに合わせて、name
プロパティを引数として渡します。
expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ name: "result.png" })
toHaveScreenshotの場合
こちらでも可能です。引数の形が異なるので注意してください。Page
クラスに対するアサーションになります。
また、toHaveScreenshot
はPromiseを返すweb-specific matcherなので、expectの前にawaitが必要です。
await expect(page).toHaveScreenshot("result.png", { fullPage: true });
以下、試してみてダメだったNG例を供養します。なむなむ。
NG集
toMatchSnapshot / toHaveScreenshotを引数なしで2回実行
await expect(page).toHaveScreenshot()
// ページ遷移
await expect(page).toHaveScreenshot()
or
expect(await page.screenshot()).toMatchSnapshot()
// ページ遷移
expect(await page.screenshot()).toMatchSnapshot()
これら二つのMatcherは同一テスト内で連続して引数なしで実行されると、
<テストファイル名>-snapshot/<テスト名>_1-<ブラウザ>-<環境名>
<テストファイル名>-snapshot/<テスト名>_2-<ブラウザ>-<環境名>
と序数がついて、別ファイルとして保存され、比較はされません。
toMatchSnapshot / toHaveScreenshotを引数ありでパス指定して2回実行
await expect(page).toHaveScreenshot("result.png")
// ページ遷移
await expect(page).toHaveScreenshot("result.png")
or
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
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)を返します。これを比較するのも一案。
const production:Buffer = await page.screenshot()
// ページ推移
const staging = await page.screenshot()
expect(staging).toEqual(production)
試してみましたが、一致する場合は問題なくテストできましたが、一致しない場合にフリーズしてしまいました。
下記IssueのコメントによるとBufferの差分が出るとのことですが、いずれにせよ差分の場所がわからないので、実用に堪えません。
参考文献
Playwrightドキュメント
Issue
-
ここでは本番と同じDBを参照している開発環境、を指します。 ↩