はじめに
社内ネットワークでPlaywrightを動かそうとして、こんな壁にぶつかったことはありませんか?
Error: browserType.launch: Failed to launch chromium because executable doesn't exist at ...
npx playwright install を実行してもプロキシにブロックされ、playwright codegen を試してもブラウザが起動しない。「Playwrightって社内では使えないのかな……」と諦めかけていたところに見つけた突破口が、CDP(Chrome DevTools Protocol)接続でした。
本記事では以下を解説します。
- なぜセキュリティ制限環境でPlaywrightが動かないか(原因の整理)
- CDP接続で制限を回避する具体的な手順とコード
-
codegenなしでセレクターを特定する代替手法 - AIとの協業を支える CLAUDE.md への環境制約記録術
なぜ動かないのか:セキュリティ制限の実態
chromium.launch() が止まる仕組み
Playwrightの標準起動コードは次のとおりです。
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
内部では、Playwrightが自身にバンドルしているChromiumバイナリを 新規プロセスとして起動 しています。エンタープライズ環境でよく見られる以下のポリシーが、このステップを止めます。
| ポリシーの種類 | 具体的な制限 |
|---|---|
| プロセス起動制限 | 許可リスト外のパスからの実行ファイル起動をブロック |
| ダウンロード制限 |
npx playwright install によるバイナリ取得をプロキシでブロック |
| ネットワーク制限 | Playwright が疎通確認に使う外部エンドポイントへの通信をブロック |
npx playwright install を実行しても次のようなエラーで止まります。
Error: Failed to download Chromium r1234
caused by: getaddrinfo ENOTFOUND storage.googleapis.com
「インストールしろと言われたのにインストールもできない」という無限ループです。
codegen が使えない理由
npx playwright codegen https://example.com
codegen は内部で chromium.launch() を呼ぶため、同じ理由で動作しません。セレクターを視覚的に取得する最も手軽な手段が、最初から封じられてしまいます。
解決策:「起動する」から「接続する」へ
CDPとは何か
CDP(Chrome DevTools Protocol)は、ChromeやEdgeが標準で持つデバッグ用のプロトコルです。ブラウザを --remote-debugging-port オプション付きで起動すると、そのポートでCDPサーバーが立ち上がります。
ブラウザの開発者ツール(DevTools)自体もCDPで動いています。Playwrightもこのプロトコルを使ってブラウザを操作しているため、CDPさえ繋がれば通常どおり動作します。
1行の切り替えで突破できる
// ❌ Before: Playwrightがブラウザを起動(セキュリティ制限で失敗)
const browser = await chromium.launch();
// ✅ After: 起動済みブラウザにCDP経由で接続
const browser = await chromium.connectOverCDP('http://localhost:9222');
ブラウザの起動は 人間が手動で行う ため、セキュリティポリシーの対象になりません。Playwrightは「すでに動いているブラウザ」に接続するだけなので、バイナリの新規起動も発生しません。
事前準備:デバッグポート付きでブラウザを起動する
起動コマンド(OS別)
既存のChromeプロセスが起動中だと接続できないケースがあります。先にChromeを完全に終了してから実行してください。
Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--remote-debugging-port=9222 ^
--user-data-dir=C:\tmp\chrome-debug
macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug
Linux
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug
--user-data-dir に 普段使いのプロファイルとは別のディレクトリ を指定するのが重要です。同じプロファイルを指定すると既存セッションと競合し、Chromeが「すでに起動中」として新しいプロセスを立ち上げずにデバッグポートが開かないことがあります。
接続確認
curl http://localhost:9222/json/version
以下のようなJSONが返れば準備完了です。
{
"Browser": "Chrome/124.0.0.0",
"Protocol-Version": "1.3",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/..."
}
実践:CDP接続でテストを書く
基本的な接続パターン
import { chromium } from '@playwright/test';
async function main() {
// 起動済みブラウザに接続
const browser = await chromium.connectOverCDP('http://localhost:9222');
// contexts()[0] で既存のコンテキスト(セッション)を取得
const contexts = browser.contexts();
if (contexts.length === 0) {
throw new Error('ブラウザにコンテキストがありません。Chromeでタブを1つ以上開いてください。');
}
const context = contexts[0];
// 既存のタブを取得(なければ新規作成)
const pages = context.pages();
const page = pages.length > 0 ? pages[0] : await context.newPage();
await page.goto('https://example.com');
console.log(await page.title());
// close() はブラウザを終了しない(接続を切るだけ)
await browser.close();
}
main().catch(console.error);
launch() との最大の違いは browser.close() でブラウザ本体が終了しない 点です。接続を切るだけなので、ログイン状態などのセッションはブラウザ側に残ります。
また、contexts()[0] が空の場合(Chromeを起動してもタブがない状態)は明示的にエラーにしておくと、無言で失敗するより原因が追いやすくなります。
playwright.config.ts でCDP接続を共通化する
テストファイルごとに接続コードを書くのは非効率です。設定ファイルで共通化できます。
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'cdp-chrome',
use: {
// CDP接続先を設定
// launch() は呼ばれないため、バイナリ不要
connectOptions: {
wsEndpoint: 'http://localhost:9222',
},
},
},
],
});
connectOptions.wsEndpoint はPlaywright v1.37以降で使用可能です。それ以前のバージョンでは chromium.connectOverCDP() をfixture経由で共通化してください。
ログイン自動化の実装例
import { test, expect, chromium } from '@playwright/test';
test.describe('ログイン自動化(CDP接続)', () => {
test('社内アプリにログインできる', async () => {
const browser = await chromium.connectOverCDP('http://localhost:9222');
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();
await page.goto(process.env.APP_URL!);
// セレクターはDevToolsで事前に確認したものを使用
await page.locator('#username').fill(process.env.APP_USERNAME!);
await page.locator('#password').fill(process.env.APP_PASSWORD!);
// waitForNavigation は v1.26 以降で非推奨。
// waitForURL または waitForLoadState を使う
await Promise.all([
page.waitForURL('**/dashboard'),
page.locator('button[type="submit"]').click(),
]);
await expect(page).toHaveURL(/dashboard/);
await browser.close();
});
});
ポイント①:waitForNavigation は使わない
waitForNavigation はPlaywright v1.26以降で非推奨になりました。代わりに waitForURL や waitForLoadState を使います。CDP接続環境でも同様です。
ポイント②:Promise.all でクリックとナビゲーションを同時に待つ
クリック後に await waitForURL() を単独で呼ぶと、高速なリダイレクトに乗り遅れることがあります。Promise.all で「クリックと同時に待ち始める」のが定石です。
ポイント③:認証情報は環境変数で
.env ファイル + dotenv か、CI/CDのシークレット変数に入れるのが安全です。コードにハードコードしないよう注意してください。
セレクターの調べ方:codegen なしの3つの代替手法
codegen が使えない環境での現実的なセレクター特定方法をまとめます。
方法①:DevToolsの「Copy selector」
F12でElementsパネルを開き、対象要素を右クリック → 「Copy」→「Copy selector」。
ただし、これで得られるセレクターは次のように冗長になりがちです。
/* DevToolsが生成する冗長なセレクター */
#root > div > main > form > div:nth-child(2) > input
できれば id や name 属性、data-testid などシンプルな識別子に整理しましょう。
方法②:page.pause() でPlaywright Inspectorを起動
// CDP接続後のコード中に挿入
await page.pause();
CDP接続環境でも page.pause() は動作します。Inspectorウィンドウが開き、要素にホバーするとロケーター候補が表示されます。codegen との違いは「既存ページに対してInspectorを開ける」点で、ログイン後の画面など再現が難しいページでのセレクター確認に特に便利です。
方法③:REPLで直接検証
Node.jsのREPL環境(node --experimental-repl-await)やテストコード内で、次のようにその場で確認できます。
// 要素が存在するか確認
const el = await page.$('#login-form input[name="username"]');
console.log(el ? '✅ 見つかった' : '❌ 見つからない');
// 複数マッチしていないか確認(1件のみが期待値)
const count = await page.locator('button[type="submit"]').count();
console.log(`マッチ数: ${count}`); // 1 以外なら要セレクター見直し
// テキストで要素を特定する(ロケーターAPIを使う)
const btn = page.getByRole('button', { name: 'ログイン' });
console.log(await btn.isVisible());
Playwrightの ロケーターAPI(getByRole・getByText・getByLabel)はセレクター文字列より壊れにくく、可読性も高いのでおすすめです。
Before / After:制限環境での変化まとめ
| 項目 | Before(制限あり・回避策なし) | After(CDP接続) |
|---|---|---|
chromium.launch() |
❌ 起動ブロックでハング | ✅ 不要(接続に切り替え) |
playwright codegen |
❌ ブラウザ起動できず使用不可 | 🔄 page.pause() + DevTools で代替 |
| テスト実行 | ❌ 全滅 | ✅ 通常どおり動作 |
| ログイン状態 | ❌ 毎回ログインが必要 | ✅ 既存セッションをそのまま利用可能 |
| バイナリ管理 | ❌ npx playwright install がブロック |
✅ 不要(会社支給Chromeを使用) |
CLAUDE.mdに環境制約を記録する
CDP接続という回避策を発見したとき、次に考えたのは「この知見をAIに渡す方法」でした。
次回ClaudeにPlaywrightのコードを生成してもらうとき、制約を知らなければ chromium.launch() を使ったコードが出力されます。毎回「この環境では使えません」と訂正するのは非効率ですし、「なぜ使えないか」の背景まで毎回説明するのも面倒です。
CLAUDE.md はその問題を解決します。
CLAUDE.mdとは
プロジェクトルートに置く CLAUDE.md は、Claude(AIエージェント)がプロジェクトを開いた際に自動的に読み込むコンテキストファイルです。人間向けの README.md と同様に、AIのためのプロジェクト説明書として機能します。
実際に使っているCLAUDE.mdテンプレート
# プロジェクト:ログイン自動化
## ⚠️ 環境制約(必ず読むこと)
このプロジェクトは社内セキュリティポリシーにより、Playwrightの標準的な起動方法が使えません。
### 使用禁止
- `chromium.launch()` / `firefox.launch()` / `webkit.launch()`
- `npx playwright codegen`(ブラウザを新規起動するため)
### 代替手段:CDP接続を必ず使うこと
```typescript
// ❌ NG
const browser = await chromium.launch();
// ✅ OK
const browser = await chromium.connectOverCDP('http://localhost:9222');
```
### テスト実行前の手順(人間がやる)
1. 既存のChromeをすべて終了する
2. 以下のコマンドでデバッグモードで起動する(Windows):
```cmd
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\tmp\chrome-debug
```
3. `curl http://localhost:9222/json/version` でJSONが返ることを確認する
### セレクターの調べ方
`codegen` が使えないため以下の手順で確認する:
1. DevTools(F12)→ Elementsパネルで対象要素を右クリック →「Copy selector」
2. コード中に `await page.pause()` を挿入してPlaywright Inspectorで確認
3. `page.getByRole()` / `page.getByLabel()` などロケーターAPIを優先使用
## よく使うコマンド
```bash
# テスト実行(Chromeのデバッグ起動を先に済ませること)
npx playwright test
# デバッグ実行
npx playwright test --debug login.spec.ts
# 特定テストのみ
npx playwright test -g "ログイン"
```
環境制約を記録する3つのカテゴリ
CLAUDE.mdに書く制約情報は次の3つに整理すると抜け漏れが減ります。
| カテゴリ | 書く内容 | 例 |
|---|---|---|
| ① 使えないもの | 動かないAPI・コマンド |
chromium.launch() 禁止 |
| ② 代替手段 | 回避策と理由 |
connectOverCDP を使う理由 |
| ③ 事前手順 | 人間がやる必要があること | デバッグポート付き起動 |
この3つが揃えば、AIが「制約の背景」を理解したうえでコードを生成できます。「禁止」だけ書いて代替手段がないと、AIは推測でコードを書いてしまいます。
まとめ
「セキュリティ制限でPlaywrightが動かない」問題は、connectOverCDP という公式サポートの機能で突破できます。
今日から使えるチェックリスト:
- Chromeをデバッグモードで起動するコマンドをメモする
-
chromium.launch()→chromium.connectOverCDP('http://localhost:9222')に切り替える -
page.pause()でセレクター確認できることを確認する -
waitForNavigationではなくwaitForURLを使う - 環境制約・代替手段・事前手順を CLAUDE.md に記録する
CDP接続はPlaywrightが公式にサポートする機能で、リモートブラウザのテストやCI環境でのヘッドレス実行でも広く使われています。「セキュリティ制限があるから使えない」ではなく、「制限があるなら回り道を探す」。その発想を持てると、エンタープライズ環境での自動化の可能性がぐっと広がります。