Dev Guide / 2026 ・ No.07
Playwright 入門と実践
生まれた背景と仕組み、経費精算ページのテスト、そして AI との付き合い方
公開
テストを書けば、ブラウザが勝手に確かめてくれる
画面を開き、入力し、ボタンを押し、表示を目で確かめる。リリースのたびに人が繰り返してきたこの確認を、
コードにして何度でも再生できるようにするのが 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 は本物に近いぶん時間がかかります。細かいルールの組み合わせは単体テストに任せ、E2E では「大事な流れが最後までつながるか」を見ます
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 に至る道と、その後
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 本体(サーバー)、そしてブラウザです。
命令はどう届くのか
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 も保存領域も別々の「シークレットウィンドウ」をいくつでも作れます。公式ドキュメントの言葉では「速く、安く作れる」。
一つのブラウザに、別々の「人」を同居させる
経費精算なら、申請者と承認者が同時にログインしている状況を、一つのテストの中で再現できます(十章)
Playwright Test は、テストごとに新しいコンテキストを用意します。前のテストのログイン状態や入力内容を引きずらないので、テストを並列に走らせても、順番を入れ替えても結果が変わりません。「前のテストが落ちたせいで、次のテストも全部落ちる」という連鎖を防げます。
仕組み③:自動で待つ
画面の自動テストでいちばん多いトラブルは「待ち」です。ボタンがまだ出ていないのに押そうとして失敗する。それを避けるために「2秒待つ」と書くと、速い日はムダに待ち、遅い日はやはり失敗します。Playwright はこの問題を自動待機(オートウェイト)で解決しました。
「何秒待つか」を書かない
確かめる項目は操作によって変わります(入力なら「編集できるか」も見る)。条件がそろわないまま制限時間を過ぎると、TimeoutError で失敗します
確認(アサーション)も同じ考え方です。expect(合計).toHaveText('3,000円') と書くと、Playwright は表示が「3,000円」になるまで繰り返し確かめます。これを Web ファーストのアサーションと呼びます。既定の制限時間は、確認1つにつき5秒、テスト1本で30秒(この記事のテストでも、失敗時の表示でこの値を確認しました)。
仕組み④:要素の探し方と道具
画面のどの部品を操作するかを指定するのがロケーターです。Playwright の公式ガイドは、利用者に見える手がかりで探すことを勧めています。
ロケーターのおすすめ順
getByRole の「役割」は、ボタン・入力欄・表・見出しといった、画面の読み上げソフトにも伝わる部品の種類です
ロケーターには厳密モードもあります。操作しようとした指定に要素が2つ以上当てはまると、「どちらか分からない」としてエラーにします。適当に1つ目を押して、たまたま通ってしまうことを防ぐ仕組みです。この記事のテストでも、二度これに助けられました(十一章)。
テストを書いて直すための道具も揃っています。
| 道具 | コマンド・場所 | 何に使う |
|---|---|---|
| codegen | npx 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、外部ライブラリなし)を作りました。申請者がログインして明細を入力し、承認者が承認する、という流れです。
この画面には、次のようなルールがあります。テストを書く前に、まずルールを表にするところから始めます。
| # | ルール(受入条件) | 確かめ方 |
|---|---|---|
| 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 で入れ、ファイルは手で書きました)。テスト一式は次のように組み立てます。
ファイルの役割分担
まず設定です。テストの前にアプリを起動する(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 は、申請者(田中さん)と承認者(佐藤さん)の二人が登場します。ここでブラウザコンテキストが活きます。一つのブラウザの中に二人分のコンテキストを作り、それぞれのログイン状態で画面を開きます。
一つのテストで、二人が交互に動く
// 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)
ただ、最初から全部うまくいったわけではありません。厳密モードに二度止められました。
- 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 の力を引き出せるかどうかは、テストを書く前、つまり設計と開発の段階でほぼ決まります。
テストのしやすさは「前倒し」で作る
| 準備しておくこと | なぜ | 経費精算での例 |
|---|---|---|
| 受入条件を、確かめられる文で書く | 「使いやすいこと」はテストにできない。「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 });
}
テストのしやすさは、企画・設計・デザインの担当者も関わる品質です。画面設計書に「各入力欄のラベル」「エラーの出し方」「test ID」の欄を一つ足すだけで、後のテストの手間は大きく変わります。そしてこの準備は、AI にテストを書かせるときにも、そのまま効いてきます(十四章)。
開発段階と CI での準備
テストは、書いただけでは価値が半分です。変更のたびに自動で走るようにして初めて、「いつの間にか壊れていた」を防げます。
プルリクエストごとに、同じ確認を
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 はページをどう見ているか
スクリーンショットを画像として読むのではなく、役割・名前・番号の一覧で読むので、速く、迷いにくい
この仕組みを使った実例を、執筆時点(2026年10月)のものから挙げます。
| 実例 | 時期 | 何ができる |
|---|---|---|
| Playwright MCP | 2025年3月〜 | MCP(AI と道具をつなぐ共通の仕組み)のサーバー。VS Code、Cursor、Claude などの AI から、ブラウザを開く・入力する・クリックする・スナップショットを取る、といった操作ができる。README は「スクリーンショットや画像を読むモデルが要らない」とうたう |
| GitHub Copilot coding agent | 2025年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.
テストエージェントの分担
ヒーラーの定義には「機能そのものが壊れていると判断したら、テストをスキップすることがある」とあります。直した中身は必ず人が見ます
十一章でわざと失敗させたテストについて、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(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 でも、同じやり方が効きます。
AI にブラウザを操作させるときは、検証環境とテスト用アカウントだけを使わせてください。本番のデータや個人のアカウントに触れさせないこと。また、Web ページに書かれた文章が AI への「指示」として紛れ込む攻撃(プロンプトインジェクション)も知られています。社外のサイトを開かせる使い方では、AI に許す操作を絞り、結果は人が確かめるようにしましょう。
これからの展望
最後に、これからの方向を考えます。ここからは、すでにある機能を手がかりにした筆者の見立てです。
- 仕様からテストへ、が近づく ── テストエージェントは、Markdown の計画からテストを作る。受入条件をきちんと書いておけば、そこからテストの下書きまでは AI が作る、という流れが普通になっていくと考える。そのぶん、受入条件の書き方の重みが増す
- 「直すテスト」から「見張るテスト」へ ── 画面の変更にテストが追いつく修復は、すでにヒーラーが行っている。ただし、何でも直してしまうとテストが「今の画面」を追いかけるだけになり、不具合を見逃す。期待値の変更には人の承認を必須にする運用が、今後ますます大事になる
- アクセシビリティが品質の中心へ ── AI も Playwright も、ラベルや役割で画面を読む。画面の読み上げに配慮した作りが、そのまま「テストしやすさ」「AI の扱いやすさ」になる。三つの目的が一致するのは、開発の現場にとって追い風
- ページが AI に「操作の口」を差し出す ── 1.64 で入った WebMCP 対応のように、ページ自身が AI 向けの操作を宣言する試みが始まっている。まだ実験段階だが、画面をなぞる操作と、決められた口を呼ぶ操作が併用されていくかもしれない
- 人の仕事は「何が正しいか」を決めることに ── テストを書く・流す・直す手間は、AI と道具がどんどん引き受ける。残るのは、どんな振る舞いが正しいかを決め、AI の出した変更を見極める仕事だ
結 — 一枚まとめ
- Playwright は、2020年に Microsoft が公開したブラウザ自動化・テストの道具。Puppeteer を作ったメンバーが、Chromium・Firefox・WebKit を一つの API で動かすために作った。
- 仕組みの柱は、ブラウザとのつなぎっぱなしの通信、テストごとにまっさらなブラウザコンテキスト、自動待機と Web ファーストのアサーション、利用者に見える手がかりで探すロケーター。
- 経費精算ページでは、ルール表 → Page Object → テスト本体 の順に書き、ログインの保存、為替 API のモック、二人のコンテキストを使って、8件のテストが約6秒で通った。厳密モードが、偶然に頼るテストを二度防いだ。
- テストのしやすさは設計段階で作る。確かめられる受入条件、ラベルと名前、エラーの出し方、test ID、データ初期化の口、外部サービスの差し替え。CI と lint で、変更のたびに守る。
- AI とは、ルールファイルで方向を示し、lint とテストで止め、期待値の判断は人が持つ。MCP・CLI・テストエージェントはすでに使える。分かりやすい画面は、人にも AI にも分かりやすい。
参考
- Playwright Docs — Installation
- Playwright Docs — Release notes
- Playwright Docs — Best Practices
- Playwright Docs — Locators
- Playwright Docs — Auto-waiting (Actionability)
- Playwright Docs — Isolation (Browser contexts)
- Playwright Docs — Authentication
- Playwright Docs — Continuous Integration
- Playwright Docs — Playwright Test Agents
- GitHub — microsoft/playwright-mcp
- GitHub — microsoft/playwright-cli
- InfoQ — Microsoft Announces Playwright Alternative to Puppeteer (2020-01-30)
- Wikipedia — Playwright (software)
- Selenium — History
- Thoughtworks Technology Radar — Playwright
- GitHub Changelog — Copilot coding agent now has its own web browser (2025-07-02)
- GitHub — eslint-plugin-playwright
- npm — @playwright/test
テストは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