Playwright でスクリーンショットテストを行う方法
ビジュアルベースライン、ブラウザープロジェクト、安定した待機、差分のしきい値、更新のレビューを備えた Playwright のスクリーンショットテストを作成します。
15分で読めますWebサイトキャプチャ作成者:Temposhot Team
スクリーンショットテストは、現在のページの見た目を、以前に承認された画像と比較します。ピクセルの差が許容量を超えると、テストは失敗し、その差を表示します。Playwright Test は追加のプラグインなしで、expect(page).toHaveScreenshot() によりこの機能を標準で備えています。この手法は ビジュアルリグレッションテスト とも呼ばれます。
この記事の例は 1 つのスイートを構築します。3 つのブラウザープロジェクトでテストする 12 のマーケティングページとアプリのページ、および 8 つの共有コンポーネントです。ベースラインの数から差分のしきい値まで、以下のすべての数値はこのスイートから得ています。
スクリーンショットテストが有効な場合
ビジュアルテストは、DOM のアサーションでは検出できない問題を捉えます。読み込みに失敗したスタイルシート、画面外に押し出されたボタン、システムの既定フォントに置き換わったフォントなどです。次の場合は、メンテナンスの負担に見合う価値があります。
- UI に、多くのページで使われるデザインシステムや共有コンポーネントがある
- CSS の変更が頻繁で、レビュー担当者がすべてのページを手作業で確認できない
- レイアウトの不具合が過去に本番環境に到達したことがある
ライブデータ、広告、ユーザーコンテンツを含むページでは、かかるコストに見合う成果が得られません。実行のたびに結果が変わり、スイートはノイズだらけになります。
テストを書く前にスイートの規模を見積もる
アサーションごとに、プロジェクトの数だけベースライン画像が作られます。例のスイートには全ページのアサーションが 12 件、コンポーネントのアサーションが 8 件あり、それぞれ 3 つのプロジェクトで実行されます。そのため、リポジトリで作成、レビュー、保持する PNG ファイルは 60 個になります。
スイートから外すページ
どのように書いたテストでも、毎回失敗するページがあります。ライブチャートのあるダッシュボード、ユーザー投稿のフィード、回転するバナーのあるページなどです。こうしたページでは、代わりに空の状態のページや、固定データを使ったコンポーネントをテストします。
例のチームは、候補の 5 ページをスイートから外しました。ダッシュボード 2 件、毎週新しい投稿が追加されるブログ一覧、ステータスページ、検索結果ページです。それぞれは、共有部分の要素アサーションでカバーしています。全ページのリストは 12 件のままです。
長いマーケティングページの全ページ PNG は、1 MB 以上になることがあります。このサイズのファイルが 60 個あると、ベースラインを更新するたびに Git 履歴に積み重なります。コンポーネントのアサーションはファイルサイズが小さく、変更頻度も低いため、スイートの大部分を担います。
Playwright Test をインストールする
npm init playwright@latest
インストーラーは playwright.config.ts、テストの例を置くフォルダー、任意の CI ワークフローを作成し、ブラウザーをダウンロードします。スクリーンショットのアサーションには、これ以外に必要なものはありません。
Playwright のバージョンを固定する
Playwright の各リリースには、特定のブラウザービルドが含まれています。ブラウザーの更新で文字の描画がわずかに変わると、変更していないスクリーンショットテストが失敗することがあります。package.json で正確なバージョンを指定し、バージョンの更新は別のプルリクエストで行います。
{
"devDependencies": {
"@playwright/test": "1.48.0"
}
}
バージョンを変更する場合は、同じプルリクエストですべてのベースラインを再生成し、まとめて確認します。バージョン更新と機能変更を混ぜると、どちらが差分の原因かを判断できなくなります。
最初のスクリーンショットテストを作成する
// tests/visual.spec.ts
import { test, expect } from '@playwright/test';
test('home page matches baseline', async ({ page }) => {
await page.goto('https://example.com/');
await expect(page).toHaveScreenshot('home.png', { fullPage: true });
});
最初の実行には、比較対象のベースラインがありません。Playwright はキャプチャを期待画像として書き込み、新しいベースラインをレビューできるようにテストを失敗させます。npx playwright test をもう一度実行すると、保存されたファイルと比較してテストが成功します。
後の実行で差が出ると、Playwright は期待画像、実際のキャプチャ、変更されたピクセルを強調表示した差分画像を保存します。npx playwright show-report で開く HTML レポートには、この 3 つがすべて表示されます。
1 つのリストから 12 ページをテストする
ページごとに 12 個のテストを別々に書くと、同じ行が繰り返されます。ページのリストとループを使えば、ページごとに名前付きのテストを 1 つ作成できます。テストのタイトルにはページ名が含まれるため、後から -g で 1 ページだけを指定できます。
// tests/pages.visual.spec.ts
import { test, expect } from '@playwright/test';
const pages = [
{ name: 'home', path: '/' },
{ name: 'pricing', path: '/pricing' },
{ name: 'features', path: '/features' },
{ name: 'docs-index', path: '/docs' },
// ...8 more entries
];
for (const p of pages) {
test(`page ${p.name} matches baseline`, async ({ page }) => {
await page.goto(p.path);
await page.getByRole('heading', { level: 1 }).waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot(`${p.name}.png`, { fullPage: true });
});
}
相対パスを使うには、設定に baseURL が必要です。次のセクションでは、ローカルの Web サーバーと合わせてこれを追加します。
本番環境ではなくローカルビルドで実行する
本番環境のページは、テスト対象のコードとは関係のない理由で変わります。コンテンツの編集、A/B テスト、サードパーティのスクリプトなどです。ブランチのローカルビルドでは、そのブランチで変わった内容だけが表示されます。webServer オプションは、テストの前にこのビルドを起動します。
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: { baseURL: 'http://localhost:3000' },
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
reuseExistingServer を使うと、開発者はローカル実行時に自分のサーバーを起動したままにできます。CI では false になるため、ジョブは毎回新しいビルドを起動します。
ページの一部分をテストする
test('pricing card', async ({ page }) => {
await page.goto('https://example.com/pricing');
await expect(page.getByTestId('pro-plan')).toHaveScreenshot('pro-card.png');
});
要素単位のアサーションは小さく、無関係な理由で失敗することも少なくなります。ヘッダーを変更しても、料金カードのテストは壊れません。主要な数ページには全ページのアサーションを、コンポーネントには要素のアサーションを使います。
ページの種類ごとにアサーションを選ぶ
例のスイートでは、12 ページが 4 つのページタイプに分かれます。全体として評価されるページだけが全ページのアサーションを受けます。共有コンポーネントで構成されたページは、代わりにそのコンポーネントの要素アサーションを受けます。
ブラウザーとデバイスのプロジェクトを追加する
playwright.config.ts の projects は、同じテストを複数のブラウザーとデバイスプロファイルで実行します。
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});
各プロジェクトは独自のベースラインを保持します。ファイル名にはプロジェクト名とプラットフォームが含まれます(例: home-chromium-darwin.png)。そのため、プロジェクトが 3 つあると、アサーションごとに 3 つのベースラインが作られます。レイアウトが実際に異なる場合にのみプロジェクトを追加します。
ベースラインを予測しやすい 1 つのフォルダーに保存する
既定の場所では、各テストファイルの隣に -snapshots フォルダーが作られます。テストファイルが複数あると、ベースラインがテストのディレクトリ全体に分散します。snapshotPathTemplate オプションを使うと、共通のルート配下にテストファイルごとのフォルダーへまとめられます。
// playwright.config.ts
export default defineConfig({
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{-projectName}{ext}',
});
このテンプレートはプラットフォームのサフィックスを省きます。すべての実行が同じ Linux Docker イメージを使うため、この省略で問題ありません。一部の実行を macOS でも行う場合は、テンプレートに {-snapshotSuffix} を残し、2 つのプラットフォームが同じファイルを共有しないようにします。
レビュー担当者は 1 つのフォルダーを開くだけで、スペックファイルのすべてのベースラインを確認できます。テストファイルの名前を変更すると、ベースラインは新しいフォルダーに移動します。名前を変更する場合は、ベースラインを再生成するプルリクエストに含めます。
ページの状態を安定させる
スクリーンショットを比較するには、実行のたびにページが同じ状態に到達している必要があります。
- アサーションの前に、見出しや読み込み済みの画像などの準備完了の要素を待つ。
- ライブアカウントの代わりに、固定のテストデータを使う。
- 設定で固定のビューポートを指定する。
await page.evaluate(() => document.fonts.ready)でウェブフォントの読み込みを待つ。
toHaveScreenshot は、2 回連続でキャプチャが一致するまで既定で再試行し、CSS アニメーションも既定で停止します。この 2 つの動作により、追加のコードなしでタイミングによるノイズの大部分が取り除かれます。
固定時間ではなくシグナルを待つ
固定の page.waitForTimeout(3000) はスイートを遅くし、遅いランナーでは依然として失敗します。特定の要素を待つと、アサーションがページの状態に結び付きます。上のページループにある見出しの待機が、その一例です。
ネットワークがアイドルになるまで待つ方法は、見た目ほど確実ではありません。API をポーリングするページや接続を開いたままにするページは、その状態に決して到達せず、待機はタイムアウトで終わります。データ読み込み後にだけ表示される要素、たとえばテーブルの最初の行を待つ方が確実です。
画面外の遅延読み込み画像は、スクロールして表示領域に入るまで読み込まれません。そうしたページの全ページキャプチャには、空の枠が表示されます。アサーションの前に一度ページの下端までスクロールするか、テストビルドで遅延読み込みをオフにします。
時刻を固定し、サードパーティのスクリプトをブロックする
「3 分前に更新」のような日付は、実行のたびに変わります。Clock API は、ページが参照する時刻を固定します。チャットウィジェットや解析用のサードパーティスクリプトは、後から要素を挿入することがあるため、テストではネットワークレベルでこれらをブロックします。
test.beforeEach(async ({ page }) => {
await page.clock.setFixedTime(new Date('2026-05-01T10:00:00Z'));
await page.route(/(analytics|chat-widget)\./, (route) => route.abort());
});
スクリプトをブロックすると、そのスクリプトが描画する内容も表示されなくなります。これは意図どおりです。このビジュアルテストが対象とするのは、サイト自身のレイアウトであり、ウィジェットではありません。
不安定さの原因と対処
例のスイートは、最初の 2 週間でコードを変更していないのに 9 回失敗しました。各失敗は 5 つの原因のいずれかにたどり着きました。下の表は、各原因と、それを止めた変更を対応付けています。
変動する要素を隠すかマスクする
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [page.locator('.timestamp'), page.locator('.user-avatar')],
});
マスクされた領域は、両方の画像で単色の四角形により覆われるため、その内容は無視されます。要素を完全に消したい場合は、stylePath オプションでスタイルシートを渡し、そこで非表示にします。要素を削除するより、マスクする方が適しています。要素の周囲のレイアウトは引き続き検証されるためです。
レイアウトではなくロールでマスクする
レイアウト用のクラスに依存するマスクのロケーターは、マークアップが変わると壊れます。テスト ID やアクセシブルなロールは、リデザインをまたいで安定します。例のスイートでは、変動するすべての要素が volatile- で始まる data-testid を持ちます。
await expect(page).toHaveScreenshot('account.png', {
mask: [page.locator('[data-testid^="volatile-"]')],
maskColor: '#FF00FF',
});
maskColor を目立つ色にすると、レポートでマスクされた箱が明確に見えます。レビュー担当者は、テストが無視している領域を一目で確認できます。
差分のしきい値を慎重に設定する
| オプション | 意味 |
|---|---|
maxDiffPixels | 差があってもよいピクセル数。 |
maxDiffPixelRatio | 差があってもよいピクセルの割合。0 から 1 まで。 |
threshold | 1 ピクセルの色の差が差分として数えられるしきい値。0 から 1 まで。既定値は 0.2。 |
許容値は低く保ちます。maxDiffPixelRatio が 0.01 の場合、ピクセルの 1% が変化しても許容されます。1280×4000 の全ページキャプチャでは、これは 50,000 ピクセルを超えます。欠けたアイコンを隠すのに十分な量です。まず maxDiffPixels を 100 程度に設定し、特定の誤検知が発生した場合にのみ調整します。
全ページキャプチャ 1 枚のしきい値の計算
以下の数値は、例の料金ページと同じ 1280 × 4000 ピクセルのキャプチャ 1 枚を使っています。24 × 24 ピクセルのアイコンは 576 ピクセルを占めます。1% の比率で許容値を設定すると、そのアイコン 88 個分が消えても失敗しません。
設定で既定値を一度だけ指定する
新しいテストでは、呼び出しごとのオプションを忘れがちです。設定ファイルの expect.toHaveScreenshot ブロックは、すべてのアサーションの既定値を指定します。個別のテストでは、引き続きこれを上書きできます。
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
threshold: 0.2,
animations: 'disabled',
caret: 'hide',
scale: 'css',
},
},
});
scale: 'css' は CSS ピクセル 1 つを画像ピクセル 1 つに対応させるため、Retina 相当のデバイスプロファイルでも画像サイズが 2 倍になりません。ベースラインのサイズが小さく保たれ、上記のピクセル数の計算も意味を持ち続けます。
ベースラインを確認して更新する
意図的なデザイン変更を行った場合は、影響するベースラインを再生成します。
npx playwright test --update-snapshots
ファイル名を指定するか、テストタイトルを -g で指定して、失敗したテストだけを更新します。スイート全体を一括で更新すると、意図しない変更を見落としやすくなります。
npx playwright test tests/pages.visual.spec.ts -g "page pricing" --update-snapshots
3 つの画像を順に確認する
まず差分ではなく、実際の画像から確認します。実際の画像を見れば、人の目からページが壊れて見えるかどうかがわかります。次に差分を開き、ピクセルが変化した箇所を特定します。最後に期待画像を開き、変更前の状態を確認します。
差分では、変化したピクセルが赤で示されます。テキストブロックを囲む細い赤い枠は、通常、フォントやアンチエイリアスの変更を示し、レイアウトの不具合ではありません。1 か所の赤い四角形は、通常、要素が移動したか、表示されたか、消えたことを示します。
HTML レポートには、期待画像と実際の画像を切り替えるスライダー表示もあります。変更された領域でスライダーをドラッグすると、差分よりも、2 ピクセルのマージン変更のような小さなずれを明確に確認できます。
ベースラインのライフサイクル
ページが意図的に変更されるたびに、ベースラインは同じサイクルをたどります。このサイクルには判断ポイントが 1 つあります。差分が意図したものかどうかです。それ以外の工程は機械的に進みます。
プルリクエストでベースラインの変更をレビューする
プルリクエストで変更された PNG は、コードの変更とみなされます。GitHub や多くのレビューツールは、画像の差分を並べて表示します。下のチェックリストは、例のチームが承認前に確認する項目です。
CI でスクリーンショットテストを実行する
macOS で作成したベースラインは、Linux ランナーでのキャプチャと一致しません。フォントやアンチエイリアスが異なるためです。確実な構成は、同じ環境でベースラインを生成して比較することです。通常は、インストールされたパッケージと同じバージョンの公式 Playwright Docker イメージを使います。
一般的な運用手順は次のとおりです。
- 開発者は、macOS ではなく Docker イメージ内でビジュアルスイートを実行する。
- CI は同じイメージで実行し、失敗時に HTML レポートをアーティファクトとしてアップロードする。
- 意図した変更には新しいベースラインを作成し、コード変更と一緒にコミットする。
- レビュー担当者は、プルリクエストで画像の差分を確認する。
Mac で Docker を使ってベースラインを生成する
イメージタグは、固定された Playwright のバージョンと一致させる必要があります。一致しないと、画像で取り除こうとしているフォントや描画の差が再び現れます。コマンドはプロジェクトのルートで実行します。
docker run --rm --ipc=host -v "$(pwd)":/work -w /work \
mcr.microsoft.com/playwright:v1.48.0-noble \
/bin/bash -c "npm ci && npx playwright test --update-snapshots -g 'page pricing'"
ビジュアルスイートの CI ジョブ
ジョブは同じイメージ内で実行されるため、ブラウザーのダウンロード手順は不要です。レポートは、テストが失敗したときだけアップロードされます。レポートには、失敗ごとの期待画像、実際の画像、差分画像が含まれます。
# .github/workflows/visual.yml
name: visual
on: pull_request
jobs:
visual:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.48.0-noble
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test tests/*.visual.spec.ts
env:
HOME: /root
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
例では、ジョブ全体の実行時間は約 5 分です。8 件のコンポーネントアサーションはすべてのプルリクエストで実行し、36 件のページアサーションは main ブランチでのみ実行すると、プルリクエストのジョブは 3 分未満に短縮されます。
単純なスクリーンショット自動化が適する場面
ビジュアルテストが答えるのは 1 つの問いだけです。このページは、ベースラインから変わったか。デザインミーティング、クライアントへのレポート、日付付きのアーカイブ向けのレビュー用セットは作成しません。また、エンジニア以外のメンバーがこれを実行することはほとんどありません。
そうした用途には、単純なキャプチャツールの方が手軽です。Temposhot Website Capture は、Mac 上で URL のリストを受け取り、テストコードなしで各ページを Desktop 1920×1080、Tablet 768×1024、Mobile 390×844 でキャプチャします。2 つのキャプチャを目視で比較するには、Compare Images がブラウザーで違いを強調表示します。スクリーンショット比較 の項目では、この手法を一般的に説明しています。
2 つのツールは別の問いに答える
ビジュアルスイートとキャプチャツールは、同じチーム内で並行して使われることがよくあります。スイートはエンジニアが管理します。デザイナーやプロダクトマネージャーは、キャプチャをレビューやアーカイブに使います。
よくある間違い
- ノート PC で生成したベースラインをコミットし、Linux の CI で比較する。
- 日付、カルーセル、広告を含む全ページをアサーションする。
- 不安定なテストを黙らせるためにしきい値を上げ、ページの状態を修正しない。
- 1 件の意図した変更の後に、スイート全体に対して
--update-snapshotsを実行する。 - デスクトップのベースラインが安定する前に、多くのデバイスプロジェクトを追加する。
失敗したビジュアルビルドのトリアージ
ビジュアルジョブが失敗した場合は、再実行ではなく判断が必要です。不安定なテストを合格するまで再実行すると、原因が隠れ、次の本当の回帰が見逃されます。以下の順番で、ほとんどの失敗は数分で切り分けられます。
- レポートのアーティファクトをダウンロードし、失敗したアサーションの数を数える。
- 1 つのプロジェクトのすべてのアサーションが失敗した場合は、まずそのプロジェクトでブラウザーやフォントが変わっていないか確認する。
- 1 ページが 3 つのプロジェクトすべてで失敗した場合は、そのページの実際の画像を開く。原因は通常ページ側にある。
- 1 ページが 1 つのプロジェクトだけで失敗した場合は、ブラウザー固有の CSS の問題か、上の表にある不安定さの原因を確認する。
- 原因を修正するか、そのベースライン 1 件だけを更新する。両方を同じコミットで行うことはしない。
よくある質問
ベースライン画像はどこに保存されますか?
既定では、テストファイルの隣に、テストファイル名に -snapshots サフィックスを付けたフォルダーに保存されます。設定の snapshotPathTemplate オプションで保存先を変更できます。
Playwright Test を使わずにスクリーンショットを比較できますか?
はい。任意のツールでキャプチャし、画像差分ツールでファイルを比較できます。Playwright Test は、その比較に再試行、マスク、レポートを追加します。
スクリーンショットテストはすべてのプルリクエストで実行すべきですか?
コンポーネント単位のアサーションであれば、通常は実行します。全ページを対象にした大規模なスイートは、パイプラインが遅くなる場合にスケジュール実行やリリース前の実行に回します。
コードを変更していないのに、テストがローカルで成功し CI で失敗するのはなぜですか?
2 つの環境ではテキストの描画方法が異なるためです。フォント、アンチエイリアス、ブラウザーのビルドは、macOS と Linux で違う場合があります。スイートはローカルと CI の両方で同じ Docker イメージ内で実行します。
新しいスイートは何個のプロジェクトで始めるべきですか?
1 つです。デスクトップのプロジェクト 1 つだけで始め、そのベースラインが 1、2 週間緑のままになるまで待ちます。その後、モバイルのプロジェクトを追加します。モバイルのレイアウトは最も差が大きいためです。2 つ目のデスクトップブラウザーは最後に追加し、ブラウザー固有のレイアウトの不具合がユーザーから報告された場合にのみ追加します。
スイートでログインの後ろにあるページをテストできますか?
はい。セットアップ用のプロジェクトで一度ログインし、storageState でセッションを保存して、そのファイルをビジュアルプロジェクトで読み込みます。名前や数値が毎回同じになるように、固定データを持つ専用のテストアカウントを使います。
ベースラインは Git LFS で保存すべきですか?
スイートに数百の画像が含まれる場合や、PNG ファイルの変更が頻繁な場合に役立ちます。約 60 ファイルのスイートであれば、通常は通常の Git で十分です。数か月間ベースラインを更新した後に、リポジトリのサイズを確認します。
参考資料
ノーコードでウェブサイトのスクリーンショット自動化が必要な場合
目的が合否判定ではなくスクリーンショットのセットである場合は、Temposhot Website Capture が Mac 上で URL のリストをローカルで実行します。ウェブサイトのスクリーンショット自動化 または Website Capture クイックスタート を参照してください。
playwrightvisual-testingscreenshot-comparisonci
このページで使用する用語: playwright, responsive screenshot, screenshot automation, screenshot comparison, viewport screenshot, visual regression testing
繰り返し撮影するスクリーンショットを、完成したデータに仕上げます
Temposhotは、タイマーまたはクリックごとに撮影し、結果をPDF、画像、ZIP、MP4で書き出します。
関連記事
- Playwright で Webサイトのスクリーンショットを自動化する方法Playwright と JavaScript で Webサイトのスクリーンショットを自動化します。1ページの取得、ページ全体、モバイル表示、リトライ付きの URL バッチを扱います。
- PuppeteerでWebサイトのスクリーンショットを自動化する方法JavaScriptとPuppeteerでWebサイトのスクリーンショットを自動化します。1ページのキャプチャ、フルページ撮影、URLの一括処理、モバイル表示幅を、再利用可能なスクリプトで扱います。
- ノーコードでWebサイトのスクリーンショットを自動化する方法ノーコードでWebサイトのスクリーンショットを自動化する方法を選びます。URL一覧からのキャプチャ、スクリーンショットAPIを使うオートメーションプラットフォーム、監視ツール、ブラウザーの補助ツールを扱います。
- 2026 年版: Mac 向け Xnapper の代替アプリ比較Mac 向け Xnapper の代替アプリを、繰り返し撮影、スクロールキャプチャと録画、無料の背景、クロスプラットフォームのガイド、標準搭載アプリの観点で比較します。