【Playwright】Playwrightで始めるビジュアル＆アクセシビリティ回帰テスト

# はじめに
UI の視覚的・構造的変更を意図せず検知し、品質を保つために「スナップショットテスト」は欠かせません。Playwright のスナップショット機能を活用すると、テスト実行時に自動でベースラインを生成・比較し、差分が生じた場合には失敗として報告できます。また、ARIA ツリーを YAML 形式で比較できるため、アクセシビリティ構造の回帰も同時に検知可能です。

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

この記事では、Playwright のスナップショットテストの概要と、画像スナップショットおよび ARIA スナップショットの仕組み、さらに通常のスクリーンショット取得との違いを記載します。

# 開発環境
開発環境は以下の通りです。

- Windows 11
- VSCode
- Vite 6.3.4
- React 18.3.1
- TypeScript 5.8.3
- Playwright 1.52.0

# 事前準備
まず、Playwright の利用設定を行います。
手順は公式ドキュメントや以下の記事に記載があります。

https://playwright.dev/docs/next/intro

https://qiita.com/Yasushi-Mo/items/59cb2e090c0da6f5c988

上記の設定に加え、`playwright.config.ts` にスナップショットの出力先やグローバル設定を追加します。

```playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // ─── 全スナップショット共通の出力先テンプレート ───
  // {projectName} や {testFilePath} などのプレースホルダーが利用可能
  snapshotPathTemplate:
    "src/__tests__/e2e/__snapshots__/{projectName}/{testFilePath}/{arg}{ext}",

  expect: {
    // ─── 画像スナップショットのグローバル設定 ───
    toHaveScreenshot: {
      // ベースライン画像と比較して「ピクセル単位」で許容する差分の絶対値
      maxDiffPixels: 100,
      // 画面全体のピクセル数に対する「差分ピクセルの割合」 (0-1)
      threshold: 0.1,
      // スナップショット安定化のために動的要素を隠す CSS ファイルを指定
      stylePath: "__tests__/styles/screenshot.css",
    },
  },
});

```

```__tests__/styles/screenshot.css
.loading-spinner,
.dynamic-id {
  visibility: hidden !important;
}
```

:::note
`maxDiffPixels` と `threshold` について

どちらもテストの「差分許容度」を制御しますが、役割が少し異なります。

- `maxDiffPixels`（最大ピクセル差分数）
maxDiffPixels: 100 とすると、テスト中のスクリーンショットとベースライン画像のあいだで最大 100 ピクセルまでの違い（色の異なるピクセル）が許されます。101 ピクセル以上変化があるとテスト失敗します。

- `threshold`（差分ピクセル比率）
threshold: 0.02 とすると、スクリーン全体の 2% 以内の違いであれば許容されます。たとえば 1,000×1,000px（100 万ピクセル）の場合、20,000 ピクセル（100 万 × 0.02）まで差分が合格です。

両者の使い分け

- `maxDiffPixels`: テスト対象の画面サイズが変動しにくく、「何ピクセルまでなら絶対的にズレを許すか」を厳密に設定したいとき向き
- `threshold`: 画面サイズが異なる環境（画面解像度やデバイス）でも「差分は画面の何％まで許容するか」で相対的に調整したいとき向き

:::

# スナップショットテスト概要
Playwright では、以下の２種類のスナップショットアサーションが利用できます。

- 画像スナップショット：`expect(page).toHaveScreenshot()` / `expect(locator).toHaveScreenshot()`
  - DOMの描画結果をピクセル単位で比較
- ARIA スナップショット：`expect(locator).toMatchAriaSnapshot()`
    - アクセシビリティツリー（role や aria-* 属性など）を比較

いずれも初回実行時にベースラインを生成し、再実行時に差分がある場合にテスト失敗となります。更新したい場合は `--update-snapshots` を使います。

:::note
スクリーンショット取得との違い

`page.screenshot()` を使えば任意のタイミングでスクリーンショットを取得できます。ただしこれは差分検出ではなく、取得して保存するだけです。テストとしての検証には使われません。

```test.ts
// ただ保存するだけ
await page.screenshot({ path: 'example.png' });
```

一方、スナップショットテストは下記のように差分比較による失敗検知が目的です。

```test.ts
// 差分があればエラーに
await expect(page).toHaveScreenshot('expected.png');
```

:::

# テスト対象のコンポーネントの実装
テストコードを実装する前にテスト対象のコンポーネントを実装します。
このコンポーネントでは、ボタン操作による数値変化を ビジュアル差分 として、また `role／aria-*` 属性を ARIA ツリー として検証できます。

```Snapshot.tsx
import { FC, useState } from "react";

export const Snapshot: FC = () => {
  const [count, setCount] = useState(0);

  return (
    <div role="region">
      <h1>Counter</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <span role="status">{count}</span>
    </div>
  );
};

```

# 画像スナップショットテスト
画像スナップショットテストでは、ページ全体または要素単位の見た目（ピクセル）に差分があるかどうかを検証できます。
スタイルの崩れや、意図しない変更を検知するのに非常に有効です。
特に、UI の安定性が求められるプロダクトでは、CI で常時テストを回すことで品質を維持できます。
また、動的要素を非表示にする CSS を併用することで、意図しない揺れ（フレーキング）を最小化できます。

```Snapshot.test.ts
import { test, expect } from "@playwright/test";

test("画像スナップショットテスト", async ({ page }) => {
  await page.goto("/snapshot");

  // 初期状態（count: 0）
  await expect(page).toHaveScreenshot("snapshot/image/count-0.png");

  // ボタンを押して count を変化させる
  await page.getByRole("button", { name: "Increment" }).click();

  // 状態変化後（count: 1）
  await expect(page).toHaveScreenshot("snapshot/image/count-1.png");
});
```

# ARIA スナップショットテスト
ARIA スナップショットテストでは、アクセシビリティツリー（スクリーンリーダーの読み取り構造）を YAML 形式で定義し、その構造の差分を検出できます。
`role` 属性の設定漏れや、`aria-*` の状態変化など、見た目ではわからないアクセシビリティの退行を防ぐ手段として有効です。

また、マッチングモード（`contain`, `equal`, `deep-equal`）や、部分一致、正規表現を使った柔軟な検証も可能で、実用性が高いです。
視覚に頼らない検証手段として、アクセシビリティ対応の品質チェックに組み込むと効果的です。

```Snapshot.test.ts
import { test, expect } from "@playwright/test";

test("ARIAスナップショットテスト", async ({ page }) => {
  await page.goto("/snapshot");

  const body = page.locator("body");

  // 初期状態のARIA構造（count: 0）
  await expect(body).toMatchAriaSnapshot(`
    - region:
      - heading "Counter" [level=1]
      - button "Increment"
      - status: "0"
  `);

  // カウント変更
  await page.getByRole("button", { name: "Increment" }).click();

  // 変更後のARIA構造（count: 1）
  await expect(body).toMatchAriaSnapshot(`
    - region:
      - heading "Counter" [level=1]
      - button "Increment"
      - status: "1"
  `);
});

```

# まとめ
Playwright のスナップショットテストを活用することで、UI の視覚的な変更と構造的な変更（アクセシビリティ構造）をいずれも自動で検知できます。

- 画像スナップショット：UI の見た目の変化をピクセル単位で検出
- ARIA スナップショット：アクセシビリティツリーの差分を YAML で比較

通常の E2E テストに加えて、目に見える UI 変更と見えないアクセシビリティ構造の変化を同時に担保できるため、より堅牢な回帰テストを構築できます。

CI に組み込んで定期的に実行することで、UI 品質を継続的に監視しやすくなります。
特に、アクセシビリティ要件があるプロジェクトでは、ARIA スナップショットの導入は有用です。


# 参照
- [Snapshot testing](https://playwright.dev/docs/aria-snapshots)