Dev Guide / 2026 ・ No.07

Playwright 入門と実践

生まれた背景と仕組み、経費精算ページのテスト、そして AI との付き合い方

公開

テストを書けば、ブラウザが勝手に確かめてくれる

expense.spec.ts test('合計が出る', ... getByLabel('金額') .fill('14720') getByRole('button', {name:'申請する'}) .click() expect(total) .toHaveText(..) 人が書くテスト Playwright ブラウザを操作 Chromium Firefox WebKit テストを1回書けば、3種類のブラウザで同じ操作と確認を自動で行う 人がクリックして目で確かめていた作業を、コードにして何度でも再生する

画面を開き、入力し、ボタンを押し、表示を目で確かめる。リリースのたびに人が繰り返してきたこの確認を、
コードにして何度でも再生できるようにするのが Playwright です。この記事では、まず Playwright が生まれた背景と仕組みを図で整理し、
次に経費精算ページを例に、実際にテストを書いて動かします。後半では、Playwright を活かすために設計や開発の段階から
用意しておきたいこと、そして AI にテストを書かせるときにルールを守らせる方法と、すでにある実例、これからの展望を紹介します。

一

Playwright とは

Playwright(プレイライト)は、Microsoft が開発しているブラウザ自動化とテストのためのオープンソースです(ライセンスは Apache 2.0)。プログラムからブラウザを起動し、ページを開き、入力し、クリックし、表示を確かめる。人が手でやっていた操作を、コードで正確に再現します。

  • 3種類のブラウザエンジンに対応 ── Chromium(Chrome・Edge)、Firefox、WebKit(Safari)。Windows・macOS・Linux で動く
  • 4つの言語で書ける ── TypeScript/JavaScript、Python、Java、.NET(C#)。この記事は TypeScript で書く
  • テストの道具一式が付いてくる ── テストランナー(Playwright Test)、操作の録画からコードを作る codegen、失敗を再生するトレースビューア、HTML レポート

主な持ち場は E2E(エンドツーエンド)テストです。画面から DB まで、システムを丸ごと通して「利用者の操作で、期待どおりに動くか」を確かめます。テストにはいくつかの段があり、よく「ピラミッド」にたとえられます。

テストのピラミッドと Playwright の持ち場

E2E 結合・API 単体テスト 画面で申請→承認まで Playwright の主な持ち場 申請 API の入力チェック API を直接呼んで確かめる 合計を計算する関数 数が多く、一瞬で終わる 上ほど本物に近く、遅い E2E は「利用者と同じ操作」で確かめる最後の砦。数は絞って要所に置く 細かい計算の組み合わせは下の段で。上の段は、つながりと大事な流れを見る

E2E は本物に近いぶん時間がかかります。細かいルールの組み合わせは単体テストに任せ、E2E では「大事な流れが最後までつながるか」を見ます

CORE

Playwright は、「人が画面でやる確認」を、速く、何度でも、同じように行うための道具です。ただし、何を確かめるかを決めるのは人。良いテストは、良い仕様と良い画面設計から生まれます。この記事の後半のテーマです。

二

生まれた背景:Selenium、Puppeteer、そして Playwright

ブラウザを自動で動かす試みは、20年以上前から続いています。最初の大きな流れは Selenium です。Selenium の公式サイトによると、始まりは2004年、シカゴの ThoughtWorks 社で、ジェイソン・ハギンズ(Jason Huggins)が社内の「勤怠・経費」アプリ(Time and Expenses)をテストするために作った道具でした。この記事で経費精算を題材にするのは、ちょっとした縁でもあります。Selenium は後に WebDriver と合流し、ブラウザを外から操作する方法は W3C の標準(WebDriver)になりました。

2017年、Google の Chrome DevTools チームが Puppeteer を公開します。Chrome の開発者ツールが使う通信方式(CDP)で Chrome を直接操作するため、速くて細かい制御ができました。ただし当初は Chrome(Chromium)専用。実際のユーザーは Safari も Firefox も使います。

そこで、Puppeteer を作った中心メンバーが Microsoft に移り、2020年1月に公開したのが Playwright です。当時の InfoQ の記事には、チームの「私たちは、もともと Google で Puppeteer を作ったのと同じチームです」という言葉が紹介されています。狙いは二つ。Chromium・Firefox・WebKit を一つの API で動かすこと、そしてテストに向いた APIにすることでした。Puppeteer のテックリードだったアンドレイ・ルシニコフ(Andrey Lushnikov)も、Playwright のコアチームのメンバーとして紹介されています。

Playwright に至る道と、その後

2004 Selenium 誕生(社内の経費アプリのテストから) 2017 Puppeteer(Google・当初は Chrome 専用) 2020.01 Playwright 公開(Microsoft・元 Puppeteer 組) 2020.05 バージョン 1.0 2021.06 Playwright Test とトレースビューア(1.12) 2022.10 getByRole などのロケーター(1.27) 2023.03 UI モード(1.32) 2025.03 Playwright MCP(AI がブラウザを操作) 2025.10 テストエージェント 計画・生成・修復(1.56) 2026.07 MCP サーバーと CLI を本体に同梱(1.62) 2026.10 1.64(WebMCP 対応)── 執筆時点の最新 白丸は前史。括弧内は Playwright のバージョン(日付は npm の公開日)

2021年にテストランナーが付いて「テストの道具」として完成し、2025年からは AI とつなぐ機能が一気に増えています

その後の広がりは急でした。技術コンサルティング会社 Thoughtworks の「Technology Radar」では、2020年10月に Assess(評価)、2021年4月に Trial(試行)、2023年9月に Adopt(採用を推奨)へ。npm の @playwright/test は、2026年10月の時点で週に約7,600万回ダウンロードされています(2026年10月6〜12日。CI や AI エージェントによる自動インストールも含む数字で、利用者数そのものではありません)。執筆時点の最新版は 1.64.1(2026年10月14日)で、ほぼ毎月新しい版が出ています。

三

仕組み①:テストとブラウザのつながり

Playwright は三つの層でできています。テストを書く言語のライブラリ、命令を実際のブラウザ操作に変える Playwright 本体(サーバー)、そしてブラウザです。

命令はどう届くのか

① テストを書く言語 ② Playwright ③ ブラウザと話す TypeScript / JS Python Java .NET Playwright 要素を探す(ロケーター) 準備ができるまで待つ 操作を記録(トレース) 同じプロセス/WebSocket Chromium CDP Firefox Juggler WebKit 独自 パイプで直結 WebDriver(Selenium) 命令ごとに HTTP で問い合わせて返事を待つ Playwright つなぎっぱなしで命令を送り、ブラウザからも出来事が届く Firefox と WebKit は Playwright 用の版を使う(npx playwright install)

TypeScript なら本体はテストと同じプロセスで動き、Python・Java・.NET からは Node.js の本体に WebSocket でつなぎます

  • ブラウザごとの「通訳」を持っている ── Chromium とは CDP、Firefox とは「Juggler」と呼ばれる独自の通信、WebKit とも専用の通信で話す。Playwright のソースには、Firefox 用の Juggler や WebKit 用のパッチがそのまま入っている
  • ブラウザは「パイプ」で直結 ── 起動したブラウザとはパイプ(プロセス間の通り道)でつながり、命令を送るだけでなく、ページの読み込みや画面の変化といった出来事もブラウザ側から届く
  • 手を入れたブラウザを使う ── Firefox と WebKit は、自動操作に必要な機能を足した Playwright 用の版を npx playwright install で入れる。Chromium は、Chrome や Edge 本体を使うこともできる

昔ながらの WebDriver は、命令のたびに HTTP で問い合わせて返事を待つ方式です。Playwright はつなぎっぱなしの通り道の上で双方向にやり取りするので、ブラウザの状態の変化をすぐ知ることができます。これが、次に見る「自動で待つ」仕組みの土台になっています。

四

仕組み②:ブラウザコンテキスト

Playwright の大事な発明がブラウザコンテキストです。一つのブラウザの中に、Cookie も保存領域も別々の「シークレットウィンドウ」をいくつでも作れます。公式ドキュメントの言葉では「速く、安く作れる」。

一つのブラウザに、別々の「人」を同居させる

ブラウザ(1つだけ起動) Chromium など コンテキスト A 田中さん(申請者)としてログイン 申請画面 一覧 Cookie・保存領域は別々 コンテキスト B 佐藤さん(承認者)としてログイン 承認待ち Cookie・保存領域は別々 コンテキストは「まっさらなシークレットウィンドウ」。作るのは一瞬 テストごとに新しいコンテキストを使うので、前のテストの状態を引きずらない

経費精算なら、申請者と承認者が同時にログインしている状況を、一つのテストの中で再現できます(十章)

Playwright Test は、テストごとに新しいコンテキストを用意します。前のテストのログイン状態や入力内容を引きずらないので、テストを並列に走らせても、順番を入れ替えても結果が変わりません。「前のテストが落ちたせいで、次のテストも全部落ちる」という連鎖を防げます。

五

仕組み③:自動で待つ

画面の自動テストでいちばん多いトラブルは「待ち」です。ボタンがまだ出ていないのに押そうとして失敗する。それを避けるために「2秒待つ」と書くと、速い日はムダに待ち、遅い日はやはり失敗します。Playwright はこの問題を自動待機(オートウェイト)で解決しました。

「何秒待つか」を書かない

0.0秒 0.5秒 1.0秒 1.5秒 2.0秒 画面の準備ができた 固定で待つ昔ながらの書き方 waitForTimeout(2000) ムダな待ち(遅い日は失敗) 自動待機Playwright すぐ押す クリックの前に Playwright が確かめること 1 1つに決まる 2 見えている 3 止まっている 4 隠れていない 5 押せる 「何秒待つか」を書かない。準備ができたら、すぐ動く expect(…).toHaveText() も同じで、期待どおりになるまで最大5秒くり返し確かめる

確かめる項目は操作によって変わります(入力なら「編集できるか」も見る)。条件がそろわないまま制限時間を過ぎると、TimeoutError で失敗します

確認(アサーション)も同じ考え方です。expect(合計).toHaveText('3,000円') と書くと、Playwright は表示が「3,000円」になるまで繰り返し確かめます。これを Web ファーストのアサーションと呼びます。既定の制限時間は、確認1つにつき5秒、テスト1本で30秒(この記事のテストでも、失敗時の表示でこの値を確認しました)。

六

仕組み④:要素の探し方と道具

画面のどの部品を操作するかを指定するのがロケーターです。Playwright の公式ガイドは、利用者に見える手がかりで探すことを勧めています。

ロケーターのおすすめ順

おすすめ順 変更への強さ 1 getByRole('button', { name: '申請する' }) 利用者に見える「役割」と「名前」で探す(いちばんのおすすめ) 2 getByLabel('金額') 入力欄はラベルで。画面の読み上げとも一致する 3 getByText('申請しました') 表示される文言で。文言を変えたら直す前提 4 getByTestId('total-amount') 開発者とテストで決めた目印(data-testid) 5 locator('#form > div:nth-child(3) button') 画面の作り(入れ子や順番)に依存。少し変わるだけで壊れる 人が画面を見て探すのと同じ手がかりで探すと、テストが壊れにくい 1〜2で探せない画面は、作りの側(ラベルやボタン名)を直すのが先

getByRole の「役割」は、ボタン・入力欄・表・見出しといった、画面の読み上げソフトにも伝わる部品の種類です

ロケーターには厳密モードもあります。操作しようとした指定に要素が2つ以上当てはまると、「どちらか分からない」としてエラーにします。適当に1つ目を押して、たまたま通ってしまうことを防ぐ仕組みです。この記事のテストでも、二度これに助けられました(十一章)。

テストを書いて直すための道具も揃っています。

道具コマンド・場所何に使う
codegennpx playwright codegenブラウザで操作すると、そのとおりのテストコードを書き出す。ロケーターも上のおすすめ順で選ぶ
UI モードnpx playwright test --uiテストの一覧、実行、時間をさかのぼった画面の確認、保存のたびの再実行
トレースビューアnpx playwright show-trace操作の1つ1つについて、前後の画面・通信・コンソールを再生する。CI で落ちたテストの調査に
HTML レポートnpx playwright show-report結果の一覧と、失敗時のスクリーンショット・トレース
VS Code 拡張Playwright Test for VS Codeエディタからの実行・デバッグ、ロケーターの選び出し
七

実践:経費精算ページを題材に

ここからは、実際にテストを書いて動かします。題材は、多くの会社にある経費精算。説明のために、筆者が小さなデモアプリ(Node.js、外部ライブラリなし)を作りました。申請者がログインして明細を入力し、承認者が承認する、という流れです。

この記事のために作った経費精算のデモ画面。件名、日付・区分・内容・金額・通貨の明細4行(交通費 14,720円、宿泊費 12,800円、会議費 3,300円、クラウド利用料 20ドル=3,000円)、合計 33,820円、領収書の添付欄、「合計が3万円以上のときは領収書を添付してください」というエラー、申請するボタン、その下に申請の状況の一覧がある
テストの題材にした経費精算の画面。明細の合計、ドル建ての円換算、3万円以上の領収書チェックなど、確かめたいルールが詰まっています。テストを書く前に、筆者が作ったデモアプリです(筆者の検証環境で撮影。Playwright 1.64.1、2026年10月16日)

この画面には、次のようなルールがあります。テストを書く前に、まずルールを表にするところから始めます。

#ルール(受入条件)確かめ方
R1明細の金額を入れると、合計が自動で計算される2行入れて、合計の表示を確かめる
R2ドル建ての明細は、為替レートで円に換算される為替 API を固定値(1ドル=150円)に差し替えて確かめる
R3合計が3万円以上のときは、領収書がないと申請できない領収書なしでエラー、添付すると通ることを確かめる
R4未来の日付は申請できない明日の日付でエラーになることを確かめる
R5承認者が承認すると、申請者の一覧で「承認済み」になる申請者と承認者の二人を同時に動かす

R1〜R4 は1画面の中のルール、R5 は二人の利用者をまたぐ流れです。なお、R3 の境目(29,999円と30,000円)のような細かい組み合わせは、本来は単体テストや API のテストで網羅し、E2E では代表的なケースだけを見るのが効率的です。

八

テスト一式の組み立て

Playwright を入れるのは1行です。npm init playwright@latest を実行すると、設定ファイルとサンプルが作られます(今回は npm i -D @playwright/test で入れ、ファイルは手で書きました)。テスト一式は次のように組み立てます。

ファイルの役割分担

playwright.config.ts 全体の設定 経費精算アプリ テスト用モードで起動 webServer auth.setup.ts 初期化+ログイン 最初に1回 .auth/*.json ログイン状態を保存 ログイン済みで始める テスト本体 expense.spec.ts approval.spec.ts fixtures.ts モックと下準備 pages/expense-page.ts 画面の操作をまとめる 「何を確かめるか」はテスト本体に、「どう操作するか」は Page Object に ログインは最初に1回だけ。各テストは保存した状態から始めるので速い

まず設定です。テストの前にアプリを起動する(webServer)、ログイン用の下準備(setup)を先に走らせる、といったことをここに書きます。

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,                       // テストは並列に走らせる
  retries: process.env.CI ? 2 : 0,           // CI では失敗時に2回まで再実行
  reporter: [['list'], ['html', { open: 'never' }]],
  use: {
    baseURL: 'http://localhost:3000',
    locale: 'ja-JP',                         // 日付や数値の表示を日本の形式に
    timezoneId: 'Asia/Tokyo',
    trace: 'on-first-retry',                 // 再実行したときだけトレースを残す
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    { name: 'chromium', use: { ...devices['Desktop Chrome'] }, dependencies: ['setup'] },
    // Firefox や Safari(WebKit)も、同じ形で1行ずつ足せる
  ],
  webServer: {                               // テストの前にアプリを起動しておく
    command: 'node app/server.js',
    env: { APP_ENV: 'test' },
    url: 'http://localhost:3000/login',
    reuseExistingServer: !process.env.CI,
  },
});

次に、最初に1回だけ走る下準備です。データを初期化し、申請者と承認者でログインして、その状態(Cookie など)を storageState としてファイルに保存します。各テストはこのファイルを読み込むだけで、ログイン済みの状態から始められます。テストのたびにログイン画面を操作するより、ずっと速く、安定します。

// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

// テスト用アカウント(検証環境だけに存在する)
const accounts = [
  { id: 'tanaka', password: 'pass-tanaka', file: '.auth/applicant.json', home: '/' },        // 申請者
  { id: 'sato',   password: 'pass-sato',   file: '.auth/approver.json',  home: '/approve' }, // 承認者
];

setup('テストデータを初期化する', async ({ request }) => {
  const res = await request.post('/api/test/reset');   // テスト専用の API
  expect(res.ok()).toBeTruthy();
});

for (const a of accounts) {
  setup(`${a.id} でログインしておく`, async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('社員ID').fill(a.id);
    await page.getByLabel('パスワード').fill(a.password);
    await page.getByRole('button', { name: 'ログイン' }).click();
    await expect(page).toHaveURL(a.home);
    await page.context().storageState({ path: a.file });   // ログイン状態をファイルに保存
  });
}
九

Page Object とテスト本体

画面の操作のしかたは Page Object という1つのクラスにまとめます。「金額欄はラベル『金額』で探す」「申請ボタンは名前『申請する』で探す」といった知識をここに集めておけば、画面が変わったときに直す場所は1か所で済みます。

// tests/pages/expense-page.ts
import { type Page, type Locator } from '@playwright/test';

type Line = { date: string; kind: string; memo: string; amount: number; currency?: 'JPY' | 'USD' };

// 申請画面の「操作のしかた」をここに集める(Page Object)
export class ExpensePage {
  readonly lines: Locator;
  readonly total: Locator;
  readonly errors: Locator;
  readonly done: Locator;

  constructor(readonly page: Page) {
    this.lines  = page.getByTestId('expense-line');
    this.total  = page.getByTestId('total-amount');
    this.errors = page.getByRole('alert');
    this.done   = page.getByRole('status', { name: '申請結果' });
  }

  async goto() {
    await this.page.goto('/');
  }

  async fillLine(i: number, l: Line) {
    if (await this.lines.count() <= i)
      await this.page.getByRole('button', { name: '明細を追加' }).click();
    const row = this.lines.nth(i);
    await row.getByLabel('日付').fill(l.date);
    await row.getByLabel('区分').selectOption(l.kind);
    await row.getByLabel('内容').fill(l.memo);
    await row.getByLabel('金額').fill(String(l.amount));
    if (l.currency) await row.getByLabel('通貨').selectOption(l.currency);
  }

  async attachReceipt(name = 'receipt.pdf') {
    await this.page.getByLabel(/領収書/).setInputFiles({
      name, mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4 dummy'),
    });
  }

  async submit() {
    await this.page.getByRole('button', { name: '申請する' }).click();
  }
}

明細の各行には data-testid="expense-line" という目印を付けました。行そのものには利用者に見える名前がないためです。見える手がかりで探せないところだけ、開発者と約束した目印を使う。これがロケーターのおすすめ順の使い方です。

次はフィクスチャ(テストの下準備)です。ここで外部の為替 API をモックに差し替えます。本物の為替レートは毎日変わるので、そのままでは「3,000円になるはず」と書けません。page.route() を使うと、ブラウザが出す通信を途中で受け止めて、決まった答えを返せます。

// tests/fixtures.ts
import { test as base, expect } from '@playwright/test';
import { ExpensePage } from './pages/expense-page';

export const test = base.extend<{ expensePage: ExpensePage }>({
  expensePage: async ({ page }, use) => {
    // 外部の為替 API は、いつも同じ値を返すモックに差し替える
    await page.route('https://api.rates.example/**', route =>
      route.fulfill({ json: { base: 'USD', rates: { JPY: 150 } } }));
    const expensePage = new ExpensePage(page);
    await expensePage.goto();
    await use(expensePage);
  },
});
export { expect };

そして、テスト本体です。ルール表の R1〜R4 が、そのまま4本のテストになっています。

// tests/expense.spec.ts
import { test, expect } from './fixtures';

test.use({ storageState: '.auth/applicant.json' });   // 申請者としてログイン済みで始める

test('明細の合計が自動で計算される', async ({ expensePage }) => {
  await expensePage.fillLine(0, { date: '2026-10-14', kind: '交通費', memo: '新幹線 東京→新大阪', amount: 14720 });
  await expensePage.fillLine(1, { date: '2026-10-14', kind: '会議費', memo: '打ち合わせ(3名)', amount: 3300 });
  await expect(expensePage.total).toHaveText('18,020円');
});

test('ドル建ての明細は為替レートで円に換算される', async ({ expensePage }) => {
  await expensePage.fillLine(0, { date: '2026-10-14', kind: 'その他', memo: 'クラウド利用料', amount: 20, currency: 'USD' });
  await expect(expensePage.total).toHaveText('3,000円');            // 20ドル × 150円
});

test('3万円以上は領収書がないと申請できない', async ({ expensePage }) => {
  await expensePage.fillLine(0, { date: '2026-10-14', kind: '宿泊費', memo: 'ホテル 2泊', amount: 32000 });
  await expensePage.submit();
  await expect(expensePage.errors).toContainText('領収書を添付してください');

  await expensePage.attachReceipt();
  await expensePage.submit();
  await expect(expensePage.done).toContainText('申請しました');
});

test('未来の日付は申請できない', async ({ expensePage }) => {
  const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
  await expensePage.fillLine(0, { date: tomorrow, kind: '交通費', memo: 'タクシー', amount: 2400 });
  await expensePage.submit();
  await expect(expensePage.errors).toContainText('未来の日付は申請できません');
});

2本目のテストを見てください。通貨を USD に切り替えると、画面は為替 API に問い合わせてから合計を書き換えます。でもテストには「API の応答を待つ」という行がありません。toHaveText('3,000円') が、合計が 3,000円になるまで自動で確かめ続けるからです。

十

申請から承認まで:二人の登場人物

R5 は、申請者(田中さん)と承認者(佐藤さん)の二人が登場します。ここでブラウザコンテキストが活きます。一つのブラウザの中に二人分のコンテキストを作り、それぞれのログイン状態で画面を開きます。

一つのテストで、二人が交互に動く

田中さん コンテキスト A 経費精算アプリ テスト用モード 佐藤さん コンテキスト B 1 申請する 2 申請しました(No.0003) 3 No.0003 を承認 4 承認済み 5 申請の一覧を開く 6 No.0003:承認済み 番号(No.0003)は画面から読み取り、その番号で行を探す 件名や「1行目」で探すと、並列で走るほかのテストのデータとぶつかる
// tests/approval.spec.ts
import { test, expect } from '@playwright/test';
import { ExpensePage } from './pages/expense-page';

test('申請して承認されると「承認済み」になる', async ({ browser }) => {
  // 1つのブラウザの中に、申請者と承認者の2つの「コンテキスト」を作る
  const applicant = await browser.newContext({ storageState: '.auth/applicant.json' });
  const approver  = await browser.newContext({ storageState: '.auth/approver.json' });
  let no = '';                                              // 例: "No.0003"

  await test.step('田中さんが申請する', async () => {
    const p = new ExpensePage(await applicant.newPage());
    await p.goto();
    await p.page.getByLabel('件名').fill('大阪出張');
    await p.fillLine(0, { date: '2026-10-14', kind: '交通費', memo: '新幹線 往復', amount: 29440 });
    await p.submit();
    await expect(p.done).toContainText('申請しました');
    no = (await p.done.textContent())!.match(/No\.\d{4}/)![0];   // 採番された番号で後から探す
  });

  await test.step('佐藤さんが承認する', async () => {
    const page = await approver.newPage();
    await page.goto('/approve');
    const row = page.getByRole('row').filter({ hasText: no });
    await row.getByRole('button', { name: '承認' }).click();
    await expect(row.getByText('承認済み')).toBeVisible();
  });

  await test.step('田中さんの画面でも「承認済み」になっている', async () => {
    const page = await applicant.newPage();
    await page.goto('/');
    const row = page.getByRole('table', { name: '申請一覧' }).getByRole('row').filter({ hasText: no });
    await expect(row).toContainText('承認済み');
  });

  await applicant.close();
  await approver.close();
});

test.step() で区切ると、レポートやトレースに「田中さんが申請する」といった見出しが付き、どこで失敗したかがひと目で分かります。

十一

動かした結果と、つまずいたところ

実行は npx playwright test だけです。アプリの起動、ログインの下準備、8件のテストまでが自動で流れます。

$ npx playwright test
Running 8 tests using 1 worker

  ✓  1 [setup] › tests/auth.setup.ts:9:6 › テストデータを初期化する (44ms)
  ✓  2 [setup] › tests/auth.setup.ts:15:8 › tanaka でログインしておく (606ms)
  ✓  3 [setup] › tests/auth.setup.ts:15:8 › sato でログインしておく (472ms)
  ✓  4 [chromium] › tests/approval.spec.ts:4:5 › 申請して承認されると「承認済み」になる (1.2s)
  ✓  5 [chromium] › tests/expense.spec.ts:5:5 › 明細の合計が自動で計算される (600ms)
  ✓  6 [chromium] › tests/expense.spec.ts:11:5 › ドル建ての明細は為替レートで円に換算される (435ms)
  ✓  7 [chromium] › tests/expense.spec.ts:16:5 › 3万円以上は領収書がないと申請できない (536ms)
  ✓  8 [chromium] › tests/expense.spec.ts:26:5 › 未来の日付は申請できない (461ms)

  8 passed (6.3s)
Playwright の HTML レポート。auth.setup.ts の3件(テストデータの初期化、tanaka と sato のログイン)、approval.spec.ts の1件、expense.spec.ts の4件がすべて緑のチェックで並び、Passed 8、Failed 0、合計 8.1 秒と表示されている
テストを流したあとの HTML レポート。8件すべて成功。それぞれの「View Trace」から、次のトレースビューアに飛べます(筆者の検証環境で撮影。Playwright 1.64.1、2026年10月16日)
Playwright のトレースビューア。上に時間軸と画面のサムネイル、左に「田中さんが申請する」「佐藤さんが承認する」などの手順、中央に承認待ち一覧の画面のスナップショットがあり、承認ボタンを押した位置に赤い点が付いている。下にはロケーター getByRole row filter No.0001 getByRole button 承認 が表示されている
申請から承認までのテストのトレース。左で「Click」を選ぶと、その瞬間の画面と、押した場所(赤い点)、使ったロケーターが見られます。失敗したテストでも同じように、どこで何が起きたかを後から再生できます(筆者の検証環境で撮影。Playwright 1.64.1、2026年10月16日)

ただ、最初から全部うまくいったわけではありません。厳密モードに二度止められました。

  • 1回目:「申請しました」の表示が2つ見つかった ── 完了メッセージを getByRole('status') で探したところ、合計金額の表示に使った <output> 要素も、HTML の決まりで同じ「status」の役割を持っていた。メッセージ側に aria-label="申請結果" と名前を付け、getByRole('status', { name: '申請結果' }) で区別した
  • 2回目:並列で走らせたら、同じ件名の行が2つ ── 最初は件名に一意な文字列を付けて行を探していたが、同じテストを並列で5回ずつ繰り返す負荷試験(--workers 4 --repeat-each 5)をかけると、その文字列が繰り返しのあいだで同じになり、行が2つ見つかった。アプリが採番した番号(No.0003 など)を画面から読み取り、その番号で探す方式に変えて、28件すべて成功

どちらも、厳密モードがなければ「たまたま1つ目が当たって通る」テストになっていたはずです。偶然に頼るテストは、いつか理由もなく落ちます(いわゆる「フレーキーなテスト」)。

もう一つ、わざと失敗させてみました。画面のボタン名を「申請する」から「提出する」に変えると、テストは次のように落ちます。

  1) [chromium] › tests/expense.spec.ts:26:5 › 未来の日付は申請できない ────────────────────────────────────────
    Test timeout of 30000ms exceeded.
    Error: locator.click: Test timeout of 30000ms exceeded.
    Call log:
      - waiting for getByRole('button', { name: '申請する' })
       at pages/expense-page.ts:41
      39 |
      40 |   async submit() {
    > 41 |     await this.page.getByRole('button', { name: '申請する' }).click();
         |                                                           ^
      42 |   }
      43 | }
      44 |

ここで大事なのは、この失敗が「テストが古い」のか「画面の不具合」なのかは、Playwright には分からないということです。ボタン名の変更が仕様どおりならテストを直し、意図しない変更なら画面を戻す。この判断は、後で見る AI の時代にも人の仕事として残ります。

十二

設計段階から準備しておくこと

ここまでのテストが短く、安定して書けたのは、実はアプリの側にテストしやすい作りを最初から入れておいたからです。Playwright の力を引き出せるかどうかは、テストを書く前、つまり設計と開発の段階でほぼ決まります。

テストのしやすさは「前倒し」で作る

1 要件・設計 ・受入条件を「確かめられる文」で書く ・テスト観点を一覧にする ・テスト環境と専用アカウントを決める 2 画面設計 ・入力欄にラベル、ボタンに明確な名前 ・エラーの出し方(role=alert)を統一 ・test ID の命名規則を決める 3 開発 ・データを初期化・投入する口を用意 ・外部サービスを差し替え可能に ・日時・採番をテストから制御できる 4 CI・運用 ・プルリクエストごとに自動で実行 ・失敗時はトレースとレポートを残す ・不安定なテストは放置せず直す テストのしやすさは、あとから足すより最初から設計に入れるほうが安い 1・2 は企画や設計の担当者も関わる仕事。テストは開発の最後の工程ではない
準備しておくことなぜ経費精算での例
受入条件を、確かめられる文で書く「使いやすいこと」はテストにできない。「3万円以上は領収書必須」ならできる七章のルール表 R1〜R5
入力欄にラベル、ボタンに名前getByRole・getByLabel で探せる。画面の読み上げにも効く(アクセシビリティ)「金額」「申請する」
エラーと完了の出し方を統一どの画面でも同じロケーターで確かめられるエラーは role="alert"、完了は role="status"
test ID の命名規則見える手がかりがない部品の目印。名前がバラバラだと探せないexpense-line、total-amount(kebab-case)
テスト用のデータ初期化・投入の口毎回同じ状態から始められる。画面からデータを作るより速いPOST /api/test/reset(テスト用モードのときだけ有効)
外部サービスを差し替えられる作り為替・決済・メールなど、テストのたびに結果が変わるものを固定できる為替 API を page.route() でモック
テスト用アカウントと権限役割ごとの操作を確かめるため。本番のアカウントは使わない申請者 tanaka、承認者 sato
日時・採番を制御できる「今日」や連番に依存するテストを安定させる採番した No. を画面に出す。Playwright の Clock API で時刻を固定することもできる

たとえば、データ初期化の口はこれだけです。テスト用モードで起動したときだけ有効にし、本番では存在しないようにしておきます。

// app/server.js(抜粋)
// テスト専用:データを初期状態に戻す(本番では無効)
if (TEST_MODE && req.method === 'POST' && url.pathname === '/api/test/reset') {
  db = { seq: 0, expenses: [] };
  return send(res, 200, { ok: true });
}
POINT

テストのしやすさは、企画・設計・デザインの担当者も関わる品質です。画面設計書に「各入力欄のラベル」「エラーの出し方」「test ID」の欄を一つ足すだけで、後のテストの手間は大きく変わります。そしてこの準備は、AI にテストを書かせるときにも、そのまま効いてきます(十四章)。

十三

開発段階と CI での準備

テストは、書いただけでは価値が半分です。変更のたびに自動で走るようにして初めて、「いつの間にか壊れていた」を防げます。

プルリクエストごとに、同じ確認を

プッシュ PR を作る 準備 アプリ起動 テスト実行 shard 1/2 shard 2/2 まとめる レポート マージ可 全件成功 必須チェックに 失敗 → トレースで原因を見て直す 人の手を介さず、変更のたびに同じ確認が走る。これが自動テストの本当の価値 ブラウザは使うものだけ入れ(chromium など)、テストは分割(--shard)して時間を短く

GitHub Actions なら、たとえば次のように書けます(公式ドキュメントの例を、この記事の構成に合わせて短くしたもの)。

# .github/workflows/e2e.yml
name: e2e
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npx playwright install chromium --with-deps   # 使うブラウザだけ入れる
      - run: npx playwright test
        env:
          CI: true                                  # 再実行2回・トレースが有効になる
      - uses: actions/upload-artifact@v7
        if: ${{ !cancelled() }}                     # 失敗しても結果は残す
        with:
          name: playwright-report
          path: playwright-report/

もう一つの準備が、書き方のルールを機械でチェックすることです。ESLint のプラグイン eslint-plugin-playwright と TypeScript の規則を使うと、「CSS セレクタで探している」「2秒待っている」「await を付け忘れている」といった、壊れやすいテストの書き方を、実行する前に見つけられます。

// eslint.config.mjs
import tseslint from 'typescript-eslint';
import playwright from 'eslint-plugin-playwright';

export default tseslint.config({
  files: ['tests/**/*.ts'],
  extends: [playwright.configs['flat/recommended']],
  languageOptions: { parser: tseslint.parser, parserOptions: { projectService: true } },
  plugins: { '@typescript-eslint': tseslint.plugin },
  rules: {
    '@typescript-eslint/no-floating-promises': 'error',   // await の付け忘れ
    'playwright/no-raw-locators': 'error',                // CSS・XPath で探さない
    'playwright/no-wait-for-timeout': 'error',            // 「とりあえず2秒待つ」を禁止
    'playwright/prefer-web-first-assertions': 'error',
  },
});

次のような「悪い例」を書いて、実際にチェックにかけてみました。

// AI が書いてきた「悪い例」
import { test, expect } from '@playwright/test';

test('合計が出る(悪い例)', async ({ page }) => {
  await page.goto('/');
  await page.locator('#lines tr:nth-child(1) td:nth-child(4) input').fill('14720');
  page.locator('button.primary').click();
  await page.waitForTimeout(2000);
  expect(await page.locator('#total').isVisible()).toBe(true);
});
$ npx eslint tests
tests/bad/bad.spec.ts
  5:9   error  Usage of raw locator detected. Use methods like .getByRole() or .getByText() instead of raw locators  playwright/no-raw-locators
  6:3   error  Promises must be awaited, end with a call to .catch, ... or be explicitly marked as ignored with the `void` operator  @typescript-eslint/no-floating-promises
  6:3   error  Usage of raw locator detected. ...  playwright/no-raw-locators
  7:9   error  Unexpected use of page.waitForTimeout()  playwright/no-wait-for-timeout
  8:3   error  Replace isVisible() with toBeVisible()  playwright/prefer-web-first-assertions
  8:16  error  Usage of raw locator detected. ...  playwright/no-raw-locators

✖ 6 problems (6 errors, 0 warnings)

6か所すべてが止められました。この仕組みは、人が書くテストにも、AI が書くテストにも同じように効きます。

  • 不安定なテストは放置しない ── CI で「再実行したら通った」テストは、レポートに Flaky として出る。原因を調べて直すか、直るまで隔離する
  • ブラウザは必要なものだけ ── まずは Chromium だけで始め、Safari の利用者が多いなら WebKit を足す、と段階的に
  • 時間が延びてきたら分割 ── --shard=1/2 のように分けて複数台で並列に流す
十四

AI と Playwright:すでにある実例

Playwright は、2025年から「AI にブラウザを使わせるための道具」としても使われるようになりました。鍵になるのが、ページを文字にしたアクセシビリティ・スナップショットです。

AI はページをどう見ているか

/login ログイン 社員ID e8 パスワード e10 ログイン e11 AI に渡る「ページの文字版」 - heading "ログイン" - textbox "社員ID" [ref=e8] - textbox "パスワード" [ref=e10] - button "ログイン" [ref=e11] AI が送る命令 $ playwright-cli fill e8 tanaka 実行したコードが返る await page.getByRole('textbox', { name: '社員ID' }).fill('tanaka'); AI は画像ではなく「役割・名前・番号」の一覧でページを読む ラベルやボタン名がきちんと付いた画面ほど、AI は迷わず、良いロケーターを書く ※ 筆者がデモアプリで playwright-cli を動かした出力を簡略化

スクリーンショットを画像として読むのではなく、役割・名前・番号の一覧で読むので、速く、迷いにくい

この仕組みを使った実例を、執筆時点(2026年10月)のものから挙げます。

実例時期何ができる
Playwright MCP2025年3月〜MCP(AI と道具をつなぐ共通の仕組み)のサーバー。VS Code、Cursor、Claude などの AI から、ブラウザを開く・入力する・クリックする・スナップショットを取る、といった操作ができる。README は「スクリーンショットや画像を読むモデルが要らない」とうたう
GitHub Copilot coding agent2025年7月〜GitHub の AI エージェントが、Playwright MCP によるブラウザを最初から使える(既定で有効)。作業中に Web ページを開いて確かめられる
Copy prompt とエラー文脈2025年3月〜(1.51)HTML レポート・トレースビューア・UI モードのエラーに、AI に渡すプロンプトをコピーする「Copy prompt」ボタンが付いた。いまは失敗したテストごとに、エラー・ページのスナップショット・該当コードをまとめた error-context.md も書き出される(1.64.1 で確認)
テストエージェント2025年10月〜(1.56)計画(planner)・生成(generator)・修復(healer)の3つの AI エージェントの定義を、npx playwright init-agents でプロジェクトに入れる。執筆時点の対応先は Claude Code、Codex、GitHub Copilot、OpenCode、VS Code
playwright-cli とスキル2026年〜(1.59 で強化、1.62 で同梱)AI のコーディングエージェント向けのコマンド。MCP より受け渡す情報が少なく、トークン(AI の処理量)を節約できると README は説明している
WebMCP 対応2026年10月(1.64)ページ自身が「AI 向けの操作」を登録する実験的なブラウザの仕組み(WebMCP)を、テストから一覧・呼び出しできる

試しに、デモアプリのログイン画面を playwright-cli で操作してみました。AI が「番号 e8 に tanaka と入力」と命令すると、Playwright はそれを getByRole を使ったコードとして実行し、そのコードを返してきます。AI はこのコードをそのままテストに使えます。

$ playwright-cli open http://localhost:3000/login
$ playwright-cli fill e8 tanaka
### Ran Playwright code
await page.getByRole('textbox', { name: '社員ID' }).fill('tanaka');
$ playwright-cli fill e10 pass-tanaka
### Ran Playwright code
await page.getByRole('textbox', { name: 'パスワード' }).fill('pass-tanaka');
$ playwright-cli click e11
### Ran Playwright code
await page.getByRole('button', { name: 'ログイン' }).click();
### Page
- Page URL: http://localhost:3000/
- Page Title: 経費精算の申請

ここで十二章の準備が効いてきます。入力欄にラベルがあったから、返ってきたコードは getByRole('textbox', { name: '社員ID' }) という読みやすく壊れにくい形になりました。ラベルのない画面なら、AI も壊れやすい探し方をするしかありません。人にとって分かりやすい画面は、AI にとっても分かりやすいのです。

テストエージェントも入れてみました(AI の実行まではしていません。生成されたファイルの確認まで)。

$ npx playwright init-agents --loop=claude
 🎭 Using project "chromium" as a primary project
 📝 specs/README.md - directory for test plans
 🌱 tests/seed.spec.ts - default environment seed file
 🤖 .claude/agents/playwright-test-generator.md - agent definition
 🤖 .claude/agents/playwright-test-healer.md - agent definition
 🤖 .claude/agents/playwright-test-planner.md - agent definition
 🔧 .mcp.json - mcp configuration
 ✅ Done.

テストエージェントの分担

seed.spec.ts 下準備 プランナー アプリを探索 specs/*.md テスト計画 ジェネレーター コードにする tests/*.spec.ts テストコード ヒーラー 直す 失敗したら 人が確認 計画(Markdown)を人が読めるのがミソ。AI 任せにせず、計画の段階で口を出す

ヒーラーの定義には「機能そのものが壊れていると判断したら、テストをスキップすることがある」とあります。直した中身は必ず人が見ます

十一章でわざと失敗させたテストについて、Playwright が書き出した error-context.md の中身も見てみます。AI への指示文、エラー、そして失敗した瞬間のページのスナップショットが入っていて、「申請する」ボタンがなく「提出する」ボタンがあることが、AI にも読み取れる形になっています。

# error-context.md(抜粋・自動で書き出される)
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Error details
Error: locator.click: Test timeout of 30000ms exceeded.
  - waiting for getByRole('button', { name: '申請する' })

# Page snapshot
      - status [ref=e39]: 2,400円
      ...
      - button "提出する" [ref=e42] [cursor=pointer]
十五

AI にルールを守らせる

AI にテストを書かせると、速い反面、書き方がばらばらになりがちです。CSS セレクタで探したり、2秒待ったり、テスト同士がデータを共有したり。そこで、プロジェクトのルールを文章にして AI に渡し、さらに機械のチェックで守らせる、という二段構えにします。

ルール・テスト・人の三段構え

AI に渡すルール AGENTS.md など プロンプトのひな形 ESLint の決まり 手本のテストコード Page Object の場所 実装とテストを書く AI エージェント Playwright で実行 CLI・MCP・test 失敗の手がかり エラー文脈・トレース 失敗 直す 1 2 3 全件成功 4 機械のチェック → 人のレビュー lint と全テスト成功を必須に。期待値を変えた差分は人が判断する ルールで書き方をそろえ、テストで動作を確かめ、人が正しさを見る AI は速いが、テストを通すために期待値を書き換えることもある。そこを人が止める

まずルールファイルです。AI のコーディングエージェントの多くは、リポジトリに置いた AGENTS.md(Claude Code なら CLAUDE.md、GitHub Copilot なら .github/copilot-instructions.md など)を読んでから作業します(AGENTS.md についてはAGENTS.md 覚書で詳しく書きました)。この記事のテストに合わせると、たとえば次のような節を足します。

## E2E テスト(Playwright)のルール

- テストは tests/ に置く。画面の操作は tests/pages/ の Page Object に書き、
  テスト本体には「何を確かめるか」だけを書く
- 要素は getByRole → getByLabel → getByText → getByTestId の順で探す。
  CSS セレクタ・XPath・nth-child は使わない
- 待つときは expect(...).toBeVisible() などの Web ファーストのアサーションを使う。
  waitForTimeout() は使わない
- テストは互いに独立させる。ほかのテストのデータや実行順に頼らない。
  行を探すときは、アプリが採番した番号を使う
- ログインは auth.setup.ts で保存した storageState を使う
- 外部サービス(為替 API など)は page.route() でモックする
- 画面を作るときは、入力欄にラベル、ボタンに名前を付ける。
  目印が必要なら data-testid を足す(kebab-case)
- 失敗したテストを直すとき、expect の期待値を変えない。仕様の変更か不具合か
  判断できなければ、手を止めて理由を報告する
- 終わったら npx eslint tests と npx playwright test を実行し、結果を報告に書く

次に、テストを頼むときのプロンプトの「ひな形」を用意しておきます。ポイントは、手本のファイルを名指しすること、いきなりコードを書かせず、先に計画を出させること、実行結果を貼らせることです。

# AI へのお願い(ひな形)
経費精算の「差戻し」の E2E テストを追加してください。

- 仕様:承認者が差し戻すと、申請者の一覧で状態が「差戻し」になる
- 手本:tests/approval.spec.ts と同じ書き方。AGENTS.md の
  「E2E テスト(Playwright)のルール」に従うこと
- まず specs/reject.md に手順と期待結果を書き、私の確認を待つ
- 確認後に実装し、npx eslint tests と npx playwright test の結果を貼る
- 画面にラベルや名前が足りなければ、テストで回避せず、画面側の修正案を出す

そして、ルールが守られているかを機械で確かめるのが十三章の ESLint です。文章のルールは AI が読み飛ばすことがありますが、CI の lint は読み飛ばせません。「ルールファイルで方向を示し、lint とテストで外れたら止める」。人のチームでも AI でも、同じやり方が効きます。

NOTE

AI にブラウザを操作させるときは、検証環境とテスト用アカウントだけを使わせてください。本番のデータや個人のアカウントに触れさせないこと。また、Web ページに書かれた文章が AI への「指示」として紛れ込む攻撃(プロンプトインジェクション)も知られています。社外のサイトを開かせる使い方では、AI に許す操作を絞り、結果は人が確かめるようにしましょう。

十六

これからの展望

最後に、これからの方向を考えます。ここからは、すでにある機能を手がかりにした筆者の見立てです。

  • 仕様からテストへ、が近づく ── テストエージェントは、Markdown の計画からテストを作る。受入条件をきちんと書いておけば、そこからテストの下書きまでは AI が作る、という流れが普通になっていくと考える。そのぶん、受入条件の書き方の重みが増す
  • 「直すテスト」から「見張るテスト」へ ── 画面の変更にテストが追いつく修復は、すでにヒーラーが行っている。ただし、何でも直してしまうとテストが「今の画面」を追いかけるだけになり、不具合を見逃す。期待値の変更には人の承認を必須にする運用が、今後ますます大事になる
  • アクセシビリティが品質の中心へ ── AI も Playwright も、ラベルや役割で画面を読む。画面の読み上げに配慮した作りが、そのまま「テストしやすさ」「AI の扱いやすさ」になる。三つの目的が一致するのは、開発の現場にとって追い風
  • ページが AI に「操作の口」を差し出す ── 1.64 で入った WebMCP 対応のように、ページ自身が AI 向けの操作を宣言する試みが始まっている。まだ実験段階だが、画面をなぞる操作と、決められた口を呼ぶ操作が併用されていくかもしれない
  • 人の仕事は「何が正しいか」を決めることに ── テストを書く・流す・直す手間は、AI と道具がどんどん引き受ける。残るのは、どんな振る舞いが正しいかを決め、AI の出した変更を見極める仕事だ

結 — 一枚まとめ

  1. Playwright は、2020年に Microsoft が公開したブラウザ自動化・テストの道具。Puppeteer を作ったメンバーが、Chromium・Firefox・WebKit を一つの API で動かすために作った。
  2. 仕組みの柱は、ブラウザとのつなぎっぱなしの通信、テストごとにまっさらなブラウザコンテキスト、自動待機と Web ファーストのアサーション、利用者に見える手がかりで探すロケーター。
  3. 経費精算ページでは、ルール表 → Page Object → テスト本体 の順に書き、ログインの保存、為替 API のモック、二人のコンテキストを使って、8件のテストが約6秒で通った。厳密モードが、偶然に頼るテストを二度防いだ。
  4. テストのしやすさは設計段階で作る。確かめられる受入条件、ラベルと名前、エラーの出し方、test ID、データ初期化の口、外部サービスの差し替え。CI と lint で、変更のたびに守る。
  5. AI とは、ルールファイルで方向を示し、lint とテストで止め、期待値の判断は人が持つ。MCP・CLI・テストエージェントはすでに使える。分かりやすい画面は、人にも AI にも分かりやすい。

参考

テストは2026年10月16日に、Playwright 1.64.1(@playwright/test)で、筆者が作った経費精算のデモアプリ(Node.js 22)に対して実際に動かしたものです。検証はクラウドのサンドボックス上の Chromium のみで行い、Firefox と WebKit では動かしていません。CI の設定例は公式ドキュメントをもとにした例で、この記事のためには実行していません。テストエージェントは定義ファイルの生成までを確認しました。ダウンロード数は npm の公開統計(2026年10月6〜12日)、各機能の時期は Playwright のリリースノートと npm の公開日によります。「これからの展望」は筆者の見立てです。

開発ガイド No.7 — Playwright 入門と実践 / 2026.10

← 記事一覧へ