Playwright technical reference

Self-Healing Playwright via connectOverCDP with LLM Selector Fallback

LumaBrowser is a drop-in Chrome DevTools Protocol target for Playwright. Its CDP server runs on port 9222 inside the browser, so chromium.connectOverCDP attaches unchanged — every locator, click, fill, goto, and screenshot keeps working. The Lumabyte.* domain adds LLM-powered selector fallback, and Playwright MCP attaches to the same endpoint via --cdp-endpoint http://127.0.0.1:9222.

Looking for the marketing intro? Start at Self-Healing Selectors — the hub page covers Selenium, Playwright, and Puppeteer side-by-side. This page is the Playwright-specific deep dive.

LumaBrowser vs Playwright at a glance

Playwright is a cross-browser automation library that can drive its own protocol natively or attach to an existing Chromium over CDP. LumaBrowser is the browser itself, exposing CDP on port 9222 and the Lumabyte.* domain for LLM-resolved selectors. chromium.connectOverCDP is the documented path — the same path every cloud-browser provider (Browserless, Browserbase, Steel.dev) uses — so the move is a one-line change.

CapabilityPlaywrightLumaBrowser
Attach methodchromium.launch() or connectOverCDP()chromium.connectOverCDP('http://127.0.0.1:9222')
Chromium binaryDownloaded via npx playwright installNone — LumaBrowser is the browser
Locators, auto-wait, expect(...)Full supportFull support — client-side, unchanged over CDP
LLM selector fallbackNot availableLumabyte.find, Lumabyte.click via newCDPSession
DOM + AX tree + screenshotThree separate CDP callsOne call via Lumabyte.domSnapshot
Firefox / WebKitNativeChromium only — connectOverCDP is Chromium-only by design
Playwright MCP (@playwright/mcp)Bundles its own browserAttaches via --cdp-endpoint http://127.0.0.1:9222
Playwright codegenRecords against its own browserRecord elsewhere, execute against LumaBrowser
Also works with Puppeteer / SeleniumNoYes — same CDP on 9222, separate WebDriver on 9515
MCP server for AI agentsVia @playwright/mcp (separate)Built-in local MCP server
Setupnpm i playwright && npx playwright installnpx lumabrowser start
The drop-in promise

LumaBrowser’s cdp-driver extension is a standards-compliant CDP WebSocket server on port 9222 — the same port and discovery surface Chromium exposes when launched with --remote-debugging-port=9222. Playwright’s chromium.connectOverCDP is the documented path for driving a pre-existing Chromium over CDP. What you don’t have to change:

  • No new library. Keep your existing playwright or @playwright/test install. Works from JavaScript/TypeScript, Python, .NET, and Java bindings — they all speak connectOverCDP.
  • Locators, auto-wait, and the Page API are unchanged. Those are client-side features. The driver just sees the resulting CDP commands, which forward through to Chromium as normal.
  • browser.contexts()[0] resolves on connect. LumaBrowser advertises a default BrowserContext so Playwright’s “attach to the existing context” idiom works immediately.
  • Native CDP is untouched. Network.setUserAgentOverride, Fetch.enable, Emulation.setDeviceMetricsOverride, DOMSnapshot.captureSnapshot — all pass through to Chromium.
  • page.context().newCDPSession(page) still works. That’s how you dispatch custom CDP domains from Playwright — including LumaBrowser’s Lumabyte.* methods.

Just change the connect:

Playwright (classic)
// Launches a bundled Chromium binary.
const { chromium } = require('playwright');

const browser = await chromium.launch({
  headless: false,
});
const context = await browser.newContext();
const page    = await context.newPage();

await page.goto('https://example.com');
console.log(await page.title());

await browser.close();
LumaBrowser
// LumaBrowser is already running. Attach.
const { chromium } = require('playwright');

const browser = await chromium.connectOverCDP(
  'http://127.0.0.1:9222',
);
const context = browser.contexts()[0]
  ?? await browser.newContext();
const page    = await context.newPage();

await page.goto('https://example.com');
console.log(await page.title());

await browser.close();

That’s the entire migration. Change launch(...) to connectOverCDP(url), pick up the implicit default context, and point at port 9222. Everything below is what you gain by opting in.

What’s actually different

Three concrete wins. Each one is opt-in: pay the cost only where you want the benefit.

Win 1: Resilient locators that survive a redesign

Playwright’s locators are already smart (role-based, text-based, test-id-first) — but they still resolve against a concrete DOM. Change a data-testid and getByTestId('submit') breaks the same way any other selector does. LumaBrowser’s Lumabyte.* CDP domain adds a description-based resolver that retries via an LLM using a natural-language description of what the element is, not how it’s currently labeled.

You dispatch it via page.context().newCDPSession(page) — Playwright’s canonical escape hatch for raw CDP.

Playwright (classic)
// Breaks the moment the test-id is renamed
// during a UI refresh.
const { chromium } = require('playwright');

const browser = await chromium.launch();
const page = await (await browser.newContext()).newPage();
await page.goto('https://example.shop/cart');

await page.getByTestId('signup-btn').click();
LumaBrowser
// LLM fallback re-resolves when selectors miss.
const { chromium } = require('playwright');

const browser = await chromium.connectOverCDP(
  'http://127.0.0.1:9222',
);
const context = browser.contexts()[0]
  ?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.shop/cart');

const cdp = await context.newCDPSession(page);
await cdp.send('Lumabyte.configureFallback', {
  enabled: true,
});

// Option A: keep your locator, add a description fallback.
await cdp.send('Lumabyte.click', {
  description: 'the sign-up button',
  selector:    '[data-testid="signup-btn"]',
});

// Option B: skip selectors entirely.
await cdp.send('Lumabyte.click', {
  description: 'the sign-up button',
});
Win 2: One round-trip to read the whole page

Playwright agent loops that need full page state typically make three separate round-trips: DOMSnapshot.captureSnapshot, Accessibility.getFullAXTree, and Page.captureScreenshot. LumaBrowser’s Lumabyte.domSnapshot returns all three in a single call, ordered and correlated server-side — and it does this over the same connectOverCDP session you already have.

This matters especially for Playwright via connectOverCDP: Playwright adds a Node.js driver hop between your client and the browser for every protocol call. Cutting 3 × RTT to 1 × RTT is meaningful when you’re paying two network hops per round-trip.

Playwright (classic)
// Three CDP round-trips, each going client →
// Playwright driver → browser → back.
const cdp = await page.context().newCDPSession(page);
await cdp.send('DOM.enable');
await cdp.send('Accessibility.enable');

const snapshot = await cdp.send(
  'DOMSnapshot.captureSnapshot',
  { computedStyles: [] }
);
const axTree = await cdp.send(
  'Accessibility.getFullAXTree'
);
const { data: screenshot } = await cdp.send(
  'Page.captureScreenshot',
  { format: 'png' }
);
LumaBrowser
// One call, server-correlated payload.
const cdp = await page.context().newCDPSession(page);

const { snapshot, axTree, screenshot } =
  await cdp.send('Lumabyte.domSnapshot', {
    includeAxTree:     true,
    includeScreenshot: true,
  });

// snapshot   — DOMSnapshot.captureSnapshot
// axTree     — Accessibility.getFullAXTree
// screenshot — base64 PNG of the viewport
// 1 request. Parse locally.
Win 3: No bundled Chromium, dedicated automation tabs

Classic Playwright ships a ~170 MB Chromium per-platform binary, downloads it during playwright install, and launches it as a child process for every script run. CI caches drift, --user-data-dir trees pile up, the binary version lags Chrome stable. LumaBrowser’s CDP server runs inside the browser process itself — no binary download, no launcher lifecycle, no temp profile to reap.

And unlike attaching to a user’s regular Chrome (where your automation would fight for the same tabs the user is browsing in), LumaBrowser gives the client dedicated automation tabs: only tabs created with kind: 'cdp' surface as CDP targets, rendered in the tab strip with a purple accent and a CDP badge so you can watch the session drive them live.

Playwright (classic)
// playwright install downloads a bundled
// Chromium and spawns it per run.
const { chromium } = require('playwright');

const browser = await chromium.launch({
  headless: false,
  args: ['--no-sandbox'],
});

// Plus: Chromium version drift, ~170MB
// per-platform binary download during
// `playwright install`, stale --user-data-dir
// trees, and an occasional "browser closed
// unexpectedly" from the launcher at 3am.
LumaBrowser
// LumaBrowser is already running. Attach.
const { chromium } = require('playwright');

const browser = await chromium.connectOverCDP(
  'http://127.0.0.1:9222',
);

// Dedicated "CDP" tabs, visible in the strip.
// No binary download — skip `playwright install`
// for the chromium target.
// No child process.
// No --user-data-dir to clean up.
// The user's regular tabs stay invisible to
// the CDP client, so Network Interceptor and
// AI Chat don't fight for the debugger lock.
Full example: a semantic end-to-end flow

What a realistic flow looks like when you mix Playwright locators, native CDP, and Lumabyte.*.

const { chromium } = require('playwright');

// 1. Attach to the already-running browser.
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const context = browser.contexts()[0] ?? await browser.newContext();
const page    = await context.newPage();

await page.goto('https://example.shop/products/coffee-grinder');

const cdp = await context.newCDPSession(page);

// 2. Opt in to LLM fallback for this session.
await cdp.send('Lumabyte.configureFallback', {
  enabled:            true,
  onFindFail:         true,
  onClickIntercepted: true,
});

// 3. Mix Playwright's native locators with Lumabyte.click for anything
//    where the DOM label is less stable than the intent.
await page.getByRole('button', { name: /sign up/i }).click();  // locator
await cdp.send('Lumabyte.click', { description: 'the add-to-cart button' });
await cdp.send('Lumabyte.click', { description: 'the cart icon in the header' });

// 4. Pull DOM + AX tree + screenshot in one round-trip.
const { snapshot, axTree, screenshot } = await cdp.send('Lumabyte.domSnapshot', {
  includeAxTree:     true,
  includeScreenshot: true,
});

// 5. Playwright's own API still works on top of the same page.
console.log(await page.title());

await browser.close();

Playwright’s locator API for the elements that are stable, description-based resolution for the ones that aren’t, and one round-trip for full page capture when the agent needs it. Same page, same session, no context switching between tools.

Playwright MCP — the 2026 pattern

@playwright/mcp is Microsoft’s MCP server for Playwright — the idiomatic way to give an LLM agent (Claude Desktop, Cursor, OpenClaw, any MCP-compatible host) tool-driven control of a browser. It supports --cdp-endpoint for attaching to an existing browser, which is exactly what LumaBrowser exposes.

# Start LumaBrowser's CDP server (once per launch).
curl -X POST http://localhost:3000/api/cdp/start

# Point Playwright MCP at it. Your LLM agent now drives LumaBrowser.
npx @playwright/mcp --cdp-endpoint http://127.0.0.1:9222

The win: the MCP server’s tool calls land in a browser with built-in LLM selector fallback. When the agent produces a locator that doesn’t quite match, the Lumabyte orchestrator catches it instead of the agent loop crashing. And the agent’s own LLM doesn’t need to regenerate the tool call from scratch.

Compatibility matrix

The full row-by-row matrix — connectOverCDP bootstrap, browser contexts, locators / auto-wait / expect, navigation and input, page.route, screenshots, Playwright MCP via --cdp-endpoint, the additive Lumabyte.* methods, plus the documented limits (Firefox / WebKit, full tracing, codegen, launch()) — lives in the API reference: /apis → CDP Driver section.

How to enable it

Two ways, takes under a minute either way:

1. From the UI

  1. Open Settings → CDP Driver.
  2. (Optional) Check “Start automatically when LumaBrowser launches.”
  3. Click Save & Start.
  4. Point your Playwright client at http://127.0.0.1:9222.

Or start it programmatically: POST /api/cdp/start. Full CDP domain reference, Lumabyte.* method signatures, host/port settings, and MCP tool surface are in the CDP Driver section of the API docs.

FAQ

Does my existing Playwright script just work?

Yes for anything that uses chromium.connectOverCDP. Replace chromium.launch(...) with chromium.connectOverCDP('http://127.0.0.1:9222') and every page, locator, click, fill, screenshot, and Network/Fetch call keeps working. LumaBrowser’s cdp-driver extension implements the Target domain, advertises a default BrowserContext on connect, and forwards every other CDP command straight through Electron’s webContents.debugger to Chromium.

Can I still use Playwright’s auto-wait, locators, and expect assertions?

Yes, unchanged. They’re client-side features of the Playwright driver. The server just sees the resulting CDP commands. If your script depends on getByRole, getByTestId, or expect(locator).toBeVisible(), all of that keeps working against LumaBrowser.

What about playwright codegen?

codegen launches its own Chromium and cannot record against a connectOverCDP target — that’s a Playwright design choice. Record against your own Chromium the way you always have, then point the finished script at LumaBrowser for execution. The generated page.* calls work unchanged.

Does Playwright MCP work?

Yes. Launch @playwright/mcp with --cdp-endpoint http://127.0.0.1:9222 and it attaches to LumaBrowser like any other connectOverCDP client. This is the idiomatic 2026 pattern for LLM agents driving a browser via Playwright — LumaBrowser gives the MCP server a dedicated browser with built-in LLM selector fallback.

What about Puppeteer or Selenium?

Same CDP server for Puppeteer (puppeteer.connect({ browserURL })); separate WebDriver server on port 9515 for Selenium. See Puppeteer, Selenium, or the cross-driver hub.

Ready to try it

Install LumaBrowser, enable the cdp-driver extension, and swap chromium.launch() for chromium.connectOverCDP('http://127.0.0.1:9222'). Every Playwright locator, assertion, and Page API keeps working. The Lumabyte.* domain is there when you want it.