Nuxtで構築したWebアプリのリリース前に「画面が正しく動くか」を手動で全ページ確認するのは、現実的ではありません。SSRとCSRが混在するNuxt特有のレンダリング挙動は、単体テストだけではカバーしきれない領域です。

Playwrightは、Chromium・Firefox・WebKitの3エンジンを単一APIで自動操作できるE2Eテストフレームワークです。Nuxt公式パッケージ @nuxt/test-utils がPlaywrightとの統合をファーストクラスでサポートしており、SSRハイドレーション完了の自動待機など、Nuxt固有の課題を解決する仕組みが組み込まれています。

PlaywrightがNuxtのE2Eテストに適している理由

NuxtアプリのE2Eテストでは、通常のSPA向けテストにはない固有の課題があります。

SSRハイドレーションの同期問題: Nuxtはサーバーサイドで描画したHTMLをブラウザに送信し、その後JavaScriptがDOMを引き継ぐ「ハイドレーション」を行います。テストコードがハイドレーション完了前に要素を操作すると、ボタンが反応しない・イベントが発火しないといった不安定なテスト(Flaky Test)が生まれます。

@nuxt/test-utils/playwrightgoto フィクスチャに waitUntil: 'hydration' オプションを提供しており、ハイドレーション完了を自動で待機してからテストを実行できます。これはPlaywright標準の page.goto() にはないNuxt専用の拡張機能です。

3ブラウザエンジンの同時テスト: Nuxtで構築するWebサイトはSEOを重視するケースが多く、Safari(WebKit)を含むクロスブラウザ動作保証が求められます。PlaywrightはChromium・Firefox・WebKitをネイティブサポートしているため、1つのテストコードで3ブラウザの動作を検証できます。

環境構築の手順

必要なパッケージのインストール

Nuxtプロジェクトのルートで以下を実行します。

npm install -D @playwright/test @nuxt/test-utils
npx playwright install

@playwright/test がPlaywrightのテストランナー本体、@nuxt/test-utils がNuxtとの統合レイヤーです。npx playwright install で各ブラウザのバイナリをダウンロードします。

playwright.config.ts の作成

プロジェクトルートに playwright.config.ts を配置します。

import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'

export default defineConfig<ConfigOptions>({
  testDir: './test/e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
    nuxt: {
      rootDir: fileURLToPath(new URL('.', import.meta.url)),
    },
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
})

nuxt.rootDir にプロジェクトルートを指定することで、@nuxt/test-utils がNuxtの設定ファイルを自動認識し、テスト実行時にNuxtサーバーを起動します。

ディレクトリ構成

テストの種類ごとにフォルダを分離すると、テストランナーの競合を防げます。Nuxt公式ドキュメントでは test/(単数形)を使用していますが、プロジェクトの慣習に合わせて tests/ でも動作します。

test/
├── e2e/              # Playwright E2Eテスト
│   ├── home.test.ts
│   └── auth.test.ts
├── nuxt/             # Nuxtランタイム環境テスト(Vitest)
│   ├── components.test.ts
│   └── composables.test.ts
└── unit/             # 純粋なユニットテスト
    └── utils.test.ts

E2Eテスト(Playwright)とユニットテスト(Vitest)を同一ディレクトリに混在させると、テストランナーの競合が発生する場合があります。test/e2e/ に分離し、playwright.config.tstestDir をこのパスに向けることでトラブルを防げます。

最初のE2Eテストを書く

基本的なページ遷移テスト

import { expect, test } from '@nuxt/test-utils/playwright'

test('トップページが正しく表示される', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
})

test('ナビゲーションリンクで別ページに遷移できる', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await page.getByRole('link', { name: 'About' }).click()
  await expect(page).toHaveURL('/about')
  await expect(page.getByRole('heading', { level: 1 })).toContainText('About')
})

@nuxt/test-utils/playwright からインポートした testgoto を使うことで、Nuxtサーバーの起動やハイドレーション待機が自動的に処理されます。Playwright標準の page.goto() ではなく、goto() フィクスチャを使う点が重要です。

フォーム操作のテスト

import { expect, test } from '@nuxt/test-utils/playwright'

test('お問い合わせフォームを送信できる', async ({ page, goto }) => {
  await goto('/contact', { waitUntil: 'hydration' })

  await page.getByLabel('お名前').fill('テスト太郎')
  await page.getByLabel('メールアドレス').fill('test@example.com')
  await page.getByLabel('お問い合わせ内容').fill('テスト用のメッセージです')

  await page.getByRole('button', { name: '送信' }).click()
  await expect(page.getByText('送信が完了しました')).toBeVisible()
})

APIレスポンスのモック

外部APIに依存するテストでは、page.route() でレスポンスをモックすると安定したテストが書けます。

import { expect, test } from '@nuxt/test-utils/playwright'

test('ユーザー一覧をAPIから取得して表示する', async ({ page, goto }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: '山田太郎' },
        { id: 2, name: '佐藤花子' },
      ]),
    })
  })

  await goto('/users', { waitUntil: 'hydration' })
  await expect(page.getByText('山田太郎')).toBeVisible()
  await expect(page.getByText('佐藤花子')).toBeVisible()
})

codegenでテストコードを自動生成する

Playwrightには codegen という強力なコード自動生成ツールがあります。ブラウザ上の操作を記録し、そのままテストコードとして出力できます。

npx playwright codegen http://localhost:3000

ブラウザとインスペクタウィンドウが同時に開き、クリックやテキスト入力などの操作がリアルタイムでコードに変換されます。生成されたコードをコピーして .test.ts ファイルに貼り付け、アサーション(expect)を追加すれば実用的なテストが完成します。

codegen活用のポイント:

  • 初期テンプレートの作成に最適(ゼロからセレクタを書く手間が省ける)
  • 生成後はロケータ戦略を見直す(getByRolegetByLabel に変更し、壊れにくいテストにする)
  • Nuxt固有の goto() フィクスチャに書き換える

VRT(Visual Regression Test)の導入

見た目の意図しない変更を検出するVisual Regression Test(VRT)は、CSSリファクタリングやデザインシステム更新時に威力を発揮します。

スクリーンショット比較テスト

Playwrightには toHaveScreenshot() アサーションが組み込まれており、追加パッケージなしでVRTを実行できます。

import { expect, test } from '@nuxt/test-utils/playwright'

test('トップページの見た目が変わっていない', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.01,
  })
})

初回実行時にベースラインとなるスクリーンショットが保存され、2回目以降は差分を比較します。差分が maxDiffPixelRatio の閾値を超えるとテストが失敗します。

動的コンテンツのマスク処理

日時表示やランダムIDなど、実行ごとに変わる要素はマスクして比較対象から除外します。

test('ダッシュボードの見た目が変わっていない', async ({ page, goto }) => {
  await goto('/dashboard', { waitUntil: 'hydration' })
  await expect(page).toHaveScreenshot('dashboard.png', {
    mask: [
      page.locator('.current-time'),
      page.locator('.random-avatar'),
    ],
  })
})

VRT実行環境の統一

スクリーンショットはOS・フォント環境によって微妙に異なるため、ローカルとCIで同一環境を使うことが重要です。Playwright公式のDockerイメージを利用すると環境差異を排除できます。

docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.58.0 \
  npx playwright test --update-snapshots

認証フローの効率化(storageState)

ログインが必要なページのテストでは、毎回ログイン操作を繰り返すと実行時間が大幅に増加します。Playwrightの storageState を使えば、認証状態をJSONファイルに保存して再利用できます。

セットアッププロジェクトでログイン状態を保存

// playwright.config.ts
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'

const authFile = 'test/.auth/user.json'

export default defineConfig<ConfigOptions>({
  testDir: './test/e2e',
  use: {
    nuxt: {
      rootDir: fileURLToPath(new URL('.', import.meta.url)),
    },
  },
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: authFile,
      },
      dependencies: ['setup'],
    },
  ],
})
// test/e2e/auth.setup.ts
import { expect, test as setup } from '@nuxt/test-utils/playwright'

const authFile = 'test/.auth/user.json'

setup('ログイン状態を保存する', async ({ page, goto }) => {
  await goto('/login', { waitUntil: 'hydration' })
  await page.getByLabel('メールアドレス').fill('user@example.com')
  await page.getByLabel('パスワード').fill('password123')
  await page.getByRole('button', { name: 'ログイン' }).click()
  await page.waitForURL('/dashboard')
  await page.context().storageState({ path: authFile })
})

setup プロジェクトが最初にログインを実行し、Cookie・LocalStorageを user.json に保存します。後続のテストプロジェクトは storageState を読み込むだけで認証済み状態から開始できます。

@nuxt/test-utils が提供するヘルパー関数

@nuxt/test-utils にはE2Eテスト以外にも、ユニットテスト・コンポーネントテストで使える便利なヘルパーがあります。テスト戦略に応じて使い分けると効率的です。

ヘルパー用途テストレイヤー
mountSuspended非同期setupのあるVueコンポーネントをNuxt環境内でマウントコンポーネントテスト
renderSuspendedTesting Libraryと連携したコンポーネント描画コンポーネントテスト
mockNuxtImportuseRouteuseFetch など自動インポートのモックユニット/コンポーネント
mockComponent子コンポーネントをスタブに差し替えコンポーネントテスト
registerEndpointNitroサーバーのエンドポイントをモック化ユニット/E2E
createPagePlaywrightブラウザインスタンスの作成E2E

mountSuspended はNuxtの <Suspense> 境界やプラグインインジェクションを正しくハンドリングするため、@vue/test-utilsmount では動作しないNuxt固有のコンポーネントもテスト可能です。

CI/CDパイプラインへの組み込み

GitHub Actionsの設定例

name: E2E Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/playwright:v1.58.0
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Playwright公式のDockerイメージをコンテナとして使用すると、ブラウザのインストール手順が不要になり、VRTのスクリーンショット比較もローカルと一致します。

テスト失敗時のデバッグ:

trace: 'on-first-retry' を設定しておくと、失敗したテストの再実行時にトレースファイルが生成されます。playwright-report/ をArtifactとしてダウンロードし、npx playwright show-trace trace.zip でタイムライン形式のデバッグビューアを開けます。

Playwright vs Cypress:Nuxtプロジェクトでの選定基準

比較項目PlaywrightCypress
ブラウザエンジンChromium・Firefox・WebKitChromium・Firefox(WebKit実験的サポート・制限あり)
Nuxt公式統合@nuxt/test-utils/playwright で公式サポート公式統合パッケージなし
SSRハイドレーション待機waitUntil: 'hydration' で自動待機手動でのポーリング実装が必要
テスト実行速度基準値シナリオにより1.2〜4倍遅い(出典: Checkly
並列実行標準機能(ワーカー数指定 + シャーディング)Dashboard(有料)で並列化
マルチタブ操作対応(複数BrowserContext)非対応
対応言語TypeScript・JavaScript・Python・Java・C#TypeScript・JavaScriptのみ
テストコード生成codegen(ブラウザ操作記録)Cypress Studio(限定的)
デバッグツールTrace Viewer・UI Mode・InspectorTime-Travel Debugging

NuxtプロジェクトではPlaywrightを選ぶメリットが大きい理由は、公式テストユーティリティとの統合にあります。@nuxt/test-utils がPlaywrightのテストランナーをファーストクラスでサポートしているため、ハイドレーション待機やNuxtサーバーの自動起動が設定不要で動作します。Cypressでは同等の動作を実現するために独自のプラグイン開発やカスタム待機ロジックの実装が必要です。

よくあるトラブルと対処法

テストがランダムに失敗する(Flaky Test)

原因: ハイドレーション完了前にアサーションが実行されている

対処: goto(){ waitUntil: 'hydration' } を必ず指定する。Playwright標準の page.goto() を使っている場合は @nuxt/test-utils/playwrightgoto フィクスチャに切り替えます。

SSRモードでテストが動作しない

原因: @nuxt/test-utils が古いバージョンのままで、SSR対応の待機処理がサポートされていない

対処: @nuxt/test-utils を v4 以上にアップデートします。v4 では Playwright Test Runner との統合が強化され、SSR環境での安定性が向上しています。

npm install -D @nuxt/test-utils@latest

CIでスクリーンショットが一致しない

原因: ローカル環境とCI環境でOS・フォントが異なる

対処: ローカルでもPlaywright公式Dockerイメージを使ってスクリーンショットを更新します。

docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.58.0 \
  npx playwright test --update-snapshots

ポート競合でNuxtサーバーが起動しない

原因: 他のプロセスがNuxtの開発サーバーポートを使用している

対処: playwright.config.tsnuxt オプションでポートを明示的に指定するか、テスト実行前に既存のプロセスを停止します。

まとめ

NuxtアプリのE2Eテスト環境を構築する際、Playwrightは @nuxt/test-utils との公式統合・SSRハイドレーション自動待機・3ブラウザエンジン対応という3つの強みで最適な選択肢です。

導入手順を振り返ると:

  1. @playwright/test@nuxt/test-utils をインストール
  2. playwright.config.tsnuxt.rootDir を設定
  3. @nuxt/test-utils/playwright から testgoto をインポートしてテスト記述
  4. waitUntil: 'hydration' でSSRのFlaky Testを防止
  5. toHaveScreenshot() でVRTを追加し、見た目の変更検知も自動化
  6. GitHub Actions + Playwright Dockerイメージで安定したCIパイプラインを構築

テストの対象範囲を広げる際は、Vitestでのユニット・コンポーネントテストと組み合わせ、テストピラミッドを意識した構成にすると保守性が高まります。@nuxt/test-utilsmountSuspendedmockNuxtImport を活用すれば、E2Eに頼りすぎない効率的なテスト戦略が組み立てられます。