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. 02测试 Web 与 Electron 应用
  3. 03功能与视觉 QA 清单
  4. 04视口与原生窗口验证
  5. 05代码变更后复用会话

适用场景

  1. 01跨多轮迭代调试本地 Web 应用
  2. 02检查 Electron 启动与渲染器变更
  3. 03验证桌面与移动端布局
  4. 04为 QA 验收截取屏幕截图

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 会话来调试本地 Web 或 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 启动 Codex(或设置等效配置 sandbox_mode=danger-full-access)。这是暂时要求,因为仍在完善沙盒内的 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 检查清单:
    • 根据三个来源编制检查清单:用户提出的要求、你实际实现的用户可见功能或行为,以及你预期在最终回复中作出的声明。
    • 这三个来源中出现的每一项,都必须至少对应一项 QA 检查,才能通过验收。
    • 列出你计划验收的用户可见声明。
    • 列出每个有意义的面向用户的控件、模式切换或已实现的交互行为。
    • 列出每个控件或已实现行为可能引发的状态变化或视图变化。
    • 将其作为功能 QA 和视觉 QA 共用的覆盖清单。
    • 对每项声明或控件—状态组合,记录预期的功能检查、必须进行视觉检查的具体状态,以及预期采集的证据。
    • 如果某项要求在视觉上至关重要但较为主观,请将其转化为可观察的 QA 检查,而不要让它停留在隐含层面。
    • 至少添加 2 个探索性或偏离正常流程的场景,以发现脆弱行为。
  2. 运行一次引导单元格。
  3. 在持久化 TTY 会话中启动或确认所有必需的开发服务器。
  4. 启动正确的运行时,并持续复用同一批 Playwright 句柄。
  5. 每次代码变更后,渲染器专属变更应重新加载;主进程或启动相关变更应重新启动。
  6. 使用正常的用户输入运行功能 QA。
  7. 单独进行视觉 QA。
  8. 验证视口适配情况,并截取支持相关声明所需的屏幕截图。
  9. 仅在任务确实完成后清理 Playwright 会话。

引导(运行一次)

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}`
  );
}

绑定规则:

  • 对共享的顶层 Playwright 句柄使用 var,因为后续的 js_repl 单元格会复用它们。
  • 以下设置单元格刻意采用简短的正常流程。如果某个句柄看起来已失效,请将该绑定设为 undefined 后重新运行单元格,而不是到处添加恢复逻辑。
  • 对于你关心的每个界面,优先使用一个命名句柄(page、mobilePage、appWindow),而不是反复从上下文中重新查找页面。

共享 Web 助手:

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,而不是直接切换到原生窗口模式。
  • 需要验证启动后的窗口尺寸、操作系统级 DPI 行为、浏览器界面交互,或可能取决于主机显示配置的错误时,使用原生窗口模式(viewport: null)进行单独的有头验证。
  • 对于 Electron,始终按原生窗口行为处理。Electron 通过 Playwright 使用 noDefaultViewport 启动,因此应将其视为真实的桌面窗口,并在调整大小前检查启动时的尺寸和布局。
  • 如果验收同时依赖布局断点和真实桌面行为,请完成两轮验证:先使用明确指定的视口进行确定性 QA,再使用原生窗口模式进行最终的环境特定检查。
  • 将模式切换视为上下文重置。不要在原生窗口验证中重复使用视口模拟的 context,反之亦然;关闭旧的 page 和 context,然后为新模式创建一个新的实例。

启动或复用 Web 会话

桌面端和移动端 Web 会话共用相同的 browser、辅助工具和 QA 流程。主要区别在于创建哪一对上下文和页面。

桌面端 Web 上下文

将 TARGET_URL 设置为你正在调试的应用地址。对于本地服务器,优先使用 127.0.0.1 而不是 localhost。

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 单元格都应简短,并专注于一轮交互。
  • 复用现有的顶层绑定(browser、context、page、electronApp、appWindow),而不是重新声明它们。
  • 如果需要隔离,请在同一浏览器中创建新页面或新上下文。
  • 对于 Electron,仅在检查主进程或执行专用诊断时使用 electronApp.evaluate(...)。
  • 原地修正辅助工具中的错误;除非内核确实损坏,否则不要重置 REPL。

检查清单

会话循环
  • 启动 js_repl 一次,然后在后续迭代中保持相同的 Playwright 句柄处于活动状态。
  • 从当前工作区启动目标运行时。
  • 修改代码。
  • 根据此次修改采用正确的路径重新加载或重新启动。
  • 如果探索过程中发现其他控件、状态或可见声明,请更新共享 QA 清单。
  • 重新运行功能 QA。
  • 重新运行视觉 QA。
  • 仅当当前状态就是要评估的状态时,才捕获最终产物。
重新加载决策
  • 仅渲染器发生更改:重新加载现有页面或 Electron 窗口。
  • 主进程、预加载或启动逻辑发生更改:重新启动 Electron。
  • 对进程归属或启动代码有新的疑问:重新启动,不要猜测。
功能 QA
  • 使用真实用户控件进行验收:键盘、鼠标、点击、触摸或等效的 Playwright 输入 API。
  • 至少验证一个端到端的关键流程。
  • 确认该流程的可见结果,而不只是内部状态。
  • 对于实时应用或动画繁多的应用,在实际交互时序下验证其行为。
  • 按照共享 QA 清单进行检查,不要临时抽查。
  • 验收前至少覆盖每个明显的可见控件一次,而不仅仅是主要的理想路径。
  • 对于清单中的可逆控件或有状态切换项,测试完整周期:初始状态、更改后的状态,以及恢复到初始状态。
  • 脚本化检查通过后,使用常规输入进行 30-90 秒的简短探索,而不是只沿着预设路径操作。
  • 如果探索过程中发现新的状态、控件或声明,请将其加入共享 QA 清单,并在验收前完成检查。
  • page.evaluate(...) 和 electronApp.evaluate(...) 可以用于检查或准备状态,但不计作验收输入。
视觉 QA
  • 将视觉 QA 与功能 QA 分开处理。
  • 使用测试前定义并在 QA 过程中更新的同一份共享 QA 清单;不要从另一份隐含清单开始进行视觉检查。
  • 重述用户可见的各项声明,并逐项明确验证;不要假设功能测试通过就证明视觉声明成立。
  • 只有在用户预期感知到某项声明的特定状态下完成检查,才能确认该声明。
  • 滚动前检查初始视口。
  • 确认初始视图能直观地支持界面的主要声明;如果核心承诺的元素在初始视图中不够明显,就将其视为缺陷。
  • 检查所有要求可见的区域,而不只是主要交互界面。
  • 检查共享 QA 清单中已列出的状态和模式;如果任务具有交互性,至少检查一个有意义的交互后状态。
  • 如果动效或过渡是体验的一部分,除稳定后的起始和结束状态外,还要检查至少一个过渡中的状态。
  • 如果标签、覆盖层、标注、指南或高亮应随内容变化而同步更新,请在相关状态变化后验证这种关联。
  • 对于动态视觉效果或依赖交互的视觉内容,应检查足够长的时间,以判断其稳定性、层叠关系和可读性;不要仅凭一张截图就确认通过。
  • 如果产品在加载或交互后可能变得更密集,请在 QA 期间检查可达到的最密集现实状态,而不只是空白、加载中或折叠状态。
  • 如果产品定义了最低支持视口或窗口尺寸,请在该尺寸下单独进行视觉 QA;否则,选择一个较小但仍切合实际的尺寸,并明确检查。
  • 区分是否存在与实现效果:如果某项预期功能虽然在技术上存在,但因对比度不足、被遮挡、被裁切或不稳定而不够明显,就视为视觉缺陷。
  • 如果正在评估的状态下,任何要求可见的区域遭到裁切、截断、遮挡或被挤出视口,即使页面级滚动指标看起来正常,也要将其视为缺陷。
  • 检查裁切、溢出、变形、布局失衡、间距不一致、对齐问题、文字难以辨认、对比度不足、层叠关系错误,以及不自然的动效状态。
  • 除了正确性,也要评估美观程度。界面应让人觉得经过精心设计、风格统一,并且适合当前任务。
  • 确认通过时优先使用视口截图。仅将整页截图用作辅助调试材料;需要近距离检查某个区域时,应截取该区域。
  • 如果动效导致截图难以判断,请稍等界面稳定后,再截取实际用于评估的图像。
  • 确认通过前,明确自问:界面上还有哪些可见部分我尚未仔细检查?
  • 确认通过前,明确自问:用户仔细查看时,最可能让这个结果显得糟糕的可见缺陷是什么?
签收
  • 功能路径已通过正常用户输入测试。
  • 已针对共享 QA 清单明确覆盖范围:注明测试了哪些需求、已实现的功能、控件、状态和声明,并指出任何有意排除的内容。
  • 视觉 QA 检查已覆盖所有相关界面。
  • 每项面向用户的声明都有对应的视觉检查和经审核的截图产物,截图取自该声明适用的状态和视口或窗口尺寸。
  • 视口适配检查已通过,涵盖预期的初始视图以及任何要求的最小支持视口或窗口尺寸。
  • 如果产品以窗口形式启动,则在手动调整大小或位置之前,已检查其启动时的尺寸、位置和初始布局。
  • UI 不仅功能正常,视觉上也协调一致,不会因审美欠佳而影响任务。
  • 功能正确性、视口适配和视觉质量必须分别通过;其中一项通过不代表其他项也通过。
  • 对于交互式产品,已完成简短的探索性检查,且回复中说明了该检查的覆盖范围。
  • 如果截图审查与数值检查曾有不一致,已在签收前调查差异;截图中可见的裁切属于必须解决的问题,不能用指标推翻。
  • 简要确认已检查且未发现主要缺陷类型。
  • 已执行清理,或有意保持会话开启以便继续工作。

截图示例

如果计划通过 codex.emitImage(...) 输出截图,默认使用下一节中的 CSS 归一化路径。这些是规范示例,适用于由模型解读或用于基于坐标执行后续操作的截图。仅在需要保真度敏感型调试时,才将原始捕获作为例外;原始捕获示例见归一化指南之后。

面向模型的截图(默认)

如果要使用 codex.emitImage(...) 发送截图供模型解读,请先将截图调整为所截取区域对应的 CSS 像素尺寸。这样可以确保返回的坐标与 Playwright CSS 像素对齐,以便后续用于点击,同时还能减小图像负载和模型 token 开销。

默认不要发送原始的原生窗口截图。只有在明确需要设备像素级精度时才跳过调整,例如调试 Retina 或 DPI 伪影、检查像素级渲染,或其他原始像素比负载大小更重要的精度敏感场景。对于不会发送给模型、仅用于本地检查的截图,可以直接使用原始截图。

在原生窗口模式(viewport: null)下,不要假定 page.screenshot({ scale: "css" }) 就足够了。在 macOS Retina 显示器上,即使请求了 scale: "css",Chromium 的有界原生窗口截图仍可能以设备像素尺寸返回。通过 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 })
  • await clickCssPoint({ surface: page, clip: box, x, y }),其中 box 来自 const box = await locator.boundingBox()

当 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 });
原始截图例外示例

仅当原始像素比 CSS 坐标对齐更重要时才使用这些示例,例如 Retina 或 DPI 伪影调试、像素级精准渲染检查,或其他对保真度要求较高的审查。

Web 桌面端原始输出:

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

Electron 原始输出:

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

移动端 Web 上下文已运行后的移动端原始输出:

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 会话中运行。不要依赖短生命周期 Shell 中的一次性后台命令。

使用项目的常规启动命令,例如:

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 启动的;如有需要,请使用该参数重新启动。

评论

支持 Markdown