Files
Tommy D. Rossi 1b93ba989d docs: note shadcn-ui and Debugger listScripts test flakiness in MEMORY
Both flake locally independent of code changes: shadcn-ui snapshot times
out when the external site loads slower than the 10s exec timeout, and
relay-session listScripts intermittently returns 0 scripts but passes on
retry.

Session: ses_ff500aa18ffeiSe873v4YFxyfA
2026-08-16 20:25:39 +02:00

10 KiB

Memory

Loopback ≠ trusted under tunnel agents (Apr 2026)

Tunnel agents (traforo, ngrok, cloudflared) run as local processes that forward public traffic to localhost:19988. The relay sees every public request as socket.remoteAddress = 127.0.0.1, so any loopback-based auth bypass becomes a full bypass. Always require the token on every request regardless of source; in-process callers must attach it themselves via PLAYWRITER_TOKEN env (set by serve at startup).

Auto-returned values in playwriter CLI: skip useless Playwright handles (Apr 2026)

The CLI auto-returns single-expression code (e.g. await page.goto(url)). Playwright methods return handle objects (Response, Page, Browser, Locator) that are useless to print — they're programmatic references, not display data. The user does NOT want a hint message either — just silently skip.

Rule: if auto-returned value is a ChannelOwner (duck-type: _type + _guid + _connection), emit nothing. The Code executed successfully (no output) fallback handles the "nothing else printed" case.

User must explicitly console.log(response) or return specific fields (return response.url()) to see data. console.log still works safely via the playwright-core custom inspect handler.

Playwright browsers install is broken locally (Apr 2026)

pnpm exec playwright install fails with Command "playwright" not found because this repo uses @xmorse/playwright-core not playwright. The core CLI node playwright/packages/playwright-core/cli.js install chromium also fails with TypeError: onExit is not a function.

Workaround for pnpm test (which needs chromium-1209): symlink the already-installed 1208 build to 1209. Tests launchPersistentContext and work with the slightly older binary:

ln -sf ~/Library/Caches/ms-playwright/chromium-1208 \
       ~/Library/Caches/ms-playwright/chromium-1209
ln -sf ~/Library/Caches/ms-playwright/chromium_headless_shell-1208 \
       ~/Library/Caches/ms-playwright/chromium_headless_shell-1209

For standalone integration tests: pass executablePath: '/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing' to chromium.launch({ headless: true, executablePath }).

Node util.inspect bypasses Proxy traps (Apr 2026)

Node's util.inspect does NOT invoke Proxy get traps when looking up Symbol.for('nodejs.util.inspect.custom'). It reads the symbol property directly from the proxy target object.

To expose a custom inspect handler on a proxied object, the symbol MUST be set on the underlying target, not via the proxy's get trap:

// BAD — never fires
new Proxy(target, {
  get: (obj, prop) => {
    if (prop === Symbol.for('nodejs.util.inspect.custom'))
      return () => 'custom output'
    return obj[prop]
  },
})

// GOOD — works
target[Symbol.for('nodejs.util.inspect.custom')] = function () {
  return 'custom output'
}
const proxy = new Proxy(target, { get: (obj, prop) => obj[prop] })

Direct property access proxy[Symbol.for('nodejs.util.inspect.custom')] DOES go through the proxy get trap — only util.inspect's internal lookup bypasses it.

Popup windows auto-relocated to tabs via chrome.windows.onCreated (Apr 2026)

Implemented in extension 0.0.80 / playwriter 0.0.104. The extension listens for chrome.windows.onCreated where type === 'popup', moves the tab(s) into the main normal window with chrome.tabs.move, and closes the empty popup window. No "windows" permission needed — chrome.windows API is always available to extensions.

After relocating, the tab is NOT debugger-attached automatically — chrome's CDP auto-attach only works for child targets at the CDP protocol layer, not through the chrome.debugger Chrome extension API. So the extension must explicitly call connectTab(tabId) on the relocated tab for it to appear in context.pages() via Playwright.

Heuristic for deciding whether to auto-attach the relocated tab: use store.getState().tabs.size > 0 (any tab already connected) rather than checking the popup's openerTabId. Chrome sets openerTabId=null on popup-window tabs opened via window.open(url, '', 'popup=1,width=...') (verified on Chromium 145). The "anyTabConnected" heuristic is robust because popups opened while the user has Playwriter active on any tab should become controllable.

Listener lives at extension/src/background.ts next to the other chrome.tabs.* / chrome.windows.* listeners near the bottom of the file. Test coverage: playwriter/src/popup-relocation.test.ts uses a local HTTP server with a button that calls window.open(..., 'popup=1'), then asserts the new page appears in context.pages() and no popup-type window remains via chrome.windows.getAll.

Better alternative to openerTabId: chrome.webNavigation.onCreatedNavigationTarget

When you need to reliably know which tab opened a new tab/window in a Chrome extension, DO NOT rely on chrome.tabs.Tab.openerTabId — it is often null for popup windows created via window.open(url, '', 'popup=1,width=...') (Chromium 145 verified). Instead use chrome.webNavigation.onCreatedNavigationTarget which fires with a { sourceTabId, tabId, sourceFrameId, url, timeStamp } details object and reliably provides the source tab ID for every window.open/target=_blank navigation. This also lets you decide WHICH main window to relocate a popup into (the source tab's window, not an arbitrary "first normal" window).

Warning delivery timing in PlaywrightExecutor is fragile

enqueueWarning + flushWarningsForScope at playwriter/src/executor.ts:375-405 only emit warnings whose id > scope.cursor at the moment flushWarningsForScope runs. If a page.on('popup') (or other async) listener enqueues a warning AFTER the execute() call's flush has already run, the warning is delivered but attributed to the NEXT execute() call's scope. If execute() is never called again, the warning is silently lost.

For popup detection: enqueue synchronously inside the page.on('popup') listener so the warning lands BEFORE the current execute() call's flush. If you need info that only becomes available async (like the final URL after navigation), accept that the synchronous warning will have stale data (url: about:blank) and tell the agent to inspect context.pages() for final state.

Oracle review findings for popup relocation (Apr 2026)

Critical bug in first implementation: gated connectTab() on anyTabConnected but forgot to gate tabs.move() / windows.remove(), meaning the extension changed Chrome popup behavior globally even when Playwriter was idle. Always gate the WHOLE relocation side effect, not just the auto-attach step.

anyTabConnected is too broad a heuristic: privacy/context leak if user has playwriter on tab A for site X, then switches to tab B for site Y and B opens a popup — the popup would get auto-attached and exposed to the agent. Correct fix: use webNavigation.onCreatedNavigationTarget to map popup → source tab, then only relocate+attach if the source tab is in store.tabs.

connectTab is not tab-close-safe: if the popup closes while async attach is in flight, the error catch block can write a {state: 'error'} entry for a tabId that onTabRemoved already deleted, leaking dead tabs into store.tabs/badge/group sync. Fix: check chrome.tabs.get(tabId) fails before writing error state.

Focus policy for relocated popups: do NOT call chrome.tabs.update(tabId, {active: true}) or chrome.windows.update(windowId, {focused: true}) after moving the popup tab. Stealing focus from the user's current tab is disruptive. Let the user switch tabs themselves when they notice the new tab appearing next to its opener. The tab is already added at index: -1 (end of tab strip) where the user can see it.

Playwright ChannelOwner leaks process.env via util.inspect (issue #82)

Playwright's ChannelOwner._connection._platform.env is set to process.env on node. At depth 4, util.inspect(response) traverses:

Response → _connection → _platform → env: { ALL_ENV_VARS }

This dumps every environment variable (API keys, tokens, passwords). Happened whenever a user auto-returned a Playwright object from the CLI, e.g. playwriter -s 1 -e 'await page.goto(url)'.

Fix: custom [Symbol.for('nodejs.util.inspect.custom')] on ChannelOwner prototype + on the channel proxy target. See playwright/packages/playwright-core/src/client/channelOwner.ts.

Always-on MAIN world bundle injection from extension (Apr 2026)

To inject a playwriter/dist/*.js bundle into every Playwriter-attached tab at the extension layer: use Page.addScriptToEvaluateOnNewDocument + Runtime.evaluate in attachTab, NOT chrome.scripting.executeScript({ func }). The CDP pair survives hard navigations; executeScript is one-shot.

Inline the bundle via vite ?raw import (import code from '../../playwriter/dist/foo.js?raw'). Works because playwriter builds before the extension in pnpm filter order, and vite/client types in extension tsconfig.json cover the ?raw import.

Node-side state persistence: MAIN-world state is wiped on hard navigation, so anything that must persist across navigations (cursor position, style, hidden flag) has to be stored in a Node-side controller that rehydrates on page.on('framenavigated', frame => frame === page.mainFrame()). The bundle's IIFE-scoped runtime object is fresh on every injection — don't try to make it persist client-side.

Vendored extract-zip needs get-stream 5 (Apr 2026)

playwright-core/src/zipBundle.ts loads a vendored CommonJS extract-zip.js that still does require('get-stream'). Do not bump playwright-core to get-stream@9+ unless the vendored unzip code is rewritten or replaced.

Network.clearBrowserCookies nukes entire profile (Jul 2026)

Network.clearBrowserCookies clears ALL cookies for every domain in the Chrome profile, not just the current page. It wiped Gmail, GitHub, and all authenticated sessions. The relay now blocks this command (and Network.clearBrowserCache). Use Network.getCookies + Network.deleteCookies to clear cookies per-domain instead.

Pre-existing local test failures (Jul 2026)

cli-help.test.ts > unknown command fails on main: goke no longer prints playwriter --help to stderr. extension-connection.test.ts > reconnect after disconnecting everything also fails on clean HEAD (verified via worktree). relay-core.test.ts > shadcn-ui snapshot flakes when ui.shadcn.com loads slower than the 10s exec timeout. relay-session.test.ts > list scripts with Debugger class flakes intermittently (passes on retry). Neither is a regression signal for unrelated changes.