Playwright Interactive Skill
Use this skill when a task needs interactive browser or Electron work in a persistent
session. Keep the Playwright handles alive across code edits, reloads, and repeated checks so iteration stays fast.
Preconditions
- must be enabled for this skill.
- If is missing, enable it in :
toml
[features]
js_repl = true
- You can also start a new session with (equivalent to ).
- After enabling , start a new Codex session so the tool list refreshes.
- For now, run this workflow with sandboxing disabled: start Codex with
--sandbox danger-full-access
(or the equivalent config for sandbox_mode=danger-full-access
). This is a temporary requirement while + Playwright support inside the sandbox is still being completed.
- Run setup from the same project directory you need to debug.
- Treat as a recovery tool, not routine cleanup. Resetting the kernel destroys your Playwright handles.
One-time setup
bash
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.
Core Workflow
- Write a brief QA inventory before testing:
- Build the inventory from three sources: the user's requested requirements, the user-visible features or behaviors you actually implemented, and the claims you expect to make in the final response.
- Anything that appears in any of those three sources must map to at least one QA check before signoff.
- List the user-visible claims you intend to sign off on.
- List every meaningful user-facing control, mode switch, or implemented interactive behavior.
- List the state changes or view changes each control or implemented behavior can cause.
- Use this as the shared coverage list for both functional QA and visual QA.
- For each claim or control-state pair, note the intended functional check, the specific state where the visual check must happen, and the evidence you expect to capture.
- If a requirement is visually central but subjective, convert it into an observable QA check instead of leaving it implicit.
- Add at least 2 exploratory or off-happy-path scenarios that could expose fragile behavior.
- Run the bootstrap cell once.
- Start or confirm any required dev server in a persistent TTY session.
- Launch the correct runtime and keep reusing the same Playwright handles.
- After each code change, reload for renderer-only changes or relaunch for main-process/startup changes.
- Run functional QA with normal user input.
- Run a separate visual QA pass.
- Verify viewport fit and capture the screenshots needed to support your claims.
- Clean up the Playwright session only when the task is actually finished.
Bootstrap (Run Once)
javascript
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}`
);
}
Start or Reuse Web Session
Set
to the app you are debugging. For local servers, prefer
over
.
javascript
const TARGET_URL = "http://127.0.0.1:3000";
if (!browser) {
browser = await chromium.launch({ headless: false });
}
if (!context) {
context = await browser.newContext({
viewport: { width: 1600, height: 900 },
});
}
if (!page) {
page = await context.newPage();
}
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded:", await page.title());
Start or Reuse Electron Session
Set
to
when the current workspace is the Electron app and
points
to the right entry file. If you need to target a specific main-process file directly, use a path such as
instead.
javascript
const ELECTRON_ENTRY = ".";
if (electronApp) {
await electronApp.close().catch(() => {});
}
electronApp = await electronLauncher.launch({
args: [ELECTRON_ENTRY],
cwd: process.cwd(),
});
appWindow = await electronApp.firstWindow();
console.log("Loaded Electron window:", await appWindow.title());
Reuse Sessions During Iteration
Keep the same session alive whenever you can.
Web renderer reload:
javascript
for (const p of context.pages()) {
await p.reload({ waitUntil: "domcontentloaded" });
}
console.log("Reloaded existing tabs");
Electron renderer-only reload:
javascript
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");
Electron restart after main-process, preload, or startup changes:
javascript
await electronApp.close().catch(() => {});
electronApp = await electronLauncher.launch({
args: ["."],
cwd: process.cwd(),
});
appWindow = await electronApp.firstWindow();
console.log("Relaunched Electron window:", await appWindow.title());
Default posture:
- Keep each cell short and focused on one interaction burst.
- Reuse the existing top-level bindings (, , , , ) instead of redeclaring them.
- If you need isolation, create a new page or a new context inside the same browser.
- For Electron, use
electronApp.evaluate(...)
only for main-process inspection or purpose-built diagnostics.
- Fix helper mistakes in place; do not reset the REPL unless the kernel is actually broken.
Checklists
Session Loop
- Bootstrap once, then keep the same Playwright handles alive across iterations.
- Launch the target runtime from the current workspace.
- Make the code change.
- Reload or relaunch using the correct path for that change.
- Update the shared QA inventory if exploration reveals an additional control, state, or visible claim.
- Re-run functional QA.
- Re-run visual QA.
- Capture final artifacts only after the current state is the one you are evaluating.
- Execute cleanup before ending the task or leaving the session.
Reload Decision
- Renderer-only change: reload the existing page or Electron window.
- Main-process, preload, or startup change: relaunch Electron.
- New uncertainty about process ownership or startup code: relaunch instead of guessing.
Functional QA
- Use real user controls for signoff: keyboard, mouse, click, touch, or equivalent Playwright input APIs.
- Verify at least one end-to-end critical flow.
- Confirm the visible result of that flow, not just internal state.
- For realtime or animation-heavy apps, verify behavior under actual interaction timing.
- Work through the shared QA inventory rather than ad hoc spot checks.
- Cover every obvious visible control at least once before signoff, not only the main happy path.
- For reversible controls or stateful toggles in the inventory, test the full cycle: initial state, changed state, and return to the initial state.
- After the scripted checks pass, do a short exploratory pass using normal input for 30-90 seconds instead of following only the intended path.
- If the exploratory pass reveals a new state, control, or claim, add it to the shared QA inventory and cover it before signoff.
- and
electronApp.evaluate(...)
may inspect or stage state, but they do not count as signoff input.
Visual QA
- Treat visual QA as separate from functional QA.
- Use the same shared QA inventory defined before testing and updated during QA; do not start visual coverage from a different implicit list.
- Restate the user-visible claims and verify each one explicitly; do not assume a functional pass proves a visual claim.
- A user-visible claim is not signed off until it has been inspected in the specific state where it is meant to be perceived.
- Inspect the initial viewport before scrolling.
- Confirm that the initial view visibly supports the interface's primary claims; if a core promised element is not clearly perceptible there, treat that as a bug.
- Inspect all required visible regions, not just the main interaction surface.
- Inspect the states and modes already enumerated in the shared QA inventory, including at least one meaningful post-interaction state when the task is interactive.
- If motion or transitions are part of the experience, inspect at least one in-transition state in addition to the settled endpoints.
- If labels, overlays, annotations, guides, or highlights are meant to track changing content, verify that relationship after the relevant state change.
- For dynamic or interaction-dependent visuals, inspect long enough to judge stability, layering, and readability; do not rely on a single screenshot for signoff.
- For interfaces that can become denser after loading or interaction, inspect the densest realistic state you can reach during QA, not only the empty, loading, or collapsed state.
- If the product has a defined minimum supported viewport or window size, run a separate visual QA pass there; otherwise, choose a smaller but still realistic size and inspect it explicitly.
- Distinguish presence from implementation: if an intended affordance is technically there but not clearly perceptible because of weak contrast, occlusion, clipping, or instability, treat that as a visual failure.
- If any required visible region is clipped, cut off, obscured, or pushed outside the viewport in the state you are evaluating, treat that as a bug even if page-level scroll metrics appear acceptable.
- Look for clipping, overflow, distortion, layout imbalance, inconsistent spacing, alignment problems, illegible text, weak contrast, broken layering, and awkward motion states.
- Judge aesthetic quality as well as correctness. The UI should feel intentional, coherent, and visually pleasing for the task.
- Prefer viewport screenshots for signoff. Use full-page captures only as secondary debugging artifacts.
- If the full-window screenshot is not enough to judge a region confidently, capture a focused screenshot for that region.
- If motion makes a screenshot ambiguous, wait briefly for the UI to settle, then capture the image you are actually evaluating.
- Before signoff, explicitly ask: what visible part of this interface have I not yet inspected closely?
- Before signoff, explicitly ask: what visible defect would most likely embarrass this result if the user looked closely?
Signoff
- The functional path passed with normal user input.
- Coverage is explicit against the shared QA inventory: note which requirements, implemented features, controls, states, and claims were exercised, and call out any intentional exclusions.
- The visual QA pass covered the whole relevant interface.
- Each user-visible claim has a matching visual check and artifact from the state where that claim matters.
- The viewport-fit checks passed for the intended initial view and any required minimum supported viewport or window size.
- If the product launches in a window, the as-launched size, placement, and initial layout were checked before any manual resize or repositioning.
- The screenshots directly support the claims you are making.
- The required screenshots were reviewed for the relevant states and viewport or window sizes established during QA.
- The UI is not just functional; it is visually coherent and not aesthetically weak for the task.
- Functional correctness, viewport fit, and visual quality must each pass on their own; one does not imply the others.
- A short exploratory pass was completed for interactive products, and the response mentions what that pass covered.
- If screenshot review and numeric checks disagreed at any point, the discrepancy was investigated before signoff; visible clipping in screenshots is a failure to resolve, not something metrics can overrule.
- Include a brief negative confirmation of the main defect classes you checked for and did not find.
- Cleanup was executed, or you intentionally kept the session alive for further work.
Screenshot Examples
Prefer JPEG at
for
artifacts unless lossless inspection is specifically required.
Desktop example:
javascript
const { unlink } = await import("node:fs/promises");
const desktopPath = `${codex.tmpDir}/desktop.jpg`;
await page.screenshot({ path: desktopPath, type: "jpeg", quality: 85 });
await codex.tool("view_image", { path: desktopPath });
await unlink(desktopPath).catch(() => {});
Electron example:
javascript
const { unlink } = await import("node:fs/promises");
const electronPath = `${codex.tmpDir}/electron-window.jpg`;
await appWindow.screenshot({ path: electronPath, type: "jpeg", quality: 85 });
await codex.tool("view_image", { path: electronPath });
await unlink(electronPath).catch(() => {});
Mobile example:
javascript
const { unlink } = await import("node:fs/promises");
if (!mobileContext) {
mobileContext = await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
});
mobilePage = await mobileContext.newPage();
}
await mobilePage.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
const mobilePath = `${codex.tmpDir}/mobile.jpg`;
await mobilePage.screenshot({ path: mobilePath, type: "jpeg", quality: 85 });
await codex.tool("view_image", { path: mobilePath });
await unlink(mobilePath).catch(() => {});
Viewport Fit Checks (Required)
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.
- Define the intended initial view before signoff. For scrollable pages, this is the above-the-fold experience. For app-like shells, games, editors, dashboards, or tools, this is the full interactive surface plus the controls and status needed to use it.
- Use screenshots as the primary evidence for fit. Numeric checks support the screenshots; they do not overrule visible clipping.
- Signoff fails if any required visible region is clipped, cut off, obscured, or pushed outside the viewport in the intended initial view, even if page-level scroll metrics appear acceptable.
- Scrolling is acceptable when the product is designed to scroll and the initial view still communicates the core experience and exposes the primary call to action or required starting context.
- For fixed-shell interfaces, scrolling is not an acceptable workaround if it is needed to reach part of the primary interactive surface or essential controls.
- Do not rely on document scroll metrics alone. Fixed-height shells, internal panes, and hidden-overflow containers can clip required UI while page-level scroll checks still look clean.
- Check region bounds, not just document bounds. Verify that each required visible region fits within the viewport in the startup state.
- For Electron or desktop apps, verify both the launched window size and placement and the renderer's initial visible layout before any manual resize or repositioning.
- Passing viewport-fit checks only proves that the intended initial view is visible without unintended clipping or scrolling. It does not prove that the UI is visually correct or aesthetically successful.
Web or renderer check:
javascript
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:
javascript
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
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.
Dev Server
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:
Before
, verify the chosen port is listening and the app responds.
For Electron debugging, launch the app from
through
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
.
Cleanup
Only run cleanup when the task is actually finished:
- This cleanup is manual. Exiting Codex, closing the terminal, or losing the session does not implicitly run , , or .
- For Electron specifically, assume the app may keep running if you leave the session without executing the cleanup cell first.
javascript
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.
Common Failure Modes
Cannot find module 'playwright'
: run the one-time setup in the current workspace and verify the import before using .
- Playwright package is installed but the browser executable is missing: run
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 .
- hangs, times out, or exits immediately: verify the local dependency, confirm the 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 only when the kernel is genuinely stuck.
- timed out or reset: rerun the bootstrap cell and recreate the session with shorter, more focused cells.
- Browser launch or network operations fail immediately: confirm the session was started with
--sandbox danger-full-access
and restart that way if needed.