| name | playwright-interactive |
| description | 通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。 |
Playwright 交互式技能
使用持久化的 js_repl Playwright 会话来调试本地 web 或 Electron 应用,在多次迭代之间保持同一批句柄存活,并在不重启整套工具链(除非进程归属发生变化)的情况下运行功能与视觉 QA。
前置条件
- 本技能必须启用
js_repl。
- 如果缺少
js_repl,在你的 Codex 配置文件中启用它:
[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
node -e "import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })"
如果你之后切换到另一个工作区,在那里重复设置。
核心工作流程
- 测试前写一份简短的 QA 清单:
- 从三个来源构建清单:用户提出的需求、你实际实现的用户可见特性或行为、以及你预期在最终响应中做出的声明。
- 出现在这三个来源中任意一处的内容,在签收前都必须映射到至少一项 QA 检查。
- 列出你打算签收的用户可见声明。
- 列出每一个有意义的、面向用户的控件、模式切换或已实现的交互行为。
- 列出每个控件或已实现行为可能引起的状态变化或视图变化。
- 把它作为功能 QA 与视觉 QA 共用的覆盖清单。
- 对每个声明或控件-状态对,记下预期的功能检查、必须进行视觉检查的具体状态、以及你预期捕获的证据。
- 如果某个需求在视觉上很关键但主观,把它转化为可观察的 QA 检查,而不是让它保持隐含。
- 至少添加 2 个探索性或偏离顺利路径的场景,它们可能暴露脆弱行为。
- 运行一次引导 cell。
- 在持久化的 TTY 会话中启动或确认所需的开发服务器。
- 启动正确的运行时,并持续复用同一批 Playwright 句柄。
- 每次代码变更后,仅渲染层变更时重载,主进程/启动变更时重启。
- 用正常的用户输入运行功能 QA。
- 运行单独的一轮视觉 QA。
- 验证视口适配,并捕获支撑你声明所需的截图。
- 仅在任务真正完成时才清理 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 cell 会复用它们。
- 下面的设置 cell 是有意写成简短的顺利路径。如果某个句柄看起来失效了,把该绑定设为
undefined 并重新运行该 cell,而不是到处添加恢复逻辑。
- 对你关心的每个界面(
page、mobilePage、appWindow)优先保留一个命名句柄,而不是反复从 context 重新发现页面。
共享 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 行为、浏览器 chrome 交互,或可能依赖宿主显示配置的 bug 时,用原生窗口模式(
viewport: null)做单独的一轮 headed 检查。
- 对于 Electron,始终假定为原生窗口行为。Electron 通过 Playwright 以
noDefaultViewport 启动,所以把它当作真实的桌面窗口,在调整任何尺寸之前先检查启动时的大小和布局。
- 当签收同时依赖布局断点和真实桌面行为时,两轮都做:先用显式视口做确定性 QA,再用原生窗口验证做最终的环境相关检查。
- 把切换模式当作一次 context 重置。不要把视口模拟的
context 复用到原生窗口一轮,反之亦然;关闭旧的 page 和 context,再为新模式创建新的。
启动或复用 Web 会话
桌面与移动 web 会话共享同一个 browser、辅助函数和 QA 流程。主要区别在于你创建哪对 context 和 page。
桌面 Web Context
把 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 并重新运行该 cell。
移动 Web Context
当 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 并重新运行该 cell。
原生窗口 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 并重新运行该 cell。
如果你已经有一个 Electron 会话,但在主进程、preload 或启动变更后需要一个全新进程,改用下一节的重启 cell,而不是重新运行这个。
迭代期间复用会话
尽可能让同一个会话保持存活。
Web 渲染层重载:
await reloadWebContexts();
Electron 仅渲染层重载:
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");
主进程、preload 或启动变更后的 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 cell 简短,专注于一次交互爆发。
- 复用已有的顶层绑定(
browser、context、page、electronApp、appWindow),而不是重新声明它们。
- 如果你需要隔离,在同一个浏览器内创建一个新页面或新 context。
- 对于 Electron,只在做主进程检查或专用诊断时使用
electronApp.evaluate(...)。
- 就地修复辅助函数错误;除非内核真的坏了,否则不要重置 REPL。
检查清单
会话循环
- 引导
js_repl 一次,然后在多次迭代之间保持同一批 Playwright 句柄存活。
- 从当前工作区启动目标运行时。
- 做代码变更。
- 用适合该变更的正确路径重载或重启。
- 如果探索揭示了额外的控件、状态或可见声明,更新共享的 QA 清单。
- 重新运行功能 QA。
- 重新运行视觉 QA。
- 仅在当前状态就是你要评估的那个状态时,才捕获最终产物。
重载决策
- 仅渲染层变更:重载已有的页面或 Electron 窗口。
- 主进程、preload 或启动变更:重启 Electron。
- 对进程归属或启动代码有新的不确定:重启而非猜测。
功能 QA
- 用真实的用户控件做签收:键盘、鼠标、点击、触摸,或等价的 Playwright 输入 API。
- 至少验证一条端到端的关键流程。
- 确认该流程的可见结果,而不仅仅是内部状态。
- 对实时或大量动画的应用,在真实的交互时序下验证行为。
- 按共享的 QA 清单逐项推进,而不是临时的抽查。
- 在签收前,把每个明显的可见控件至少覆盖一次,而不只是主顺利路径。
- 对清单中的可逆控件或有状态切换,测试完整循环:初始状态、变更后状态、以及回到初始状态。
- 脚本化检查通过后,用正常输入做 30-90 秒的短暂探索,而不是只走既定路径。
- 如果探索揭示了新的状态、控件或声明,把它加入共享 QA 清单,并在签收前覆盖它。
page.evaluate(...) 和 electronApp.evaluate(...) 可以检查或预置状态,但它们不算作签收输入。
视觉 QA
- 把视觉 QA 与功能 QA 分开对待。
- 使用测试前定义、并在 QA 期间更新的那份共享 QA 清单;不要从另一份隐含的清单开始视觉覆盖。
- 重述用户可见声明并逐一显式验证;不要假定功能通过就证明了视觉声明。
- 一个用户可见声明只有在它本应被感知的那个具体状态里被检查过后,才算签收。
- 滚动之前先检查初始视口。
- 确认初始视图在视觉上明显支撑界面的主要声明;如果一个承诺的核心元素在那里无法清楚感知,把它当作 bug。
- 检查所有必需的可见区域,而不只是主交互界面。
- 检查共享 QA 清单中已枚举的状态和模式,当任务是交互式时,至少包括一个有意义的交互后状态。
- 如果动效或过渡是体验的一部分,除了检查稳定后的端点,还要至少检查一个过渡中的状态。
- 如果标签、覆盖层、注释、引导或高亮本应跟随变化的内容,在相关状态变更后验证这种关系。
- 对动态或依赖交互的视觉,检查足够长的时间以判断稳定性、层叠和可读性;不要依赖单张截图做签收。
- 对加载或交互后可能变得更密集的界面,检查你在 QA 期间能达到的最密集的真实状态,而不只是空白、加载中或折叠状态。
- 如果产品有定义的最小支持视口或窗口尺寸,在那里单独跑一轮视觉 QA;否则,选择一个较小但仍真实的尺寸并显式检查它。
- 区分"存在"与"实现":如果一个意图中的可供性技术上存在,但因对比度弱、遮挡、裁切或不稳定而无法清楚感知,把它当作视觉失败。
- 如果在你评估的状态下,任何必需的可见区域被裁切、切掉、遮挡或推到视口外,即使页面级滚动指标看起来可接受,也把它当作 bug。
- 检查是否有裁切、溢出、变形、布局失衡、间距不一致、对齐问题、文字难读、对比度弱、层叠破坏和别扭的运动状态。
- 既评判美学质量也评判正确性。UI 应当为该任务显得有意图、连贯、视觉上令人愉悦。
- 签收优先用视口截图。仅把整页捕获作为次要的调试产物,当某个区域需要更近的检查时捕获一张聚焦截图。
- 如果运动使截图含糊,短暂等待 UI 稳定,然后捕获你真正在评估的那张图。
- 签收前,明确自问:这个界面还有哪些可见部分我尚未仔细检查?
- 签收前,明确自问:如果用户仔细看,最可能让这个结果难堪的可见缺陷是什么?
签收
- 功能路径在正常用户输入下通过了。
- 覆盖是针对共享 QA 清单显式的:记下哪些需求、已实现特性、控件、状态和声明被执行过,并指出任何有意的排除。
- 视觉 QA 一轮覆盖了整个相关界面。
- 每个用户可见声明都有一个匹配的视觉检查,以及从该声明重要的那个状态与视口或窗口尺寸下审阅过的截图产物。
- 视口适配检查在意图中的初始视图以及任何必需的最小支持视口或窗口尺寸下通过。
- 如果产品在窗口中启动,在任何手动调整尺寸或重新定位之前,检查了启动时的大小、位置和初始布局。
- UI 不只是能用;它在视觉上连贯,且对该任务不显得美学薄弱。
- 功能正确性、视口适配和视觉质量必须各自独立通过;一个不蕴含其他。
- 对交互式产品完成了一轮短暂探索,且响应中提到了那一轮覆盖了什么。
- 如果截图审阅与数值检查在任何时候有分歧,在签收前调查了这个差异;截图中可见的裁切是必须解决的失败,而非指标可以推翻的。
- 包含一份简短的负向确认,说明你检查过、且未发现的主要缺陷类别。
- 已执行清理,或你有意保持会话存活以便进一步工作。
截图示例
如果你打算通过 codex.emitImage(...) 发出截图,默认使用下一节中经过 CSS 归一化的路径。那些是将被模型解读、或用于基于坐标的后续动作的截图的标准示例。仅把原始捕获作为对保真度敏感的调试的例外;原始例外的示例出现在归一化指导之后。
绑定模型的截图(默认)
如果你将用 codex.emitImage(...) 发出截图供模型解读,在发出前把它归一化为你所捕获的确切区域的 CSS 像素。这能让返回的坐标与 Playwright CSS 像素对齐(如果回复后来被用于点击),也能减小图像负载大小和模型 token 成本。
默认不要发出原始的原生窗口截图。只有在你明确需要设备像素保真度时才跳过归一化,例如 Retina 或 DPI 伪影调试、像素精确的渲染检查,或其他原始像素比负载大小更重要的保真度敏感场景。对于不会发给模型的仅本地检查,原始捕获没问题。
不要假定 page.screenshot({ scale: "css" }) 在原生窗口模式(viewport: null)下就够了。在 macOS Retina 显示器上的 Chromium 中,即使请求了 scale: "css",headed 原生窗口截图仍可能以设备像素尺寸返回。同样的注意事项适用于通过 Playwright 启动的 Electron 窗口,因为 Electron 以 noDefaultViewport 运行,appWindow.screenshot({ scale: "css" }) 可能仍返回设备像素输出。
对 web 页面和 Electron 窗口使用不同的归一化路径:
- Web:直接优先使用
page.screenshot({ scale: "css" })。如果原生窗口 Chromium 仍返回设备像素输出,在当前页面内用 canvas 重新调整尺寸;不需要临时页面。
- Electron:不要用
appWindow.context().newPage() 或 electronApp.context().newPage() 作为临时页面。Electron context 不可靠地支持那条路径。在主进程中用 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
);
};
- 对 web 使用
page 或 mobilePage,对 Electron 使用 appWindow,作为 surface。
- 把
clip 当作渲染层中 getBoundingClientRect() 的 CSS 像素。
- 除非明确需要无损保真度,否则优先用
quality: 85 的 JPEG。
- 对全图捕获,直接使用返回的
{ x, y }。
- 对裁剪捕获,点击时加回裁剪原点。
Web CSS 归一化
显式视口 context 首选的 web 路径,通常也适用于一般的 web:
await emitWebJpeg(page);
移动 web 使用同样的路径;把 page 替换为 mobilePage:
await emitWebJpeg(mobilePage);
如果模型返回 { x, y },直接点击它:
await clickCssPoint({ surface: page, x, y });
移动 web 点击路径:
await tapCssPoint({ page: mobilePage, x, y });
对于这条正常路径中的 web 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" 仍以设备像素尺寸返回时的 web 原生窗口后备方案:
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 context 已在运行后的移动原始发出:
await codex.emitImage({
bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
视口适配检查(必需)
不要仅因为主控件可见就假定截图可接受。在签收前,用截图审阅和数值检查两者,显式验证意图中的初始视图符合产品要求。
- 在签收前定义意图中的初始视图。对可滚动页面,这是首屏(above-the-fold)体验。对类应用的外壳、游戏、编辑器、仪表盘或工具,这是完整的交互界面加上使用它所需的控件和状态。
- 用截图作为适配的主要证据。数值检查辅助截图;它们不能推翻可见的裁切。
- 在意图中的初始视图里,如果任何必需的可见区域被裁切、切掉、遮挡或推到视口外,即使页面级滚动指标看起来可接受,签收也失败。
- 当产品被设计为滚动,且初始视图仍传达核心体验并暴露主要行动号召或必需的起始上下文时,滚动是可接受的。
- 对固定外壳界面,如果需要滚动才能到达主交互界面的某部分或必要控件,滚动不是可接受的变通。
- 不要仅依赖文档滚动指标。固定高度的外壳、内部窗格和隐藏溢出的容器可能裁切必需 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 调试,从 js_repl 通过 _electron.launch(...) 启动应用,这样同一个会话拥有该进程。如果 Electron 渲染层依赖单独的开发服务器(例如 Vite 或 Next),把该服务器保持运行在持久化 TTY 会话中,然后从 js_repl 重启或重载 Electron 应用。
清理
仅在任务真正完成时才运行清理:
- 这个清理是手动的。退出 Codex、关闭终端或丢失
js_repl 会话都不会隐式运行 electronApp.close()、context.close() 或 browser.close()。
- 特别对 Electron,假定如果你在未先执行清理 cell 就离开会话,应用可能继续运行。
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,先运行清理 cell 并等到 "Playwright session closed" 日志出现后再退出。
常见失败模式
Cannot find module 'playwright':在当前工作区运行一次性设置,并在使用 js_repl 之前验证 import。
- 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 超时或重置:重新运行引导 cell,用更短、更聚焦的 cell 重建会话。
- 浏览器启动或网络操作立即失败:确认会话是用
--sandbox danger-full-access 启动的,并在需要时以该方式重启。