Playwrightだけで始めるビジュアルリグレッションテスト:toHaveScreenshotの使い方と運用のコツ
追加ツールなしで「見た目の崩れ」を検知する - ベースライン作成からマスク、CI、AI活用、競合比較まで

CSSを1行直しただけなのに、別ページのボタンが崩れていた。E2Eテストはすべて成功していたのに、本番で見た目の不具合が見つかった。こうした経験はないでしょうか。
Playwrightには、スクリーンショットを撮って前回の画像と比べる機能が最初から入っています。追加のライブラリやSaaSを契約しなくても、expect(page).toHaveScreenshot() の1行でビジュアルリグレッションテスト(VRT)を始められます。
この記事では、企業や個人でテストを書いているエンジニアに向けて、内蔵の画像比較機能の使い方と、実際に運用するときのコツを解説します。
検証環境:Windows 11 / Node.js 24.21.0 / @playwright/test 1.64.0 / Chromium(Chrome Headless Shell 156)。記事内のテストコードと設定ファイルはこの環境で実行して確認しました。「実践」の章の画像と出力は、このブログのLPで実際にテストしたときのものです。DockerとGitHub Actionsのコードは考え方を示す参考例で、筆者は実行していません。
Playwright自体のセットアップはPlaywrightの基礎から実践まで、AIでテストコードを書く方法はAIプロンプト設計術で解説しています。
なぜ「見た目」のテストが必要なのか
通常のE2Eテストは「ボタンが存在する」「クリックすると画面が遷移する」といった機能を確かめます。しかし、要素の存在や画面遷移だけを確かめるテストでは、次のような見た目の変化を十分に検出できないことがあります。
- 共通CSSの変更で、別ページのレイアウトが崩れた
- ライブラリのアップデートで、フォントや余白が変わった
- ダークモードで文字が背景と同じ色になり、読めなくなった
- レスポンシブ表示で、要素の配置がずれて見づらくなった
ボタンがクリックできる状態のままなら、余白や色が崩れていてもテストは成功する場合があります。それでもユーザーから見れば明らかな不具合です。
なお、要素が別の要素に覆われてクリックできなくなった場合は、Playwrightがクリック前に操作できる状態かを確認するため、機能テストでも失敗します。VRTは、こうした機能テストでは拾いきれない「見た目」の変化を補う位置づけです。
これを人が目で確認するのは大変です。ページ数 × 画面サイズ × ブラウザの組み合わせを、リリースのたびに全部見るのは現実的ではありません。そこで、正しい見た目の画像を保存しておき、毎回機械的に比べるのがビジュアルリグレッションテストです。
Playwrightの内蔵画像比較でできること
Playwrightの画像比較は、次のような仕組みで動きます。
- 初回実行時に、スクリーンショットをベースライン(期待値の画像)として保存する
- 2回目以降は、新しく撮った画像とベースラインをピクセル単位で比べる
- 差分が許容範囲を超えたらテストを失敗させ、差分画像を出力する
主な機能は以下のとおりです。
| 機能 | 内容 |
|---|---|
| ページ全体の比較 | expect(page).toHaveScreenshot()。fullPage: true でスクロール領域まで含める |
| 要素単位の比較 | expect(locator).toHaveScreenshot()。ボタンやカードなど特定の要素だけを比べる |
| 安定待ち | 連続して撮った2枚が一致するまで待ってから比べるため、描画途中の画像で失敗しにくい |
| 許容差の設定 | maxDiffPixels(差分ピクセル数)、maxDiffPixelRatio(差分の割合)、threshold(色の差の許容度、0〜1) |
| 動的要素の除外 | mask で指定した要素を塗りつぶす。stylePath でCSSを注入して隠すこともできる |
| アニメーション停止 | animations: 'disabled' でCSSアニメーションを止めてから撮る |
| 差分の確認 | HTMLレポートで期待値・実際の画像・差分画像を並べて確認できる |
画像の比較には toMatchSnapshot() もありますが、ページや要素のスクリーンショットを比べるなら、安定待ちが付いている toHaveScreenshot() を使います。
インストール
既存のPlaywrightプロジェクトがあれば、追加のインストールは不要です。新しく始める場合は次のコマンドを実行します。
npm init playwright@latest
既存のNode.jsプロジェクトに追加する場合は、パッケージとブラウザを個別にインストールします。
npm install -D @playwright/test npx playwright install chromium
比較方法
ここからは、次のようなデモページを使って説明します。最終更新日時が毎回変わり、読み込み中のスピナーが回り続ける、VRTで問題になりやすい要素をわざと入れています。
<!-- site/index.html --> <!doctype html> <html lang="ja"> <head><meta charset="utf-8"><title>Demo Shop</title> <style> body { font-family: sans-serif; margin: 0; } header { background: #1e3a8a; color: #fff; padding: 16px; } .hero { padding: 40px 16px; } .btn-primary { background: #2563eb; color: #fff; border: 0; padding: 12px 24px; border-radius: 8px; } .spinner { width: 24px; height: 24px; border: 3px solid #ccc; border-top-color: #2563eb; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } </style></head> <body> <header><h1>Demo Shop</h1></header> <section class="hero"> <p>最終更新: <span data-testid="timestamp"></span></p> <div class="spinner"></div> <button class="btn-primary">購入する</button> </section> <script>document.querySelector('[data-testid=timestamp]').textContent = new Date().toISOString();</script> </body></html>
設定ファイル
playwright.config.ts で、すべての toHaveScreenshot() に共通する既定値を決めておきます。デモでは http-server(npm install -D http-server)でページを配信しています。
// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', // HTMLレポートを出力する(差分の確認に使う)。コンソールにはlist形式で表示 reporter: [['html', { open: 'never' }], ['list']], expect: { toHaveScreenshot: { maxDiffPixels: 100, // 100ピクセルまでの差は許容する animations: 'disabled', // CSSアニメーションを止めてから撮る }, }, use: { baseURL: 'http://localhost:4173', }, webServer: { command: 'npx http-server ./site -p 4173 -s', url: 'http://localhost:4173', reuseExistingServer: !process.env.CI, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, ], });
テストコード:ページ全体と要素単位
ページ全体と、購入ボタンだけを比べる2つのテストを書きます。
// tests/visual.spec.ts import { test, expect } from '@playwright/test'; test('トップページ全体', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('top.png', { fullPage: true, mask: [page.getByTestId('timestamp')], // 毎回変わる日時は塗りつぶす }); }); test('購入ボタン', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('button', { name: '購入する' })).toHaveScreenshot('buy-button.png'); });
ページ全体の比較は、レイアウト崩れや余白の変化など、広い範囲の変化に気づけます。ただし、ページの一部が少し変わっただけでも失敗するため、ページが大きいほどメンテナンスの手間が増えます。
要素単位の比較は、ボタン・ヘッダー・カードなど、壊れると困る部品を重点的に守るのに向いています。画像が小さいので比較も速く、失敗したときに原因を見つけやすくなります。
ステップ1:ベースラインを作る
最初にテストを実行すると、比べる画像がまだないのでテストは失敗します。これは、CIでベースラインがないまま「成功」してしまうのを防ぐための既定の動作です(後述する --update-snapshots の default モード)。
npx playwright test
Error: A snapshot doesn't exist at ...\tests\visual.spec.ts-snapshots\top-chromium-win32.png, writing actual.
失敗と同時に、撮った画像がベースラインとして保存されます。保存先はテストファイルの隣の *-snapshots フォルダです。
tests/visual.spec.ts-snapshots/ ├── buy-button-chromium-win32.png └── top-chromium-win32.png
ファイル名の末尾に chromium(プロジェクト名)と win32(OS)が付く点に注意してください。同じページでもOSやブラウザが違えば、フォントの描画が変わるため別の画像として管理されます。これについては後の「ベストプラクティス」で説明します。
保存された画像を開いて、正しい見た目であることを必ず目で確認してから、Gitにコミットします。
ステップ2:テストを実行する
もう一度実行すると、新しい画像とベースラインが比べられます。日時の部分はマスクしているので、実行するたびに日時が変わっても成功します。
npx playwright test
2 passed (3.8s)
ステップ3:差分を検知する
試しに、ボタンの色を青(#2563eb)から赤(#dc2626)に変えて実行してみます。
Error: expect(page).toHaveScreenshot(expected) failed 4093 pixels (ratio 0.01 of all image pixels) are different. Expected: tests\visual.spec.ts-snapshots\top-chromium-win32.png Diff: test-results\visual-トップページ全体-chromium\top-diff.png
ページ全体の比較では4093ピクセルが変わりました。1280×720の画像全体の約0.44%ですが、maxDiffPixels: 100 を超えたので失敗します。ログの ratio は小数第2位に切り上げて表示されるため、0.01(1%)と出ています。ボタン単体の比較では、同じ4093ピクセルがボタン画像(102×45)の約89%にあたります。
test-results フォルダには、期待値(-expected.png)・実際の画像(-actual.png)・差分を赤く示した画像(-diff.png)の3枚が出力されます。設定ファイルでHTMLレポートを出力するようにしているので、次のコマンドでレポートを開き、これらを並べたり重ねたりして確認できます。
npx playwright show-report
ステップ4:意図した変更ならベースラインを更新する
ボタンの色の変更が意図したものなら、ベースラインを新しい画像に更新します。
npx playwright test --update-snapshots=changed
--update-snapshots(短縮形 -u)には、更新する範囲を選ぶモードがあります。モードを付けずに -u だけを指定すると changed になります。
| モード | 動作 |
|---|---|
default | フラグを付けずに実行したときの既定値。足りない画像は作るが、作ったテストは失敗扱いにする |
changed | 差分があった画像だけを更新する。足りない画像も作る |
all | 実行したテストの画像をすべて撮り直す |
missing | 足りない画像だけを作る(作ったテストは成功扱い) |
none | 何も更新しない |
すべて撮り直す all は、差分のなかった画像まで微妙に変わることがあるため、普段は changed を使うと差分のレビューが楽になります。
動的な要素を除外する
日時、広告、ランダムなおすすめ商品など、毎回変わる要素をそのまま撮ると、差分が出てテストが不安定になります。デモページで mask を外し、許容差を0にして2回実行すると、日時の部分だけで差分が出て失敗しました。
Error: expect(page).toHaveScreenshot(expected) failed 104 pixels (ratio 0.01 of all image pixels) are different.
除外する方法は2つあります。
方法1:mask で塗りつぶす
指定した要素をピンク色(maskColor で変更可能)の四角で塗りつぶしてから比べます。前の例のように、ロケーターを配列で渡します。
ただし、塗りつぶすのはその時点の要素の範囲です。中身が変わって要素の大きさや位置が変わると、塗りつぶす範囲や周りのレイアウトも変わるため、差分が出ます。中身の量が変わりやすい要素は、page.route() でAPIの応答をモックするなどして、件数や文字数も固定してください。
方法2:stylePath でCSSを注入する
除外したい要素が多い場合は、スクリーンショット用のCSSファイルを用意してまとめて隠せます。
/* tests/hide.css */ [data-testid="timestamp"], .spinner { visibility: hidden; }
// tests/style.spec.ts import { test, expect } from '@playwright/test'; import path from 'node:path'; test('stylePathで動的要素を隠す', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('styled.png', { maxDiffPixels: 0, stylePath: path.join(__dirname, 'hide.css'), }); });
display: none ではなく visibility: hidden を使うと、要素の場所は残るのでレイアウトが変わりません。設定ファイルの expect.toHaveScreenshot.stylePath に書けば、すべてのテストに適用できます。
実践:このブログのLPで試してみた
デモページだけでは実感しにくいので、このブログ(Recurrent Hub)のトップページ(LP)で、ベースラインの作成から差分の検出、ベースラインの更新までを実際に行いました。LPはNext.js 15で作られていて、Hero・特徴紹介・新着記事の3つのブロックでできています。
テストの準備
本番に近い状態で比べるため、開発サーバーではなく本番ビルドを起動してテストします。
npm run build npx next start -p 3100
設定ファイルでは、画面サイズを1280×800に固定しています。
// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', reporter: [['html', { open: 'never' }], ['list']], expect: { toHaveScreenshot: { maxDiffPixels: 100, animations: 'disabled', }, }, use: { baseURL: 'http://localhost:3100', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } }, }, ], });
テストは「ファーストビュー(最初に見える範囲)」と「ページ全体」の2つです。新着記事のブロックは記事を公開するたびに中身が変わるので、ページ全体のテストでは mask で除外しました。ファーストビューは、1280×800の画面ではHeroだけが写り、新着記事が入らないのでマスクしていません。画面サイズを変えるときは、新着記事が写り込まないかを確認してください。
// tests/lp.spec.ts import { test, expect } from '@playwright/test'; test.describe('LP', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); // 新着記事はAPIから非同期で読み込まれるので、表示されるまで待つ await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); await page.locator('a[href^="/posts/"]').first().waitFor(); }); test('ファーストビュー', async ({ page }) => { await expect(page).toHaveScreenshot('lp-first-view.png'); }); test('ページ全体', async ({ page }) => { // 記事を追加するたびに変わる「新着記事」はマスクする const recentPosts = page.locator('div.max-w-6xl').filter({ hasText: '新着記事' }); await expect(page).toHaveScreenshot('lp-full.png', { fullPage: true, mask: [recentPosts], }); }); });
つまずいたポイント:最初は見出しを
getByRole('heading', { name: 'Recurrent Hub で再発見する' })で探していましたが、要素が見つからずタイムアウトしました。LPの見出しは2つの<span>を余白(ml-2)で離して表示しているだけで、文字としてはスペースが入っていません。見た目どおりの文字列で探すと失敗することがあるので、level: 1のように構造で指定するほうが確実です。
ステップ1:ベースラインを作る
初回の実行では、予想どおり2つとも失敗し、ベースラインが保存されました。
Error: A snapshot doesn't exist at ...\tests\lp.spec.ts-snapshots\lp-first-view-chromium-win32.png, writing actual. Error: A snapshot doesn't exist at ...\tests\lp.spec.ts-snapshots\lp-full-chromium-win32.png, writing actual. 2 failed
保存されたファーストビューのベースラインがこちらです(この記事に載せている画像は、表示用に縮小しています。比較にはPlaywrightが保存した原寸のPNGが使われます)。

ページ全体のベースラインでは、新着記事のブロックがピンク(マゼンタ)で塗りつぶされています。これが mask の効果で、新着記事のタイトルや画像が入れ替わっても、その中身は比較に影響しません。ただし、新着記事は最大8件で、タイトルの折り返しなどでブロックの高さが変わると、ページ全体の高さやマスクの位置が変わって差分が出ます。そうした変化も防ぎたい場合は、page.route() で /api/posts の応答をモックして、表示する記事を固定します。

2回目・3回目の実行では、どちらのテストも成功しました。ベースラインが安定していることを、何度か実行して確かめてから次に進みます。
2 passed (3.0s)
ステップ2:見た目を変えて差分を検出する
デザイン変更を想定して、Heroの「記事を読む」ボタンの色を緑からオレンジに変えました。
// src/features/lp/Hero.tsx - <Button size="lg" className="group bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700 text-white ..."> + <Button size="lg" className="group bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white ...">
ビルドし直してサーバーを再起動し、テストを実行します。
つまずいたポイント:
next startを起動したままnpm run buildを実行したところ、起動中のサーバーが古いビルドの情報のまま動き続け、ページが正しく読み込まれずにテストがタイムアウトしました。本番ビルドでテストするときは、ビルドし直したらサーバーも必ず再起動してください。
再起動後の結果です。どちらのテストも、ボタンの部分だけが変わったことを検出しました。
Error: expect(page).toHaveScreenshot(expected) failed 4470 pixels (ratio 0.01 of all image pixels) are different. Expected: tests\lp.spec.ts-snapshots\lp-first-view-chromium-win32.png Received: test-results\lp-LP-ファーストビュー-chromium\lp-first-view-actual.png Diff: test-results\lp-LP-ファーストビュー-chromium\lp-first-view-diff.png 2 failed
変わったのは4470ピクセルで、1280×800の画面全体の約0.44%です(ログの割合は小数第2位に切り上げて表示されるため0.01になっています)。わずかな割合でも、許容値の100ピクセルを大きく超えたので失敗になりました。
HTMLレポート(npx playwright show-report)を開くと、失敗したテストが一覧で表示されます。

出力された差分画像(-diff.png)では、変わった部分が赤く示され、それ以外は薄く表示されます。ボタンの色だけが変わったことがひと目で分かります。

テストを開いて[Side by side]を選ぶと、左に期待値(ベースライン)、右に実際の画像が並びます。[Slider]では2枚を重ねて、境目を動かしながら比べられます。

ボタンの周りを拡大すると、変更前(左)と変更後(右)の違いがはっきり分かります。

ステップ3:変更を判断して、ベースラインを更新する
差分が出たら、まずその変化が意図したものかどうかを判断します。
意図しない変化(崩れ)だった場合は、ベースラインには触らずにコードを直し、もう一度テストを実行して成功することを確かめます。
今回はボタンの色を意図して変えたので、ベースラインを更新します。手順は次のとおりです。
1. 差分があった画像だけを更新する
npx playwright test --update-snapshots=changed
...\tests\lp.spec.ts-snapshots\lp-first-view-chromium-win32.png is re-generated, writing actual. ok 1 [chromium] › tests\lp.spec.ts:11:7 › LP › ファーストビュー (1.4s) ...\tests\lp.spec.ts-snapshots\lp-full-chromium-win32.png is re-generated, writing actual. ok 2 [chromium] › tests\lp.spec.ts:15:7 › LP › ページ全体 (2.5s) 2 passed (4.9s)
「is re-generated」と表示された画像が、新しいベースラインに置き換わります。
2. 更新された画像を目で確認する
更新後のベースラインを開き、オレンジのボタンになっていること、それ以外に変わった部分がないことを確認します。

3. もう一度実行して、成功することを確かめる
npx playwright test
ok 1 [chromium] › tests\lp.spec.ts:11:7 › LP › ファーストビュー (870ms) ok 2 [chromium] › tests\lp.spec.ts:15:7 › LP › ページ全体 (1.1s) 2 passed (2.7s)
4. コードの変更とベースラインを同じコミットに含める
git add src/features/lp/Hero.tsx tests/lp.spec.ts-snapshots/ git commit -m "LPのCTAボタンをオレンジに変更"
コードと画像を別々のコミットにすると、レビューする人が「どのコード変更でこの見た目になったのか」を追いにくくなります。プルリクエストでは、画像の差分も合わせてレビューしてもらいましょう。
CIをLinuxで動かす場合、ここで作られる
-win32.pngの画像はCIでは使われません。チームで運用するときは、次の「ベストプラクティス」で紹介するように、CIと同じDockerイメージでベースラインを更新する方法を検討してください。
活用のベストプラクティス
1. 撮影環境を固定する
VRTで最もよくあるトラブルは、ローカルでは成功するのにCIで失敗することです。原因の多くは、OSやインストールされているフォントの違いによる描画の差です。ファイル名に win32 や linux が付くのはこのためで、WindowsのPCで作ったベースラインは、LinuxのCIでは使われません。
対策として、ベースラインの作成とテストの実行を同じDockerイメージで行います。Playwright公式のイメージには、ブラウザと必要なシステムライブラリが入っています。イメージのバージョンは、package.json のPlaywrightのバージョンと必ず合わせます。
参考例について:以下のDockerとGitHub Actionsのコードは、考え方を示すための参考例です。筆者は実行していないので、そのままでは動かない可能性があります。実際に使うときは、次の点を自分の環境に合わせてください。
- テスト対象のサイトをコンテナの中で起動する:コンテナの中の
localhostは、PCのlocalhostとは別です。設定ファイルのwebServerに、サイトをビルドして起動するコマンド(このブログならnpm run build && npx next start -p 3100)を指定します。- シェルに合わせて書き換える:以下のコマンドはBash用です(macOS・Linux、WindowsならWSLやGit Bash)。PowerShellでは行の継続やカレントディレクトリの書き方が違います。
# Linux用のベースラインをDockerで作る例(Bash) docker run --rm -v "$(pwd)":/work -w /work mcr.microsoft.com/playwright:v1.64.0-noble \ /bin/bash -c "npm ci && npx playwright test --update-snapshots=changed"
GitHub Actionsでも同じイメージを使います(参考例)。
# .github/workflows/vrt.yml name: Visual Regression on: [pull_request] jobs: vrt: runs-on: ubuntu-latest container: image: mcr.microsoft.com/playwright:v1.64.0-noble steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm ci - run: npx playwright test env: HOME: /root - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/
失敗したときにHTMLレポートをアーティファクトとしてダウンロードできるようにしておくと、差分画像をすぐ確認できます。
2. 小さく始めて、守る範囲を決める
全ページ・全画面サイズ・全ブラウザを最初から対象にすると、ベースラインの数が増えすぎて、更新のたびに大量の差分を確認することになります。
- まずはトップページ、ログイン、購入フローなど重要な数画面から始める
- ブラウザはまずChromiumだけにする
- 画面サイズは「PC」と「スマホ」の2つに絞る
- 共通部品(ヘッダー、ボタン、フォーム)は要素単位で比べる
3. 許容差は「小さく始めて、理由があれば広げる」
maxDiffPixels や threshold を大きくすると失敗は減りますが、本当の崩れも見逃します。最初は小さめに設定し、アンチエイリアス(文字の輪郭のにじみ)などで不安定なテストがあれば、そのテストだけ許容差を広げます。設定ファイルの既定値を大きくするのは最後の手段です。
4. テストデータを固定する
ユーザー名、商品一覧、件数など、データによって見た目が変わる部分は、page.route() でAPIのレスポンスをモックして固定します。日時が絡む画面は page.clock で時刻を固定する方法もあります。
5. ベースラインの更新をレビューの対象にする
ベースライン画像はGitで管理し、更新したときはプルリクエストで差分を確認します。GitHubのプルリクエストでは画像の差分を並べて見られます。「-u を付けて実行したらテストが通った」だけで済ませると、崩れた見た目を正解として登録してしまいます。
6. 機能テストとVRTを分ける
VRTは環境の違いで失敗しやすいため、機能テストと同じファイルに混ぜると、失敗の原因が分かりにくくなります。tests/visual/ のようにフォルダを分けるか、プロジェクトを分けて --project で実行対象を選べるようにしておくと管理しやすくなります。
AIの活用案
VRTはAIと組み合わせると、手間のかかる部分を減らせます。以下は活用のアイデアです。
テストコードの作成をAIに任せる
「このページの主要な部品ごとに toHaveScreenshot() を書いて。日時や広告は mask で除外して」とAIに頼めば、テストの下書きを作れます。Playwright MCPを使えば、AIが実際にページを開いて構造を確認しながらロケーターを選べます。セットアップ方法はWindows環境でのPlaywright & Claude MCP セットアップガイドを参照してください。
Playwright Test Agentsを使う
Playwrightには、テスト計画を作るplanner、テストコードを生成するgenerator、失敗したテストを修正するhealerの3つのエージェント定義が用意されています。次のコマンドでプロジェクトに追加できます(--loop には claude・codex・copilot・opencode・vscode などを指定できます)。
npx playwright init-agents --loop=claude
ただし、VRTの失敗をhealerに任せるときは注意が必要です。見た目の差分が「意図した変更」なのか「崩れ」なのかは、仕様を知っている人でないと判断できません。AIにベースラインを自動で更新させると、崩れを正解として登録してしまう恐れがあります。
差分画像の一次確認をAIに頼む
差分が大量に出たとき、期待値・実際の画像・差分画像の3枚を画像を読めるAIに渡し、「どこが変わったか」「崩れに見えるか」を説明させると、確認する順番を決めやすくなります。最終的な判断は人が行い、AIは「確認の下準備」として使うのが安全です。
競合製品との比較
Playwrightの内蔵機能は無料で始められますが、チームでの運用には足りない部分もあります。代表的なツールと比べてみましょう。以下は2026年10月時点で各公式サイトを確認した内容です。料金、無料枠の対象、チームで使うときの条件は変わることがあるので、導入前に各社の料金ページで確認してください。
| ツール | 種類 | 特徴 |
|---|---|---|
| Playwright内蔵 | 無料・OSS | 追加ツール不要。ベースラインはGitで管理。チームでの承認フローやダッシュボードはない |
| Argos | OSS + SaaS | Playwrightのテストで撮った画像をアップロードして比較・レビューできる。個人向けの無料枠あり(チームでの利用条件は料金ページで要確認) |
| Percy(BrowserStack) | SaaS | DOMをアップロードし、クラウドで複数ブラウザ・画面サイズを描画して比較する |
| Applitools Eyes | SaaS | 「Visual AI」で人の目に近い比較を行う。比較モードや設定によって、細かなノイズを無視しやすくできる。企業向けの機能が充実 |
| Chromatic | SaaS | Storybookとの連携に強く、UIコンポーネント単位の比較とレビューが得意。PlaywrightのE2Eテストにも対応 |
| reg-suit | 無料・OSS | 画像の比較とレポートに特化。撮影は別のツールで行い、S3やGCSに画像を保存する |
| BackstopJS | 無料・OSS | 設定ファイル中心でVRTを行う老舗のツール |
どれを選ぶべきか
- 個人開発・小規模チーム:まずはPlaywright内蔵から始めると、導入の負担を抑えられます。ベースラインをGitで管理し、プルリクエストでレビューします。
- ベースラインの承認をチームで行いたい:Argosのように、Playwrightのテストはそのままにレビュー画面だけを追加できるサービスが候補になります。
- 多数のブラウザ・画面サイズを確認したい、ノイズに悩んでいる:PercyやApplitoolsのようなクラウド描画・AI比較のサービスを検討します。
- Storybookでコンポーネントを管理している:Chromaticが自然な選択肢です。
内蔵機能でVRTの運用に慣れてから、足りない部分を補う形でSaaSを検討すると、ツール選びで失敗しにくくなります。
まとめ
- Playwrightの
toHaveScreenshot()を使えば、追加ツールなしでビジュアルリグレッションテストを始められる - 既定の設定では、初回の実行でベースラインが作られてテストが失敗する。画像を目で確認してからコミットする
- ページ全体と要素単位を使い分け、日時などの動的な要素は
maskやstylePathで除外する。中身の量が変わる要素はデータも固定する - ローカルとCIで描画が変わるので、公式Dockerイメージなどで撮影環境をそろえる
- AIはテストの作成や差分の一次確認に使い、ベースラインの更新は人が判断する
まずは、一番壊れると困る画面を1つ選んで、toHaveScreenshot() を1行追加するところから始めてみてください。