Web Application Testing
anthropics/skills
Tests local web applications with Python Playwright scripts, checking frontend behavior, capturing screenshots and reading browser console logs.
Persistent browser and Electron interaction through jsrepl for fast iterative UI debugging.
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install Haohao-end/openagent playwright-interactive --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .claude/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .claude/skills/playwright-interactive && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .claude/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactiveType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install Haohao-end/openagent playwright-interactive --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .agents/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .agents/skills/playwright-interactive && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .agents/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install Haohao-end/openagent playwright-interactive --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .cursor/skills/playwright-interactive && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .cursor/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/Haohao-end/openagent.git --path api/internal/core/skills/catalog/playwright-interactive--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install Haohao-end/openagent playwright-interactive --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .gemini/skills/playwright-interactive && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .gemini/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install Haohao-end/openagent playwright-interactiveInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .github/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .github/skills/playwright-interactive && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .github/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add Haohao-end/openagent --skill playwright-interactive -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install Haohao-end/openagent playwright-interactive --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/Haohao-end/openagent.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/api/internal/core/skills/catalog/playwright-interactive .opencode/skills/playwright-interactive && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "playwright-interactive" agent skill from https://github.com/Haohao-end/openagent/tree/main/api/internal/core/skills/catalog/playwright-interactive into .opencode/skills/playwright-interactive/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "playwright-interactive", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
playwright-interactivePersistent browser and Electron interaction through jsrepl for fast iterative UI debugging.
Playwright Interactive is an agent skill from Haohao-end/openagent. Persistent browser and Electron interaction through jsrepl for fast iterative UI debugging.
Its SKILL.md is about 7.9k tokens, which your agent loads only when the skill is triggered. The skill folder holds 8 other files, including assets (for example `agents/openai.yaml`, `manifest.yaml` and `skill.md`).
It sits in Testing & QA, covering Browser testing. It works with Playwright. The repository describes itself as: What if OpenAI Deep Research and Dify were one platform? OpenAgent — harness architecture for rapidly building vertical AI agents, with deep reasoning loops, visual workflows… The licence is Apache-2.0.
9 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 4013ab2. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
npmnodenpxFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npm and npx, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Playwright Interactive loads about 7.9k tokens when it runs. Until then it costs about 29 tokens; SKILL.md has 3,179 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from Haohao-end/openagent at commit 4013ab2, republished under its Apache-2.0 licence (© Haohao-end). 3,179 words, ~7,928 tokens.
.claude/skills/playwright-interactive/SKILL.md (or your agent's skills folder). This skill also uses 6 other files; get the full folder from GitHub.Use a persistent js_repl Playwright session to debug local web or Electron apps, keep the same handles alive across iterations, and run functional plus visual QA without restarting the whole toolchain unless the process ownership changed.
js_repl must be enabled for this skill.js_repl is missing, enable it in ~/.codex/config.toml:[features]
js_repl = true--enable js_repl (equivalent to -c features.js_repl=true).js_repl, start a new Codex session so the tool list refreshes.--sandbox danger-full-access (or the equivalent config for sandbox_mode=danger-full-access). This is a temporary requirement while js_repl + Playwright support inside the sandbox is still being completed.js_repl_reset as a recovery tool, not routine cleanup. Resetting the kernel destroys your Playwright handles.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); })"If you switch to a different workspace later, repeat setup there.
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}`
);
}Binding rules:
var for the shared top-level Playwright handles because later js_repl cells reuse them.undefined and rerun the cell instead of adding recovery logic everywhere.page, mobilePage, appWindow) over repeatedly rediscovering pages from the context.Shared web helpers:
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");
};For web apps, use an explicit viewport by default and treat native-window mode as a separate validation pass.
deviceScaleFactor rather than switching straight to native-window mode.viewport: null) for a separate headed pass when you need to validate launched window size, OS-level DPI behavior, browser chrome interactions, or bugs that may depend on the host display configuration.noDefaultViewport, so treat it like a real desktop window and check the as-launched size and layout before resizing anything.context for a native-window pass or vice versa; close the old page and context, then create a new one for the new mode.Desktop and mobile web sessions share the same browser, helpers, and QA flow. The main difference is which context and page pair you create.
Set TARGET_URL to the app you are debugging. For local servers, prefer 127.0.0.1 over 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());If context or page is stale, set context = page = undefined and rerun the cell.
Reuse TARGET_URL when it already exists; otherwise set a mobile target directly.
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());If mobileContext or mobilePage is stale, set mobileContext = mobilePage = undefined and rerun the cell.
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());Set ELECTRON_ENTRY to . when the current workspace is the Electron app and package.json points main to the right entry file. If you need to target a specific main-process file directly, use a path such as ./main.js instead.
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());If js_repl is not already running from the Electron app workspace, pass cwd explicitly when launching.
If the app process looks stale, set electronApp = appWindow = undefined and rerun the cell.
If you already have an Electron session but need a fresh process after a main-process, preload, or startup change, use the restart cell in the next section instead of rerunning this one.
Keep the same session alive whenever you can.
Web renderer reload:
await reloadWebContexts();Electron renderer-only reload:
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");Electron restart after main-process, preload, or startup changes:
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());If your launch requires an explicit cwd, include the same cwd here.
Default posture:
js_repl cell short and focused on one interaction burst.browser, context, page, electronApp, appWindow) instead of redeclaring them.electronApp.evaluate(...) only for main-process inspection or purpose-built diagnostics.js_repl once, then keep the same Playwright handles alive across iterations.page.evaluate(...) and electronApp.evaluate(...) may inspect or stage state, but they do not count as signoff input.If you plan to emit a screenshot through codex.emitImage(...), use the CSS-normalized paths in the next section by default. Those are the canonical examples for screenshots that will be interpreted by the model or used for coordinate-based follow-up actions. Keep raw captures as an exception for fidelity-sensitive debugging only; the raw exception examples appear after the normalization guidance.
If you will emit a screenshot with codex.emitImage(...) for model interpretation, normalize it to CSS pixels for the exact region you captured before emitting. This keeps returned coordinates aligned with Playwright CSS pixels if the reply is later used for clicking, and it also reduces image payload size and model token cost.
Do not emit raw native-window screenshots by default. Skip normalization only when you explicitly need device-pixel fidelity, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or another fidelity-sensitive case where raw pixels matter more than payload size. For local-only inspection that will not be emitted to the model, raw capture is fine.
Do not assume page.screenshot({ scale: "css" }) is enough in native-window mode (viewport: null). In Chromium on macOS Retina displays, headed native-window screenshots can still come back at device-pixel size even when scale: "css" is requested. The same caveat applies to Electron windows launched through Playwright because Electron runs with noDefaultViewport, and appWindow.screenshot({ scale: "css" }) may still return device-pixel output.
Use separate normalization paths for web pages and Electron windows:
page.screenshot({ scale: "css" }) directly. If native-window Chromium still returns device-pixel output, resize inside the current page with canvas; no scratch page is required.appWindow.context().newPage() or electronApp.context().newPage() as a scratch page. Electron contexts do not support that path reliably. Capture in the main process with BrowserWindow.capturePage(...), resize with nativeImage.resize(...), and emit those bytes directly.Shared helpers and conventions:
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 or mobilePage for web, or appWindow for Electron, as the surface.clip as CSS pixels from getBoundingClientRect() in the renderer.quality: 85 unless lossless fidelity is specifically required.{ x, y } directly.Preferred web path for explicit-viewport contexts, and often for web in general:
await emitWebJpeg(page);Mobile web uses the same path; substitute mobilePage for page:
await emitWebJpeg(mobilePage);If the model returns { x, y }, click it directly:
await clickCssPoint({ surface: page, x, y });Mobile web click path:
await tapCssPoint({ page: mobilePage, x, y });For web clip screenshots or element screenshots in this normal path, scale: "css" usually works directly. Add the region origin back when clicking.
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 }) after const box = await locator.boundingBox()Web native-window fallback when scale: "css" still comes back at device-pixel size:
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);
};For a full viewport fallback capture, treat returned { x, y } as direct CSS coordinates:
await emitWebScreenshotCssScaled({ page });
await clickCssPoint({ surface: page, x, y });For a clipped fallback capture, add the clip origin back:
await emitWebScreenshotCssScaled({ page, clip });
await clickCssPoint({ surface: page, clip, x, y });For Electron, normalize in the main process instead of opening a scratch Playwright page. The helper below returns CSS-scaled bytes for the full content area or for a clipped CSS-pixel region. Treat clip as content-area CSS pixels, for example values taken from getBoundingClientRect() in the renderer.
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);
};Full Electron window:
await emitElectronScreenshotCssScaled({ electronApp });
await clickCssPoint({ surface: appWindow, x, y });Clipped Electron region using CSS pixels from the renderer:
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 });Use these only when raw pixels matter more than CSS-coordinate alignment, such as Retina or DPI artifact debugging, pixel-accurate rendering inspection, or other fidelity-sensitive review.
Web desktop raw emit:
await codex.emitImage({
bytes: await page.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});Electron raw emit:
await codex.emitImage({
bytes: await appWindow.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});Mobile raw emit after the mobile web context is already running:
await codex.emitImage({
bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});Do not assume a screenshot is acceptable just because the main widget is visible. Before signoff, explicitly verify that the intended initial view matches the product requirement, using both screenshot review and numeric checks.
Web or renderer check:
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 check:
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,
})));Augment the numeric check with getBoundingClientRect() checks for the required visible regions in your specific UI when clipping is a realistic failure mode; document-level metrics alone are not sufficient for fixed shells.
For local web debugging, keep the app running in a persistent TTY session. Do not rely on one-shot background commands from a short-lived shell.
Use the project's normal start command, for example:
npm startBefore page.goto(...), verify the chosen port is listening and the app responds.
For Electron debugging, launch the app from js_repl through _electron.launch(...) so the same session owns the process. If the Electron renderer depends on a separate dev server (for example Vite or Next), keep that server running in a persistent TTY session and then relaunch or reload the Electron app from js_repl.
Only run cleanup when the task is actually finished:
js_repl session does not implicitly run electronApp.close(), context.close(), or browser.close().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");If you plan to exit Codex immediately after debugging, run the cleanup cell first and wait for the "Playwright session closed" log before quitting.
Cannot find module 'playwright': run the one-time setup in the current workspace and verify the import before using js_repl.npx playwright install chromium.page.goto: net::ERR_CONNECTION_REFUSED: make sure the dev server is still running in a persistent TTY session, recheck the port, and prefer http://127.0.0.1:<port>.electron.launch hangs, times out, or exits immediately: verify the local electron dependency, confirm the args target, and make sure any renderer dev server is already running before launch.Identifier has already been declared: reuse the existing top-level bindings, choose a new name, or wrap the code in { ... }. Use js_repl_reset only when the kernel is genuinely stuck.browserContext.newPage: Protocol error (Target.createTarget): Not supported while working with Electron: do not use appWindow.context().newPage() or electronApp.context().newPage() as a scratch page; use the Electron-specific screenshot normalization flow in the model-bound screenshots section.js_repl timed out or reset: rerun the bootstrap cell and recreate the session with shorter, more focused cells.--sandbox danger-full-access and restart that way if needed.© Haohao-end, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 6 other files (assets) in api/internal/core/skills/catalog/playwright-interactive of Haohao-end/openagent.
Open the folder on GitHubat commit 4013ab2
We found 1 copy of this SKILL.md (exact, near-identical or edited) in other folders, from 1 other GitHub owner. This page covers the copy in Haohao-end/openagent, which our catalogue first saw on October 7, 2026.
Playwright Interactive next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Playwright Interactive this skillHaohao-end/openagent | 808 | 1 repos | ~7.9k | Automated safety check: Pass | Apache-2.0 | |
| Web Application Testinganthropics/skills | 180k | 51 repos | ~966 | Automated safety check: Pass | Apache-2.0 | |
| Playwright CLIsanity-io/sanity | 6.4k | 18 repos | ~1.9k | Automated safety check: Pass | MIT | |
| Write and Verify Playwright Testsappsmithorg/appsmith | 41k | — | ~2.9k | Automated safety check: Notes | Apache-2.0 | |
| playwright-cli Browser Automationgithub/gh-aw | 5.4k | 23 repos | ~2.8k | Automated safety check: Pass | MIT | |
| Cucumber and Playwright E2E Testslanggenius/dify | 158k | — | ~682 | Automated safety check: Pass | Custom licence |
anthropics/skills
Tests local web applications with Python Playwright scripts, checking frontend behavior, capturing screenshots and reading browser console logs.
sanity-io/sanity
Automates browser interactions for web testing, form filling, screenshots, and data extraction.
appsmithorg/appsmith
Writes a Playwright end-to-end test from a prompt, runs it against a live Appsmith deployment and retries with fixes up to three times until it passes.
github/gh-aw
Drives a real browser from the command line with playwright-cli to open pages, interact, mock requests, save state and work with Playwright tests.
langgenius/dify
Guides changes and reviews of the Cucumber and Playwright end-to-end suite under `e2e/`: feature files, step definitions, support code, tags, locators and assertions.
langflow-ai/langflow
Write and review Playwright E2E tests for Langflow. An agent skill from langflow-ai/langflow.
Haohao-end/openagent
Build, scaffold, refactor, and troubleshoot ChatGPT Apps SDK applications that combine an MCP server and widget UI.
Haohao-end/openagent
Migrate supported instruction files, skills, agents, and MCP config into Codex project and global files.
Haohao-end/openagent
A skill your agent uses when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or…
Haohao-end/openagent
A skill your agent uses when the user wants a less default, more original solution for code, design, writing, naming, architecture, debugging, or strategy, especially when progress is stuck.
Haohao-end/openagent
A skill your agent uses when the user wants a distinctive, production-ready frontend interface with clear visual direction, strong hierarchy, and polished interaction details.
Works with
Categories
Persistent browser and Electron interaction through jsrepl for fast iterative UI debugging. Playwright Interactive is an agent skill from Haohao-end/openagent. Persistent browser and Electron interaction through jsrepl for fast iterative UI debugging.
Playwright Interactive fits situations like: tasks that involve Browser testing.
Run `npx skills add Haohao-end/openagent --skill playwright-interactive -a claude-code`. Or copy the skill folder (api/internal/core/skills/catalog/playwright-interactive in Haohao-end/openagent) into .claude/skills/playwright-interactive in your project. Claude Code loads it when a task matches its description.
Run `npx skills add Haohao-end/openagent --skill playwright-interactive -a codex`. Or copy the skill folder (api/internal/core/skills/catalog/playwright-interactive in Haohao-end/openagent) into .agents/skills/playwright-interactive in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add Haohao-end/openagent --skill playwright-interactive -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/playwright-interactive, .gemini/skills/playwright-interactive, .github/skills/playwright-interactive and .opencode/skills/playwright-interactive in your project.
Going by SKILL.md and its folder, Playwright Interactive needs the command-line tools its instructions call (npm, node and npx). Our summary lists: Node.js.
SKILL.md contains no URLs. Its commands use npm and npx, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Playwright Interactive is published under the Apache-2.0 licence (from the LICENSE file in the skill folder). It allows redistribution, so the full SKILL.md is shown on this page.
About 7.9k tokens (SKILL.md is roughly 32k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Playwright Interactive: Web Application Testing (anthropics/skills, 180k stars), Playwright CLI (sanity-io/sanity, 6.4k stars), Write and Verify Playwright Tests (appsmithorg/appsmith, 41k stars) and playwright-cli Browser Automation (github/gh-aw, 5.4k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
Haohao-end (a GitHub user) maintains it in Haohao-end/openagent, which has 808 GitHub stars. The repository holds 6 skills in this directory. The repository was last updated on July 17, 2026.
Source: Haohao-end/openagent on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.