← Blog

BLOG

Playwright Chrome Recorder: Record, Export, and Run Remotely

Playwright Chrome Recorder captures user actions into Playwright scripts. Learn what it generates, its limits, and how to run recordings on hosted Chromium.

September 17, 20269 min readRemote Browser

# Playwright Chrome Recorder: Record, Export, and Run Remotely

The Playwright Chrome Recorder is a browser extension that captures clicks, typing, and navigation in a real Chrome tab and exports them as a Playwright test script. It is the fastest way to get a working selector for a stubborn element, and a reasonable way to bootstrap a test you will later harden. It is not a test generator you can ship unedited, and it does not run anything for you — it produces code you still have to execute somewhere.

That last point matters more than it used to. Once a recording exists, the question shifts from "how do I capture this flow" to "where does this script actually run." Locally, you are managing Chrome versions, driver binaries, and a machine that has to stay awake. On a hosted runtime, you connect over CDP and the browser lives somewhere else. This guide covers the recorder itself, what its output is worth, and how to point the resulting script at a remote Chromium session.

What the Playwright Chrome Recorder Actually Does

The recorder ships as a Chrome extension. You open it against the tab you want to capture, hit record, and interact with the page normally. Each action becomes a line in a generated script. When you stop, you can copy the output as a Playwright test, a Puppeteer script, or a set of raw actions.

The extension is maintained alongside Playwright, so the generated code tracks current API conventions rather than the older page.$eval style you see in stale tutorials. Selectors are the interesting part: the recorder prefers role-based and text-based locators (getByRole, getByLabel, getByText) over brittle CSS paths, which is a meaningful improvement over naive recording tools that emit div > div:nth-child(3) > span.

What it captures:

  • Clicks and taps on interactive elements, with a best-effort accessible-name selector.
  • Text input, including per-field values as you type them.
  • Navigation, including form submissions that trigger page loads.
  • Assertions, if you manually add them through the recorder UI — visibility, text content, and value checks.

What it does not capture: network interception, multi-tab or multi-context flows, file uploads in every case, iframe interactions reliably, and anything involving authentication state you set up outside the recorded tab. It also records what you did, not what you meant. If you clicked a button twice because the first click was slow, you get two clicks.

Reading the Generated Script Critically

A recorded script is a draft. Treat the output as a starting point and expect to edit it before it goes into CI.

Selectors degrade. The recorder picks the best locator it can find at record time. If the page had a stable data-testid, great. If it fell back to visible text, that text is now a coupling point — a copy change breaks your test. Review every locator and replace text-based ones with stable attributes where the application exposes them.

Timing is implicit. Recorded scripts rely on Playwright's auto-waiting, which handles most cases but not all. Flows with debounced search, optimistic UI updates, or animations that settle late will need explicit expect(...).toBeVisible() assertions rather than a bare click.

State is missing. The recording starts from whatever state your browser was in. If you were logged in, the script assumes you are logged in. There is no storageState setup, no fixture, no teardown. You have to add that.

Values are hardcoded. Credentials, search terms, and IDs get baked in as literals. Parameterize them before the script touches a shared environment.

A practical workflow: record the flow, extract the selectors you could not figure out by hand, then rewrite the script around your own fixtures and page objects. The recorder's real value is selector discovery, not test authoring.

From Recording to Execution: Where the Script Runs

Here is where most teams hit friction. A recorded script is a Playwright script, and Playwright scripts need a browser. Three options, with real trade-offs:

ApproachSetup costIsolationScalingBest for
Local Chrome + playwright installLow initially, recurring per machineNone — shares your desktop sessionManual, one machineDebugging, one-off selector capture
Self-hosted Playwright in containersHigh — image builds, driver pinning, orchestrationPer containerRequires scheduler and capacity planningTeams with existing infra and ops headcount
Hosted Chromium over CDPLow — one connection stringPer sessionHandled by the providerCI, agents, parallel test runs

The local path is fine for recording. It stops being fine when the script needs to run on a schedule, in CI, or from a machine that is not your laptop. The self-hosted path works but the maintenance is real: Chromium updates break driver compatibility, container images drift, and you end up owning a browser fleet.

The hosted path replaces all of that with a WebSocket endpoint. You call chromium.connectOverCDP(endpoint) instead of chromium.launch(), and the rest of your script is unchanged. Remote Browser exposes exactly this — hosted Chromium sessions with CDP access, Playwright/Puppeteer/Selenium compatibility, persistent profiles, and a live viewer for debugging. You can see the connection model in the documentation.

Connecting a Recorded Script to Remote Chromium

The migration from local to remote is a two-line change. Here is a TypeScript example that takes a recorded flow and runs it against a hosted session:

import { chromium, Browser, Page } from 'playwright';

const CDP_ENDPOINT = process.env.REMOTE_BROWSER_WS!;

async function runRecordedFlow(): Promise<void> {
  // connectOverCDP attaches to an existing browser instead of launching one.
  const browser: Browser = await chromium.connectOverCDP(CDP_ENDPOINT);

  // A remote session may already have a context; reuse it if so.
  const context = browser.contexts()[0] ?? (await browser.newContext());
  const page: Page = context.pages()[0] ?? (await context.newPage());

  try {
    await page.goto('https://example.com/login');

    // Locators below are what the recorder would emit, hardened by hand.
    await page.getByLabel('Email').fill(process.env.TEST_EMAIL!);
    await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();

    // Replace implicit waits with explicit assertions.
    await page.getByRole('heading', { name: 'Dashboard' }).waitFor();

    await page.getByRole('link', { name: 'Reports' }).click();
    await page.getByRole('button', { name: 'Export CSV' }).click();

    const download = await page.waitForEvent('download');
    console.log('Saved to:', await download.path());
  } finally {
    // Close the connection, not the remote browser, unless you own its lifecycle.
    await browser.close();
  }
}

runRecordedFlow().catch((err) => {
  console.error(err);
  process.exit(1);
});

Three details worth noting. First, connectOverCDP is Chromium-only — it will not attach to Firefox or WebKit, so if your recorded flow needs cross-browser coverage you will run the local-launch path for those engines. Second, browser.contexts()[0] may already exist on a remote session; creating a second context is fine but changes your isolation semantics. Third, closing the browser object closes the CDP connection. Whether that tears down the remote session depends on the provider's lifecycle rules, so check before you rely on it.

If you are still deciding between attaching to an existing browser and launching a fresh one, the trade-offs are covered in Playwright attach to existing browser.

Production Criteria for Recorded Flows

A recording that works once is not a test. Before a recorded flow runs unattended, it needs to survive these conditions:

Authentication. Recorded flows assume a logged-in state. Move login into a setup step that writes storageState, then reuse it across tests. On a hosted runtime, persistent profiles let you keep that state across sessions without re-authenticating every run.

Parallelism. Recorded scripts often share a single browser context, which serializes them. Give each test its own context or session so runs do not collide on cookies or local storage. Session isolation is a runtime property, not something you can patch in the script.

Flakiness. The most common cause is a selector that matched something incidental at record time. The second most common is a missing wait. Both are fixable by editing the script, not by adding retries — retries hide the problem and inflate your runtime bill.

Observability. When a recorded flow fails in CI, you need the trace, the screenshot, and ideally a live view of the session. Playwright's trace viewer covers the first two. For the third, a live viewer attached to the remote session lets you watch the run as it happens rather than reconstructing it afterward.

Cost control. Browser time is metered on hosted runtimes. A recorded flow with generous waitForTimeout calls burns session minutes doing nothing. Replace fixed sleeps with event-based waits and your bill drops accordingly. Current rates are on the pricing page.

Recorder vs. Hand-Written vs. Agent-Driven

The recorder is one of three ways to produce a browser script, and it is not always the right one.

Use the recorder when you are reverse-engineering a complex UI, the selectors are non-obvious, or you need a quick smoke test for a flow you already understand.

Write by hand when the flow has branching logic, data-driven steps, or assertions that depend on computed values. Recorded scripts do not express conditionals well, and retrofitting them is more work than starting clean.

Use an agent when the flow is exploratory or changes frequently — the agent reads the page and decides what to click, so selector drift does not break it the way it breaks a recorded script. Agents still need a browser to run in, and the runtime requirements are similar: CDP access, session isolation, and a way to observe what happened. That overlap is why the same hosted runtime serves both recorded tests and agent workloads; see Remote Browser for AI agents.

A hybrid works well in practice: record once to discover selectors, hand-write the production test around them, and let an agent handle the flows that are too volatile to script.

Practical Setup Notes

A few things that save time when you wire the recorder into a real workflow:

  • Pin the extension version in your team's browser profile so everyone generates code against the same Playwright release.
  • Record against a staging environment, not production, so your captured values are not real customer data.
  • Strip credentials immediately. The recorder captures what you type. If you typed a real password, it is in the output.
  • Keep recordings out of version control until they are rewritten. A raw recording in a PR invites review comments about selectors that will not survive the first edit.
  • Test the remote connection before the flow. A connectOverCDP call that fails on a bad endpoint looks like a test failure if you do not separate the two.

For the connection layer specifically, the Playwright CDP documentation is the authoritative reference on connectOverCDP behavior, including the Chromium-only constraint and how contexts are exposed on an attached browser.

Where This Leaves You

The Playwright Chrome Recorder solves a narrow, real problem: turning a manual interaction into a locator you can trust. It does not solve execution, isolation, or maintenance, and treating it as a test-generation tool leads to brittle suites.

The productive pattern is to use the recorder for what it is good at — selector discovery — and invest your effort in the runtime and the test structure around it. If your scripts need to run on a schedule, in parallel, or from a machine that is not your laptop, a hosted Chromium session removes the browser-management work without changing your Playwright code. Start with the documentation to see the connection model, and check pricing before you plan capacity.