mcpField
P
playwright-interactive

playwright-interactive

github.com/openaiテスト・品質28.4k5.5k 回インストール18 週間前に更新
リポジトリ

playwright-interactive について

playwright-interactiveは、永続的なjs_replのPlaywrightセッションでローカルのWebアプリやElectronアプリをデバッグ・テストするスキルです。ブラウザーのハンドルを反復作業で維持しながら、機能面とビジュアル面のQAを行う開発者向けです。

主な機能

  1. 01永続的なPlaywrightセッション
  2. 02Web・Electronアプリのテスト
  3. 03機能・ビジュアルQAチェックリスト
  4. 04ビューポートとネイティブウィンドウの検証
  5. 05コード変更後のセッション再利用

活用シーン

  1. 01ローカルWebアプリの反復デバッグ
  2. 02Electronの起動やレンダラー変更の確認
  3. 03デスクトップとモバイルのレイアウト検証
  4. 04QAサインオフ用スクリーンショットの取得

MCP 対応

このスキルは永続的なjs_replセッションでPlaywrightを使用します。入力にはMCPサーバーへの接続や管理についての記載はありません。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/openai/skills --skill playwright-interactive

インストール後、Claude Code で/playwright-interactiveを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE.txt
NOTICE.txt
agents/
openai.yaml
assets/
playwright-small.svg

詳細

Playwright インタラクティブスキル

永続的な js_repl の Playwright セッションを使って、ローカルのウェブアプリや Electron アプリをデバッグします。イテレーションをまたいで同じハンドルを維持し、プロセスの所有者が変わった場合を除き、ツールチェーン全体を再起動せずに機能テストとビジュアル QA を実施します。

前提条件

  • このスキルでは js_repl が有効になっている必要があります。
  • js_repl がない場合は、~/.codex/config.toml で有効にします。
[features]
js_repl = true
  • --enable js_repl を使って新しいセッションを開始することもできます(-c features.js_repl=true と同等です)。
  • js_repl を有効にした後、ツール一覧を更新するために新しい Codex セッションを開始します。
  • 現時点では、サンドボックスを無効にしてこのワークフローを実行します。--sandbox danger-full-access(または sandbox_mode=danger-full-access に相当する設定)を指定して Codex を起動してください。サンドボックス内での js_repl と Playwright のサポートがまだ開発中のため、これは一時的な要件です。
  • デバッグするプロジェクトと同じディレクトリからセットアップを実行します。
  • js_repl_reset は通常のクリーンアップではなく、復旧用ツールとして扱います。カーネルをリセットすると、Playwright のハンドルは破棄されます。

初回セットアップ

test -f package.json || npm init -y
npm install playwright
# Web-only, for headed Chromium or mobile emulation:
# npx playwright install chromium
# Electron-only, and only if the target workspace is the app itself:
# npm install --save-dev electron
node -e "import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })"

後で別のワークスペースに切り替える場合は、そこでセットアップを繰り返します。

基本的なワークフロー

  1. テストの前に、簡潔な QA 項目一覧を作成します。
    • 次の 3 つの情報源から項目を作成します。ユーザーが求めた要件、実際に実装したユーザー向けの機能や動作、最終回答で主張する予定の内容です。
    • この 3 つのいずれかに含まれる内容は、すべてサインオフ前に少なくとも 1 つの QA チェックに対応させます。
    • サインオフする予定の、ユーザー向けの主張を一覧にします。
    • ユーザー向けの意味のあるコントロール、モード切り替え、実装したインタラクティブな動作をすべて一覧にします。
    • 各コントロールや実装した動作によって生じる状態の変化や表示の変化を一覧にします。
    • これを機能 QA とビジュアル QA の共通のカバレッジ一覧として使用します。
    • 各主張またはコントロールと状態の組み合わせについて、実施する機能チェック、ビジュアルチェックを行う具体的な状態、記録する証拠を記載します。
    • 視覚的に重要でありながら主観的な要件は、暗黙のままにせず、観察可能な QA チェックに置き換えます。
    • 脆弱な動作を見つける可能性のある、少なくとも 2 つの探索的なシナリオまたは想定外のシナリオを追加します。
  2. ブートストラップセルを 1 回実行します。
  3. 必要な開発サーバーを永続的な TTY セッションで起動するか、起動済みであることを確認します。
  4. 適切なランタイムを起動し、同じ Playwright ハンドルを引き続き再利用します。
  5. コードを変更するたびに、レンダラーのみの変更では再読み込みし、メインプロセスや起動処理の変更では再起動します。
  6. 通常のユーザー入力を使って機能 QA を実施します。
  7. 別途、ビジュアル QA を実施します。
  8. ビューポートへの収まりを確認し、主張の裏付けとなるスクリーンショットを撮影します。
  9. タスクが実際に完了した場合に限り、Playwright セッションをクリーンアップします。

ブートストラップ(1 回実行)

var chromium;
var electronLauncher;
var browser;
var context;
var page;
var mobileContext;
var mobilePage;
var electronApp;
var appWindow;

try {
  ({ chromium, _electron: electronLauncher } = await import("playwright"));
  console.log("Playwright loaded");
} catch (error) {
  throw new Error(
    `Could not load playwright from the current js_repl cwd. Run the setup commands from this workspace first. Original error: ${error}`
  );
}

遵守事項:

  • 後続の js_repl セルで再利用するため、共有するトップレベルの Playwright ハンドルには var を使います。
  • 以下のセットアップセルは、意図的に簡潔な正常系のみの手順にしています。ハンドルが古くなったように見える場合は、あちこちに復旧処理を追加するのではなく、そのバインディングを undefined に設定してセルを再実行します。
  • コンテキストからページを何度も探し直すのではなく、対象となるサーフェスごとに名前付きハンドルを 1 つ用意します(page、mobilePage、appWindow)。

共有ウェブヘルパー:

var resetWebHandles = function () {
  context = undefined;
  page = undefined;
  mobileContext = undefined;
  mobilePage = undefined;
};

var ensureWebBrowser = async function () {
  if (browser && !browser.isConnected()) {
    browser = undefined;
    resetWebHandles();
  }

  browser ??= await chromium.launch({ headless: false });
  return browser;
};

var reloadWebContexts = async function () {
  for (const currentContext of [context, mobileContext]) {
    if (!currentContext) continue;
    for (const p of currentContext.pages()) {
      await p.reload({ waitUntil: "domcontentloaded" });
    }
  }
  console.log("Reloaded existing web tabs");
};

セッションモードを選択する

Web アプリでは、デフォルトで明示的なビューポートを使用し、ネイティブウィンドウモードは別の検証パスとして扱います。

  • 通常の反復作業、ブレークポイントの確認、再現可能なスクリーンショット、スナップショットの差分、モデルを活用したローカライズには、明示的なビューポートを使用します。マシン間で安定しており、ホストのウィンドウマネージャーによるばらつきを避けられるため、これをデフォルトとします。
  • 決定論的な高 DPI の動作が必要な場合は、明示的なビューポートを維持し、ネイティブウィンドウモードへすぐに切り替えるのではなく、deviceScaleFactor を追加します。
  • 起動時のウィンドウサイズ、OS レベルの DPI 動作、ブラウザーのクロームとのインタラクション、またはホストのディスプレイ設定に依存する可能性のある不具合を検証する必要がある場合は、別の headed パスでネイティブウィンドウモード(viewport: null)を使用します。
  • Electron では、常にネイティブウィンドウの動作を前提とします。Electron は Playwright を通じて noDefaultViewport を指定して起動するため、実際のデスクトップウィンドウとして扱い、サイズを変更する前に起動時のサイズとレイアウトを確認します。
  • 承認にレイアウトのブレークポイントと実際のデスクトップ動作の両方が必要な場合は、両方のパスを実施します。決定論的な QA のために明示的なビューポートを先に確認し、その後、最終的な環境固有の確認としてネイティブウィンドウを検証します。
  • モードの切り替えはコンテキストのリセットとして扱います。ビューポートエミュレーションを使用した context をネイティブウィンドウのパスで再利用したり、その逆を行ったりしないでください。新しいモード用に新たに作成する前に、古い page と context を閉じます。

Web セッションを開始または再利用する

デスクトップとモバイルの Web セッションでは、同じ browser、ヘルパー、QA フローを共有します。主な違いは、作成するコンテキストとページの組み合わせです。

デスクトップ Web コンテキスト

デバッグするアプリに TARGET_URL を設定します。ローカルサーバーでは、localhost より 127.0.0.1 を優先してください。

var TARGET_URL = "http://127.0.0.1:3000";

if (page?.isClosed()) page = undefined;

await ensureWebBrowser();
context ??= await browser.newContext({
  viewport: { width: 1600, height: 900 },
});
page ??= await context.newPage();

await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded:", await page.title());

context または page が古くなっている場合は、context = page = undefined を設定してセルを再実行します。

モバイル Web コンテキスト

TARGET_URL がすでに存在する場合は再利用します。存在しない場合は、モバイルのターゲットを直接設定します。

var MOBILE_TARGET_URL = typeof TARGET_URL === "string"
  ? TARGET_URL
  : "http://127.0.0.1:3000";

if (mobilePage?.isClosed()) mobilePage = undefined;

await ensureWebBrowser();
mobileContext ??= await browser.newContext({
  viewport: { width: 390, height: 844 },
  isMobile: true,
  hasTouch: true,
});
mobilePage ??= await mobileContext.newPage();

await mobilePage.goto(MOBILE_TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded mobile:", await mobilePage.title());

mobileContext または mobilePage が古くなっている場合は、mobileContext = mobilePage = undefined を設定してセルを再実行します。

ネイティブウィンドウの Web パス
var TARGET_URL = "http://127.0.0.1:3000";

await ensureWebBrowser();

await page?.close().catch(() => {});
await context?.close().catch(() => {});
page = undefined;
context = undefined;

browser ??= await chromium.launch({ headless: false });
context = await browser.newContext({ viewport: null });
page = await context.newPage();

await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded native window:", await page.title());

Electron セッションを開始または再利用する

現在のワークスペースが Electron アプリで、package.json の main が適切なエントリーファイルを指している場合は、ELECTRON_ENTRY を . に設定します。特定のメインプロセスファイルを直接指定する必要がある場合は、代わりに ./main.js のようなパスを使用します。

var ELECTRON_ENTRY = ".";

if (appWindow?.isClosed()) appWindow = undefined;

if (!appWindow && electronApp) {
  await electronApp.close().catch(() => {});
  electronApp = undefined;
}

electronApp ??= await electronLauncher.launch({
  args: [ELECTRON_ENTRY],
});

appWindow ??= await electronApp.firstWindow();

console.log("Loaded Electron window:", await appWindow.title());

js_repl が Electron アプリのワークスペースからすでに実行されていない場合は、起動時に cwd を明示的に指定します。

アプリのプロセスが古くなっているように見える場合は、electronApp = appWindow = undefined を設定してセルを再実行します。

すでに Electron セッションがあり、メインプロセス、プリロード、または起動時の変更後に新しいプロセスが必要な場合は、このセルを再実行するのではなく、次のセクションにある再起動用のセルを使用します。

反復作業中にセッションを再利用する

できる限り同じセッションを維持してください。

Web レンダラーの再読み込み:

await reloadWebContexts();

Electron レンダラーのみの再読み込み:

await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");

メインプロセス、プリロード、または起動時の変更後に Electron を再起動する:

await electronApp.close().catch(() => {});
electronApp = undefined;
appWindow = undefined;

electronApp = await electronLauncher.launch({
  args: [ELECTRON_ENTRY],
});

appWindow = await electronApp.firstWindow();
console.log("Relaunched Electron window:", await appWindow.title());

起動に明示的な cwd が必要な場合は、ここでも同じ cwd を指定します。

基本方針:

  • 各 js_repl セルは短く保ち、1 回の操作に集中させます。
  • 既存のトップレベルのバインディング(browser、context、page、electronApp、appWindow)を再宣言せずに再利用します。
  • 分離が必要な場合は、同じブラウザー内に新しいページまたは新しいコンテキストを作成します。
  • Electron では、メインプロセスの調査または専用の診断に限り、electronApp.evaluate(...) を使用します。
  • ヘルパーの誤りはその場で修正します。カーネルが実際に壊れていない限り、REPL をリセットしないでください。

チェックリスト

セッションループ
  • js_repl を一度だけ初期化し、その後の反復でも同じ Playwright ハンドルを維持します。
  • 現在のワークスペースから対象のランタイムを起動します。
  • コードを変更します。
  • 変更内容に応じた正しい方法で再読み込みまたは再起動します。
  • 調査中に新たなコントロール、状態、または目に見える主張が見つかった場合は、共有 QA インベントリを更新します。
  • 機能 QA を再実行します。
  • ビジュアル QA を再実行します。
  • 評価対象の状態が現在の状態になってから、最終成果物を取得します。
再読み込みの判断
  • レンダラーのみの変更:既存のページまたは Electron ウィンドウを再読み込みします。
  • メインプロセス、プリロード、または起動処理の変更:Electron を再起動します。
  • プロセスの所有関係や起動コードに新たな不確実性がある場合:推測せずに再起動します。
機能 QA
  • サインオフには、実際のユーザー操作を使います。キーボード、マウス、クリック、タッチ、または同等の Playwright 入力 API を使用します。
  • 少なくとも 1 つの重要なエンドツーエンドのフローを検証します。
  • 内部状態だけでなく、そのフローの目に見える結果を確認します。
  • リアルタイム性の高いアプリやアニメーションを多用するアプリでは、実際の操作タイミングでの挙動を検証します。
  • その場しのぎの抜き取り確認ではなく、共有 QA インベントリに沿って確認します。
  • サインオフ前に、主要なハッピーパスだけでなく、目に見えるすべての明らかなコントロールを少なくとも 1 回確認します。
  • インベントリにある、元に戻せるコントロールや状態を持つ切り替えについては、初期状態、変更後の状態、初期状態への復帰という一連のサイクルをすべてテストします。
  • スクリプトによるチェックに合格したら、意図した経路だけをたどるのではなく、通常の入力を使って 30〜90 秒間の短い探索的な確認を行います。
  • 探索的な確認で新たな状態、コントロール、または主張が見つかった場合は、共有 QA インベントリに追加し、サインオフ前に確認します。
  • page.evaluate(...) と electronApp.evaluate(...) は状態の確認や準備に使えますが、サインオフ用の入力にはなりません。
ビジュアル QA
  • ビジュアル QA は機能 QA と分けて扱います。
  • テスト前に定義し、QA 中に更新した共通の QA 項目一覧を使用します。暗黙の別の一覧からビジュアルの確認を始めないでください。
  • ユーザーに見える主張を改めて示し、それぞれを明示的に検証します。機能テストに合格したからといって、ビジュアル面の主張も検証済みだと想定しないでください。
  • ユーザーに見える主張は、それが認識されるべき特定の状態で確認するまで、承認済みにはなりません。
  • スクロールする前に、初期ビューポートを確認します。
  • 初期表示がインターフェースの主要な主張を視覚的に裏付けていることを確認します。約束されている重要な要素がそこで明確に認識できない場合は、バグとして扱います。
  • メインの操作領域だけでなく、必要な表示領域をすべて確認します。
  • 対話型のタスクでは、少なくとも意味のある操作後の状態を 1 つ含め、共通の QA 項目一覧にすでに列挙されている状態とモードを確認します。
  • モーションやトランジションが体験の一部である場合は、安定した両端の状態に加えて、トランジション中の状態も少なくとも 1 つ確認します。
  • ラベル、オーバーレイ、注釈、ガイド、ハイライトが変化するコンテンツに追従する設計の場合は、関連する状態変化の後にその対応関係を確認します。
  • 動的なビジュアルや操作に依存するビジュアルは、安定性、レイヤー構成、読みやすさを判断できるまで十分な時間をかけて確認します。1 枚のスクリーンショットだけで承認しないでください。
  • 読み込みや操作の後に表示が密になる可能性があるインターフェースでは、空の状態、読み込み中の状態、折りたたまれた状態だけでなく、QA 中に到達できる現実的な範囲で最も情報量の多い状態を確認します。
  • 製品でサポート対象の最小ビューポートまたはウィンドウサイズが定義されている場合は、そのサイズで別途ビジュアル QA を実施します。定義されていない場合は、小さくても現実的なサイズを選び、明示的に確認します。
  • 要素が技術的には存在していても、コントラスト不足、遮蔽、クリッピング、不安定さによって明確に認識できない場合は、意図された操作要素があることと実際に機能していることを区別し、ビジュアル上の不合格として扱います。
  • 評価対象の状態で必要な表示領域がクリッピングされている、途中で切れている、隠れている、またはビューポートの外に押し出されている場合は、ページ全体のスクロール指標に問題がなくてもバグとして扱います。
  • クリッピング、オーバーフロー、歪み、レイアウトの不均衡、一貫性のない余白、配置の問題、読みにくいテキスト、コントラスト不足、レイヤーの不具合、不自然なモーション状態を探します。
  • 正しさだけでなく、見た目の質も評価します。UI は意図が明確で、一貫性があり、タスクに適した魅力的な見た目であるべきです。
  • 承認にはビューポートのスクリーンショットを優先します。ページ全体のキャプチャは補助的なデバッグ資料としてのみ使い、領域を詳しく確認する必要がある場合は、その部分に絞ったスクリーンショットを撮影します。
  • モーションによってスクリーンショットの判断が難しい場合は、UI が落ち着くまで少し待ってから、実際に評価する画像を撮影します。
  • 承認前に、明示的に次の点を確認します。まだ詳しく確認していない、このインターフェースの表示部分はどこか。
  • 承認前に、明示的に次の点を確認します。ユーザーがよく見たときに、この結果で最も目立ち、困惑させる可能性のある表示上の欠陥は何か。
サインオフ
  • 通常のユーザー入力で、機能上の一連の操作が正常に完了しました。
  • 共有 QA インベントリに照らしてテスト範囲を明示してください。要件、実装済み機能、コントロール、状態、主張のうち、何をテストしたかを記し、意図的に除外した項目があれば明記してください。
  • 関連するインターフェース全体を対象に、ビジュアル QA を実施しました。
  • ユーザーに見える各主張について、その主張が該当する状態とビューポートまたはウィンドウサイズで、対応するビジュアルチェックを実施し、スクリーンショットの成果物をレビューしました。
  • 想定される初期画面、および必要な場合はサポート対象となる最小ビューポートまたはウィンドウサイズで、ビューポートへの収まりを確認し、問題がないことを確認しました。
  • 製品がウィンドウで起動する場合は、手動でサイズ変更や位置変更を行う前に、起動時のサイズ、配置、初期レイアウトを確認しました。
  • UI は機能するだけでなく、視覚的に一貫性があり、タスクに対して美観上の問題がありません。
  • 機能の正確性、ビューポートへの収まり、ビジュアル品質は、それぞれ独立して合格する必要があります。いずれかが合格しても、ほかの合格を意味するわけではありません。
  • インタラクティブな製品では、簡単な探索的チェックを実施し、そのチェックの対象を応答に記載しました。
  • スクリーンショットのレビューと数値によるチェックが食い違った場合は、サインオフ前に不一致を調査しました。スクリーンショットで確認できるクリッピングは解決すべき不具合であり、数値を根拠に無視してはなりません。
  • 確認した主な不具合の種類について、問題が見つからなかったことを簡潔に明記してください。
  • クリーンアップを実行するか、または追加作業に備えてセッションを意図的に維持しました。

スクリーンショットの例

codex.emitImage(...) でスクリーンショットを出力する場合は、次のセクションにある CSS 正規化済みのパスを既定で使用してください。これらは、モデルによる解釈や、座標ベースの後続操作に使用するスクリーンショットの標準的な例です。忠実性が重視されるデバッグの場合に限り、例外として未加工のキャプチャを使用してください。未加工のキャプチャの例は、正規化の説明の後に記載されています。

モデル向けスクリーンショット(デフォルト)

モデルでの解釈用に codex.emitImage(...) でスクリーンショットを出力する場合は、出力前に、キャプチャした正確な領域を CSS ピクセルに正規化してください。これにより、返信が後でクリックに使われる場合も、返された座標が Playwright の CSS ピクセルと一致します。また、画像のペイロードサイズとモデルのトークンコストも削減できます。

デフォルトでは、ネイティブウィンドウのスクリーンショットをそのまま出力しないでください。Retina や DPI によるアーティファクトのデバッグ、ピクセル精度でのレンダリング検査など、ペイロードサイズよりも生ピクセルが重要となる、忠実度が重要なケースで明示的に必要な場合に限り、正規化を省略してください。モデルに出力せず、ローカルでのみ検査する場合は、生のキャプチャで問題ありません。

ネイティブウィンドウモード(viewport: null)では、page.screenshot({ scale: "css" }) で十分だと考えないでください。macOS の Retina ディスプレイ上の Chromium では、scale: "css" を指定しても、ヘッド付きのネイティブウィンドウのスクリーンショットがデバイスピクセルサイズで返されることがあります。同じ注意点は、Playwright から起動した Electron ウィンドウにも当てはまります。Electron は noDefaultViewport で実行されるため、appWindow.screenshot({ scale: "css" }) でもデバイスピクセルの出力が返されることがあります。

ウェブページと Electron ウィンドウでは、別々の正規化方法を使用してください。

  • ウェブ:page.screenshot({ scale: "css" }) を直接使用するのが望ましい方法です。ネイティブウィンドウの Chromium でデバイスピクセルの出力が返される場合は、スクラッチページを使わず、現在のページ内で canvas を使ってリサイズしてください。
  • Electron:スクラッチページとして appWindow.context().newPage() や electronApp.context().newPage() を使用しないでください。Electron のコンテキストでは、この方法は安定してサポートされません。メインプロセスで BrowserWindow.capturePage(...) を使ってキャプチャし、nativeImage.resize(...) でリサイズして、そのバイト列を直接出力してください。

共通のヘルパーと規約:

var emitJpeg = async function (bytes) {
  await codex.emitImage({
    bytes,
    mimeType: "image/jpeg",
    detail: "original",
  });
};

var emitWebJpeg = async function (surface, options = {}) {
  await emitJpeg(await surface.screenshot({
    type: "jpeg",
    quality: 85,
    scale: "css",
    ...options,
  }));
};

var clickCssPoint = async function ({ surface, x, y, clip }) {
  await surface.mouse.click(
    clip ? clip.x + x : x,
    clip ? clip.y + y : y
  );
};

var tapCssPoint = async function ({ page, x, y, clip }) {
  await page.touchscreen.tap(
    clip ? clip.x + x : x,
    clip ? clip.y + y : y
  );
};
  • ウェブでは page または mobilePage、Electron では appWindow を surface として使用します。
  • clip は、レンダラーの getBoundingClientRect() から取得した CSS ピクセルとして扱います。
  • ロスレスの忠実度が特に必要でない限り、quality: 85 の JPEG を優先してください。
  • 画像全体をキャプチャする場合は、返された { x, y } をそのまま使用します。
  • クリップしたキャプチャでは、クリック時にクリップ領域の原点を加算します。
ウェブでの CSS 正規化

ビューポートが明示的に指定されたコンテキストで推奨されるウェブの方法です。また、多くの場合、ウェブ全般にも適しています。

await emitWebJpeg(page);

モバイルウェブも同じ方法を使います。page を mobilePage に置き換えてください。

await emitWebJpeg(mobilePage);

モデルが { x, y } を返した場合は、その座標を直接クリックします。

await clickCssPoint({ surface: page, x, y });

モバイルウェブのクリック方法:

await tapCssPoint({ page: mobilePage, x, y });

この通常の方法では、ウェブの clip スクリーンショットや要素のスクリーンショットに対して、scale: "css" は通常そのまま機能します。クリック時に領域の原点を加算してください。

  • await emitWebJpeg(page, { clip })
  • await emitWebJpeg(mobilePage, { clip })
  • await clickCssPoint({ surface: page, clip, x, y })
  • await tapCssPoint({ page: mobilePage, clip, x, y })
  • const box = await locator.boundingBox() の後に await clickCssPoint({ surface: page, clip: box, x, y })

scale: "css" を使用してもデバイスピクセルサイズで返される場合の、ウェブのネイティブウィンドウ向けフォールバック:

var emitWebScreenshotCssScaled = async function ({ page, clip, quality = 0.85 } = {}) {
  var NodeBuffer = (await import("node:buffer")).Buffer;
  const target = clip
    ? { width: clip.width, height: clip.height }
    : await page.evaluate(() => ({
        width: window.innerWidth,
        height: window.innerHeight,
      }));

  const screenshotBuffer = await page.screenshot({
    type: "png",
    ...(clip ? { clip } : {}),
  });

  const bytes = await page.evaluate(
    async ({ imageBase64, targetWidth, targetHeight, quality }) => {
      const image = new Image();
      image.src = `data:image/png;base64,${imageBase64}`;
      await image.decode();

      const canvas = document.createElement("canvas");
      canvas.width = targetWidth;
      canvas.height = targetHeight;

      const ctx = canvas.getContext("2d");
      ctx.imageSmoothingEnabled = true;
      ctx.drawImage(image, 0, 0, targetWidth, targetHeight);

      const blob = await new Promise((resolve) =>
        canvas.toBlob(resolve, "image/jpeg", quality)
      );

      return new Uint8Array(await blob.arrayBuffer());
    },
    {
      imageBase64: NodeBuffer.from(screenshotBuffer).toString("base64"),
      targetWidth: target.width,
      targetHeight: target.height,
      quality,
    }
  );

  await emitJpeg(bytes);
};

ビューポート全体をキャプチャするフォールバックでは、返された { x, y } を CSS 座標としてそのまま扱います。

await emitWebScreenshotCssScaled({ page });
await clickCssPoint({ surface: page, x, y });

クリップしたフォールバックキャプチャでは、クリップ領域の原点を加算します。

await emitWebScreenshotCssScaled({ page, clip });
await clickCssPoint({ surface: page, clip, x, y });
Electron での CSS 正規化

Electron では、スクラッチの Playwright ページを開くのではなく、メインプロセスで正規化してください。以下のヘルパーは、コンテンツ領域全体、または CSS ピクセルで指定したクリップ領域の、CSS スケールに正規化されたバイト列を返します。clip はコンテンツ領域の CSS ピクセルとして扱います。たとえば、レンダラーの getBoundingClientRect() から取得した値を使用します。

var emitElectronScreenshotCssScaled = async function ({ electronApp, clip, quality = 85 } = {}) {
  const bytes = await electronApp.evaluate(async ({ BrowserWindow }, { clip, quality }) => {
    const win = BrowserWindow.getAllWindows()[0];
    const image = clip ? await win.capturePage(clip) : await win.capturePage();

    const target = clip
      ? { width: clip.width, height: clip.height }
      : (() => {
          const [width, height] = win.getContentSize();
          return { width, height };
        })();

    const resized = image.resize({
      width: target.width,
      height: target.height,
      quality: "best",
    });

    return resized.toJPEG(quality);
  }, { clip, quality });

  await emitJpeg(bytes);
};

Electron ウィンドウ全体:

await emitElectronScreenshotCssScaled({ electronApp });
await clickCssPoint({ surface: appWindow, x, y });

レンダラーの CSS ピクセルを使用した Electron のクリップ領域:

var clip = await appWindow.evaluate(() => {
  const rect = document.getElementById("board").getBoundingClientRect();
  return {
    x: Math.round(rect.x),
    y: Math.round(rect.y),
    width: Math.round(rect.width),
    height: Math.round(rect.height),
  };
});

await emitElectronScreenshotCssScaled({ electronApp, clip });
await clickCssPoint({ surface: appWindow, clip, x, y });
Raw スクリーンショットの例外的な使用例

Retina や DPI に起因するアーティファクトのデバッグ、ピクセル単位で正確なレンダリングの検査、その他忠実度が重視されるレビューなど、CSS 座標との位置合わせよりも生ピクセルが重要な場合に限り、以下を使用してください。

Web デスクトップの raw 出力:

await codex.emitImage({
  bytes: await page.screenshot({ type: "jpeg", quality: 85 }),
  mimeType: "image/jpeg",
  detail: "original",
});

Electron の raw 出力:

await codex.emitImage({
  bytes: await appWindow.screenshot({ type: "jpeg", quality: 85 }),
  mimeType: "image/jpeg",
  detail: "original",
});

モバイル Web のコンテキストがすでに実行されている場合のモバイル raw 出力:

await codex.emitImage({
  bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
  mimeType: "image/jpeg",
  detail: "original",
});

ビューポートへの収まりの確認(必須)

メインのウィジェットが見えているだけで、スクリーンショットが問題ないと判断しないでください。承認前に、スクリーンショットの確認と数値によるチェックの両方を使って、最初に表示されるビューが製品要件を満たしていることを明示的に確認してください。

  • 承認前に、最初に表示されるビューを定義してください。スクロール可能なページでは、これはファーストビューの体験を指します。アプリのようなシェル、ゲーム、エディター、ダッシュボード、ツールでは、操作可能な領域全体に加え、それらを使用するために必要なコントロールとステータス表示を指します。
  • 収まりを確認する際の主な証拠として、スクリーンショットを使用してください。数値によるチェックはスクリーンショットを補足するものであり、目視で確認できるクリッピングを覆すものではありません。
  • 最初に表示されるビューで必要な可視領域がクリッピングされている、一部が切れている、隠れている、またはビューポートの外に押し出されている場合、ページ全体のスクロール指標が許容範囲内に見えても、承認できません。
  • 製品がスクロールするよう設計されており、最初に表示されるビューで中核となる体験が伝わり、主要な行動喚起または必要な開始時のコンテキストが表示されている場合は、スクロールを許容します。
  • 固定シェルのインターフェースでは、操作可能な領域の一部や必須コントロールにアクセスするためにスクロールが必要な場合、スクロールを回避策として許容しません。
  • ドキュメントのスクロール指標だけに依存しないでください。固定高さのシェル、内部ペイン、オーバーフローが非表示のコンテナーでは、ページ単位のスクロールチェックでは問題がないように見えても、必要な UI がクリッピングされていることがあります。
  • ドキュメント全体の境界だけでなく、各領域の境界を確認してください。起動時の状態で、必要な各可視領域がビューポート内に収まっていることを確認してください。
  • Electron またはデスクトップアプリでは、手動でサイズや位置を変更する前に、起動したウィンドウのサイズと配置、およびレンダラーの最初の表示レイアウトの両方を確認してください。
  • ビューポートへの収まりのチェックに合格した場合に確認できるのは、意図しないクリッピングやスクロールなしで、最初に表示されるビューが見えることだけです。UI の見た目が正しいことや、審美的に優れていることを証明するものではありません。

Web またはレンダラーのチェック:

console.log(await page.evaluate(() => ({
  innerWidth: window.innerWidth,
  innerHeight: window.innerHeight,
  clientWidth: document.documentElement.clientWidth,
  clientHeight: document.documentElement.clientHeight,
  scrollWidth: document.documentElement.scrollWidth,
  scrollHeight: document.documentElement.scrollHeight,
  canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
  canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));

Electron のチェック:

console.log(await appWindow.evaluate(() => ({
  innerWidth: window.innerWidth,
  innerHeight: window.innerHeight,
  clientWidth: document.documentElement.clientWidth,
  clientHeight: document.documentElement.clientHeight,
  scrollWidth: document.documentElement.scrollWidth,
  scrollHeight: document.documentElement.scrollHeight,
  canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
  canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));

数値によるチェックを補うため、クリッピングが現実的な失敗要因となる場合は、UI 内の必要な可視領域に対して getBoundingClientRect() によるチェックを行ってください。固定シェルの場合、ドキュメント単位の指標だけでは不十分です。

開発サーバー

ローカル Web のデバッグでは、アプリを永続的な TTY セッションで実行し続けてください。短時間で終了するシェルから、単発のバックグラウンドコマンドを実行する方法に頼らないでください。

プロジェクトで通常使用する起動コマンドを使用してください。例:

npm start

page.goto(...) の前に、選択したポートでリッスンしていることと、アプリが応答することを確認してください。

Electron のデバッグでは、_electron.launch(...) を通じて js_repl からアプリを起動し、同じセッションでプロセスを管理してください。Electron レンダラーが別の開発サーバー(たとえば Vite や Next)に依存する場合は、そのサーバーを永続的な TTY セッションで実行し続けてから、js_repl で Electron アプリを再起動または再読み込みしてください。

クリーンアップ

タスクが実際に完了した場合にのみ、クリーンアップを実行してください。

  • このクリーンアップは手動で行う必要があります。Codex の終了、ターミナルを閉じること、または js_repl セッションを失うことによって、electronApp.close()、context.close()、または browser.close() が自動的に実行されることはありません。
  • Electron については、クリーンアップのセルを実行せずにセッションを終了した場合、アプリが実行されたままになる可能性があるものとして扱ってください。
if (electronApp) {
  await electronApp.close().catch(() => {});
}

if (mobileContext) {
  await mobileContext.close().catch(() => {});
}

if (context) {
  await context.close().catch(() => {});
}

if (browser) {
  await browser.close().catch(() => {});
}

browser = undefined;
context = undefined;
page = undefined;
mobileContext = undefined;
mobilePage = undefined;
electronApp = undefined;
appWindow = undefined;

console.log("Playwright session closed");

デバッグ直後に Codex を終了する場合は、先にクリーンアップのセルを実行し、終了する前に "Playwright session closed" のログが表示されるまで待ってください。

よくある失敗パターン

  • Cannot find module 'playwright':現在のワークスペースで初回セットアップを実行し、js_repl を使用する前にインポートを確認してください。
  • Playwright パッケージはインストールされていますが、ブラウザーの実行ファイルがありません:npx playwright install chromium を実行してください。
  • page.goto: net::ERR_CONNECTION_REFUSED:開発サーバーが永続的な TTY セッションで引き続き実行されていることを確認し、ポートを再確認して、http://127.0.0.1:<port> を優先してください。
  • electron.launch がハングする、タイムアウトする、またはすぐに終了する:ローカルの electron 依存関係を確認し、args のターゲットを確認して、起動前にレンダラー開発サーバーが実行されていることを確認してください。
  • Identifier has already been declared:既存のトップレベルのバインディングを再利用するか、新しい名前を選ぶか、コードを { ... } で囲んでください。カーネルが実際に応答しなくなった場合にのみ、js_repl_reset を使用してください。
  • Electron の操作中に browserContext.newPage: Protocol error (Target.createTarget): Not supported が発生する:スクラッチページとして appWindow.context().newPage() や electronApp.context().newPage() を使用しないでください。モデルバウンドのスクリーンショットのセクションにある、Electron 固有のスクリーンショット正規化フローを使用してください。
  • js_repl がタイムアウトまたはリセットされた:ブートストラップセルを再実行し、より短く、焦点を絞ったセルでセッションを再作成してください。
  • ブラウザーの起動やネットワーク操作がすぐに失敗する:セッションが --sandbox danger-full-access を指定して開始されたことを確認し、必要に応じてその指定で再起動してください。

コメント