PlaywrightのtestIgnore設定がグローバルからプロジェクト固有で上書きされる問題を、Vitestで設定検証する回帰ガードで解決し、Chromium・WebKit・FirefoxでのE2Eテストをパスさせるプロセスを描いた図。

はじめに

Playwright で chromium / webkit / firefox と専用 project を組んでいると、「グローバルの testIgnore で除外したはずの spec が、特定の project でだけ実行されてしまう」という罠にはまることがあります。

原因は仕様にあります。project レベルで testIgnore を定義すると、グローバルの testIgnore はマージされず、上書きされます。
この記事では、実運用のサービスで起きた漏れの事例をもとに、定数の export による再掲と、config の構造をユニットテストで守る回帰ガードの作り方を紹介します。

こんな人におすすめ

  • Playwright で複数の project を定義している方
  • グローバルの testIgnore や testMatch を設定しているのに、意図しない spec が走って困った方
  • PR 用の絞り込み実行と、nightly のフル実行を分けている方
  • 設定ファイルそのものにもテストを書きたい方

何が起きたのか:nightly だけで壊れる

実運用しているサービスで、次の2種類の spec を通常の E2E から除外していました。

  • 本番 origin を叩く視覚的回帰テスト用の spec(ベースライン画像が無い環境では必ず失敗します)
  • 専用ブラウザ環境でしか動かない Service Worker の spec

グローバルの testIgnore に書いてあるので安心していたところ、あるブラウザ project が独自の testIgnore を持っていました。
その project では、グローバルの除外が丸ごと無効になり、除外したはずの spec が走って失敗しました。

厄介だったのは、PR の実行が @critical タグの grep で絞り込まれていたことです。
そのため PR ではグリーンのまま通過し、grep なしの nightly でだけ失敗として顕在化しました。
通常実行とフル実行が別れているほど、こうした漏れは見つかりにくくなります。

対策1:glob を定数にして export し、各 project に再掲する

まず、除外したいパターンを定数として定義し、export します。

// playwright.config.ts

// 視覚的回帰テスト専用 spec — 本番 origin を叩くため通常 E2E からは除外する
export const VISUAL_SPEC_IGNORE = '**/*.visual.spec.ts'
// 専用環境でのみ動く spec — それ以外の project では一律 ignore
export const SERVICE_WORKER_SPEC_IGNORE = '**/service-worker-lifecycle.spec.ts'

// project レベルの testIgnore はグローバルを上書きするため、
// 独自 testIgnore を持つ project には必ずこの配列を渡す
const DEV_SERVER_SPEC_IGNORE = [SERVICE_WORKER_SPEC_IGNORE, VISUAL_SPEC_IGNORE]

あとは、独自の testIgnore を持つ project でこの配列を明示的に参照します。
パターンを直接書かず定数を参照させると、新しい project を足すときに「何を入れるべきか」が定数名から分かります。
コメントに「上書きされるため再掲が必要」と書いておけば、レビューのコストも下がります。

export しておくと、テストファイルから config の定数をインポートできるようになります。これが次の回帰ガードの土台です。

対策2:config の選択ロジックをユニットテストで再現する

再掲を忘れたとき、CI で即座に気づけるようにします。
Playwright は内部で minimatch を使って spec のパスを解決しているため、同じライブラリで判定を模倣できます。

// e2e/lib/playwright-config.test.ts

// Playwright と同じ「いずれかの glob に一致すれば該当」判定
function matchesAny(filePath: string, patterns: string[]): boolean {
  return patterns.some((pattern) => minimatch(filePath, pattern))
}

// project レベルの設定はグローバルを上書きする(?? でフォールバック)
function resolveSelection(
  filePath: string,
  globalConfig: { testMatch?: unknown; testIgnore?: unknown },
  project: { testMatch?: unknown; testIgnore?: unknown },
): boolean {
  const effectiveMatch = toGlobArray(project.testMatch ?? globalConfig.testMatch)
  const effectiveIgnore = toGlobArray(project.testIgnore ?? globalConfig.testIgnore)
  const matched = effectiveMatch.length === 0 || matchesAny(filePath, effectiveMatch)
  const ignored = matchesAny(filePath, effectiveIgnore)
  return matched && !ignored
}

ポイントは ?? によるフォールバックです。project 側に値があればグローバルを見ない、という上書きの仕様をそのままコードで表現しています。
toGlobArray は、文字列・正規表現・配列・未定義のいずれも配列に正規化する自前のヘルパーを想定しています。

次に、全 project に対してパラメトリックにアサートします。

const SAMPLE_VISUAL_SPEC = 'e2e/pages/top.visual.spec.ts'

it.each(projects.map((project) => [project.name ?? '(no name)', project] as const))(
  'project "%s" は visual spec を選択しない',
  (_name, project) => {
    const selected = resolveSelection(SAMPLE_VISUAL_SPEC, playwrightConfig, project)
    expect(selected).toBe(false)
  },
)

it.each で project 一覧を回しているので、新しい project が追加されると自動でテスト対象に入ります。手動でテストを追記する必要はありません。

実ファイルのパスは定数と独立に書く

テストで使うサンプルのパスは、定数から組み立てず、実際のファイルパスとして別に書きます。
定数どうしを比較するだけだと、定数の値自体が間違っていてもテストが通ってしまうためです。
「glob 文字列が妥当か」と「適用した結果が正しいか」を同時に保証できます。

対策3:Vitest の node-only グループに登録する

この config テストはブラウザを必要としません。Playwright ではなく Vitest の Node.js 環境で実行すると、高速に検証できます。

// vitest.config.ts
const nodeOnlyTests = [
  // ...
  'e2e/lib/playwright-config.test.ts', // playwright.config.ts の構造テスト
]

ブラウザの起動が要らないので、PR のたびに実行しても負担になりません。
nightly でしか見つからなかった問題を、PR の時点で検知できるようになります。

つまづきやすいポイント

  • 上書きであってマージではない:公式ドキュメントに記載はありますが、既存の testIgnore がある config に project を足すときに忘れがちです。testMatch も同様の挙動か、一度確認しておくと安心です。
  • PR の grep 実行ではグリーンに見える:タグで絞った実行では漏れが隠れます。構造テストで通常実行・フル実行の差を埋めるのが有効です。
  • 定数だけを比較するテストは抜け穴になる:実ファイルパスを独立して書き、定数の誤りも検出できるようにしてください。
  • 模倣ロジックは Playwright の仕様変更に追従が必要:minimatch で再現した判定は、Playwright の内部挙動が変わると乖離する可能性があります。アップデート時に見直すのがおすすめです。

まとめ

project 別の testIgnore はグローバル設定を上書きするため、除外したい spec は各 project に明示的に再掲する必要があります。
glob を定数として export すれば、再掲が楽になり、config 自体もテスト可能になります。
さらに minimatch で選択ロジックを再現し、it.each で全 project を検証すれば、project 追加時の漏れを CI で自動検知できます。
設定ファイルは「書いて終わり」にせず、構造をテストで守るという考え方は、他の設定ファイルにも応用できそうです。