10 Recorder Panel
Chrome DevTools — Recorder Panel
Section titled “Chrome DevTools — Recorder Panel”What the Recorder Panel Is
Section titled “What the Recorder Panel Is”The Recorder panel (introduced in Chrome 97) lets you record, replay, measure, and export user flows directly inside the browser. A “user flow” is a sequence of real interactions — clicks, navigation, form input, scrolling — captured as a structured JSON file that can be replayed on demand, exported to Puppeteer, Playwright, or Selenium, and integrated into CI pipelines.
Unlike manual testing scripts written from scratch, Recorder generates the automation baseline for you. You perform the steps once; the panel captures them with accurate selectors, timing, and assertions. You then refine and reuse that capture as many times as needed.
Key capabilities:
- Record — capture any real interaction sequence in the live page
- Replay — re-execute the recorded flow inside DevTools
- Measure — attach a Lighthouse performance run to a flow and get per-flow LCP, TBT, CLS
- Export — emit JSON, Puppeteer, Playwright, or WebDriver (Selenium) scripts
- Import — reload a previously exported JSON recording
- Edit — add, delete, reorder steps; swap selectors; add assertions
When to Use It
Section titled “When to Use It”| Scenario | How Recorder Helps |
|---|---|
| Regression testing | Record the happy path once; replay before every release to confirm it still works |
| Performance measurement | Attach Lighthouse to a flow and compare LCP/CLS across builds |
| Reproducing bugs | Record the exact steps that trigger a bug and share the JSON with your team |
| Automation baseline | Generate a working Puppeteer or Playwright script without writing boilerplate |
| Accessibility auditing | Use ARIA selectors to verify that flows are navigable by screen readers |
| Onboarding documentation | A JSON recording is a machine-readable spec of a user journey |
UI Layout (ASCII Diagram)
Section titled “UI Layout (ASCII Diagram)”+-------------------------------------------------------+| DevTools [Elements][Console][Sources][Network] ... || [Recorder] |+-------------------------------------------------------+| RECORDER [+ New] [...] ||-------------------------------------------------------|| My Flows || > checkout-flow [Play] [Edit] [Export] || > login-flow [Play] [Edit] [Export] ||-------------------------------------------------------|| RECORDING: checkout-flow || || [Stop recording] [Add step] || || 1 navigate https://example.com/cart || 2 click button#checkout || 3 change input[name="email"] "a@b.com" || 4 change input[name="card"] "4111..." || 5 click button[type="submit"] || 6 waitForElement h1.confirmation || || Playback speed: [Normal 1x v] || [Measure performance] [Export v] [Import] |+-------------------------------------------------------+Panel sections:
- Flow list (left sidebar) — all saved recordings; click one to open it
- Step list (main area) — ordered list of captured interactions
- Toolbar — record, play, export, import, measure performance controls
- Step detail (inline expand) — expand any step to edit its selector, value, or timeout
Recording a User Flow — Step by Step
Section titled “Recording a User Flow — Step by Step”1. Open the Recorder Panel
Section titled “1. Open the Recorder Panel”Open DevTools (Cmd+Option+I / F12), then find the Recorder tab. If it is not visible, click the >> overflow arrow in the tab bar.
2. Start a New Recording
Section titled “2. Start a New Recording”Click the + (Create a new recording) button. A dialog asks for a recording name. Use a descriptive name like checkout-guest-flow — this becomes the file name when you export.
Click Start recording. A red dot appears in the panel header, and a recording badge appears on the DevTools icon. Every interaction you make on the page is now captured.
3. Perform Actions
Section titled “3. Perform Actions”Interact with the page exactly as a user would:
- Navigate — type a URL or click links; Recorder captures
navigatesteps automatically - Click — click buttons, links, checkboxes; captured as
clicksteps - Type — fill form fields; captured as
changesteps with the final value - Scroll — scroll the viewport or a scrollable element; captured as
scrollsteps - Submit — pressing Enter or clicking a submit button; captured as
clickorkeyDown - Hover — limited support; see Limitations section
Keep your flow focused. Record one logical journey per file (e.g., “add to cart”, not “entire app”).
4. End the Recording
Section titled “4. End the Recording”Click End recording (the stop button). The red dot disappears. The panel now shows the full step list.
5. Review the Recorded Steps
Section titled “5. Review the Recorded Steps”Scroll through the steps. For each step you can:
- Expand it to see the full selector list and properties
- Delete spurious steps (accidental clicks, extra navigations)
- Check that selectors look stable — prefer ARIA or text selectors over generated class names
The Recording Format (JSON)
Section titled “The Recording Format (JSON)”Every recording is a plain JSON file. Understanding the schema lets you edit recordings by hand, diff them in version control, and generate them programmatically.
Complete Example
Section titled “Complete Example”{ "title": "checkout-guest-flow", "steps": [ { "type": "setViewport", "width": 1280, "height": 720, "deviceScaleFactor": 1, "isMobile": false, "hasTouch": false, "isLandscape": false }, { "type": "navigate", "url": "https://shop.example.com/", "assertedEvents": [ { "type": "navigation", "url": "https://shop.example.com/", "title": "Example Shop" } ] }, { "type": "click", "target": "main", "selectors": [ ["aria/Add to cart[role=\"button\"]"], ["#product-42 button.add-to-cart"], ["xpath///*[@id='product-42']//button[contains(@class,'add-to-cart')]"] ], "offsetX": 64, "offsetY": 18, "assertedEvents": [ { "type": "navigation", "url": "https://shop.example.com/cart", "title": "Your Cart" } ] }, { "type": "click", "target": "main", "selectors": [ ["aria/Proceed to checkout[role=\"link\"]"], ["a.checkout-btn"], ["pierce/.checkout-btn"] ], "offsetX": 50, "offsetY": 14 }, { "type": "change", "target": "main", "selectors": [ ["aria/Email address"], ["input[name=\"email\"]"], ["#guest-email"] ], "value": "test@example.com" }, { "type": "change", "target": "main", "selectors": [ ["aria/Card number"], ["input[name=\"cardNumber\"]"] ], "value": "4111111111111111" }, { "type": "change", "target": "main", "selectors": [ ["aria/Expiry date"], ["input[name=\"expiry\"]"] ], "value": "12/28" }, { "type": "change", "target": "main", "selectors": [ ["aria/CVV"], ["input[name=\"cvv\"]"] ], "value": "123" }, { "type": "scroll", "target": "main", "x": 0, "y": 600 }, { "type": "click", "target": "main", "selectors": [ ["aria/Place order[role=\"button\"]"], ["button[type=\"submit\"].place-order"] ], "offsetX": 80, "offsetY": 20, "assertedEvents": [ { "type": "navigation", "url": "https://shop.example.com/confirmation", "title": "Order Confirmed" } ] }, { "type": "waitForElement", "target": "main", "selectors": [ ["aria/Your order has been placed"], ["h1.confirmation-heading"] ], "operator": ">=", "count": 1, "timeout": 10000 } ]}Step Type Reference
Section titled “Step Type Reference”type | What it Does | Key Properties |
|---|---|---|
setViewport | Sets browser viewport size before the flow starts | width, height, isMobile |
navigate | Loads a URL | url, assertedEvents |
click | Mouse click on an element | selectors, offsetX, offsetY, button |
change | Sets an input/select/textarea value | selectors, value |
keyDown / keyUp | Raw keyboard event | key (e.g. "Enter", "Tab") |
scroll | Scrolls viewport or element | x, y, selectors (if scrollable element) |
waitForElement | Pauses until selector matches | selectors, operator (>=, ==), count, timeout |
waitForExpression | Pauses until JS expression is truthy | expression, timeout |
customStep | Placeholder for extension-supplied logic | name, parameters |
assertedEvents
Section titled “assertedEvents”assertedEvents on a step defines what must happen after the step fires. The most common assertion is a navigation event — if the navigation does not occur within the timeout, replay fails with a clear error. You can add these by hand to any step to make replays more robust.
selectors Array
Section titled “selectors Array”Each element in selectors is itself an array (a selector chain). Recorder tries them in order and uses the first one that resolves to a unique element. Providing multiple selectors of different types makes the recording resilient: if a CSS class changes, the ARIA selector still works.
Selector Types — When Each Is Appropriate
Section titled “Selector Types — When Each Is Appropriate”CSS Selector (default)
Section titled “CSS Selector (default)”["#checkout-form button.submit-btn"]Recorder generates these automatically. They are precise and fast. They break when developers rename classes or restructure the DOM. Treat auto-generated CSS selectors as a starting point, not a final answer.
Use when: the element has a stable, semantic id or data attribute (e.g., [data-testid="submit"]).
["xpath///*[@id='checkout-form']//button[contains(text(),'Place order')]"]Useful for matching by text content or traversing parent-to-child relationships that CSS cannot express.
Use when: you need to target an element by its visible text or need to navigate up/across the DOM tree.
Pierce (Shadow DOM)
Section titled “Pierce (Shadow DOM)”["pierce/.checkout-widget button.submit"]The pierce prefix instructs the selector engine to cross Shadow DOM boundaries. Regular CSS selectors cannot reach into shadow roots.
Use when: the target element lives inside a Web Component’s shadow tree.
Text Selector
Section titled “Text Selector”["text/Place order"]Matches the first element whose full text content equals the given string. Highly readable and resilient to structural changes.
Use when: button or link text is stable and unique on the page.
ARIA Selector
Section titled “ARIA Selector”["aria/Place order[role=\"button\"]"]Matches by accessible name and optional ARIA role — exactly how a screen reader identifies elements. ARIA selectors survive CSS refactors, DOM restructuring, and framework migrations. They also serve as implicit accessibility tests: if the ARIA selector breaks, the element may no longer be accessible.
Use when: writing any flow you want to remain stable across refactors. Prefer ARIA selectors whenever the element has a meaningful accessible name.
Selector Priority Recommendation
Section titled “Selector Priority Recommendation”ARIA > data-testid (CSS) > text > XPath > CSS class > PierceReplaying Recordings
Section titled “Replaying Recordings”Basic Replay
Section titled “Basic Replay”Open a saved recording and click the Play button (triangle icon). Recorder opens a new tab (or reuses the current one for the same origin), executes each step in sequence, and highlights the target element on screen as it acts.
Pass/fail is shown inline per step:
- Green checkmark — step completed successfully
- Red X with message — step failed (element not found, navigation timeout, assertion failed)
Playback Speed
Section titled “Playback Speed”The speed dropdown offers:
| Setting | Delay Between Steps | Best For |
|---|---|---|
| Normal (1x) | None | Smoke tests, CI |
| Slow (0.25x) | ~4x longer waits | Debugging timing issues, demos |
Slow playback is useful when a step fails intermittently — watching the page at 0.25x often reveals that an animation or lazy-load is still running when the next step fires.
What Happens During Replay
Section titled “What Happens During Replay”- Recorder navigates to the starting URL from the first
navigatestep - For each subsequent step it finds the element using the selector list (first match wins)
- It dispatches the appropriate browser event (click, input, keydown, scroll)
- If an
assertedEventis present, it waits for the event before continuing waitForElementsteps poll the DOM until the condition is satisfied or timeout is reached
Editing Recordings
Section titled “Editing Recordings”Click the Edit (pencil) icon next to a recording to enter edit mode. In edit mode every step has controls to modify, reorder, or remove it.
Add a Step
Section titled “Add a Step”Click Add step (+ icon) above or below an existing step. Choose a step type from the dropdown: click, change, navigate, keyDown, scroll, waitForElement, waitForExpression, customStep.
Useful scenarios:
- Add a
waitForElementafter a click that triggers an async operation - Add a
navigateto reset to a known page before the flow starts - Add a
keyDownwith key"Tab"to test keyboard navigation
Delete a Step
Section titled “Delete a Step”Click the trash icon on any step. Use this to remove accidental clicks or page visits captured during recording.
Edit a Selector
Section titled “Edit a Selector”Expand a step to reveal its selector list. Click any selector string to edit it inline. You can:
- Replace a fragile auto-generated CSS selector with an ARIA or
data-testidselector - Add a new selector to the list as a fallback
- Remove selectors that are known to be unstable
Add a waitForElement Assertion
Section titled “Add a waitForElement Assertion”To assert that an element appears after a step:
- Click Add step after the relevant step
- Choose
waitForElement - Set the selector to the element you expect (e.g.,
["aria/Order confirmed"]) - Set
operatorto>=andcountto1 - Set
timeout(milliseconds) — 5000 to 10000 is typical for async UI
Add a waitForExpression (Custom Condition)
Section titled “Add a waitForExpression (Custom Condition)”waitForExpression lets you pause until an arbitrary JavaScript expression evaluates to truthy in the page context:
{ "type": "waitForExpression", "expression": "document.querySelector('.spinner') === null", "timeout": 8000}This is the escape hatch for situations where DOM presence alone is not enough — e.g., waiting for a spinner to disappear, waiting for a data attribute to be set, or waiting for a global JS variable.
Measuring Performance with Recordings
Section titled “Measuring Performance with Recordings”Recorder integrates with Lighthouse to give you real-user-flow performance metrics — LCP, TBT, and CLS measured during a specific user journey, not just page load.
How to Run a Performance Measurement
Section titled “How to Run a Performance Measurement”- Open the recording you want to measure
- Click Measure performance (speedometer icon) instead of Play
- Recorder runs the flow with Lighthouse instrumentation active
- A Lighthouse report opens when the flow completes
Metrics Produced
Section titled “Metrics Produced”| Metric | What It Measures |
|---|---|
| LCP (Largest Contentful Paint) | When the largest visible element rendered |
| TBT (Total Blocking Time) | How long the main thread was blocked (proxy for interactivity) |
| CLS (Cumulative Layout Shift) | How much the layout shifted unexpectedly |
| FCP (First Contentful Paint) | When the first content appeared |
Why This Matters
Section titled “Why This Matters”Traditional Lighthouse measures page load in isolation. User-flow Lighthouse measures across navigation boundaries — so you can detect that your checkout page is fast but the post-payment confirmation page causes a large CLS. This is impossible to catch with a single-page Lighthouse audit.
Comparing Over Time
Section titled “Comparing Over Time”Export the Lighthouse JSON report alongside your recording JSON. Store both in version control and diff the metric values between releases to detect performance regressions.
Exporting Recordings
Section titled “Exporting Recordings”Click the Export button (arrow icon) in the recording toolbar and choose a format.
JSON (Re-import Later)
Section titled “JSON (Re-import Later)”Exports the raw recording schema. Use this to:
- Share flows with teammates
- Store flows in version control
- Re-import and replay in any Chrome instance
- Modify programmatically and re-import
Puppeteer Export
Section titled “Puppeteer Export”Exports a self-contained Node.js script using the puppeteer and @puppeteer/replay libraries.
Example exported script:
import puppeteer from 'puppeteer';import { createRunner } from '@puppeteer/replay';
// Auto-generated by Chrome DevTools Recorder// Recording: checkout-guest-flow
const flow = { title: 'checkout-guest-flow', steps: [ { type: 'setViewport', width: 1280, height: 720, deviceScaleFactor: 1, isMobile: false, hasTouch: false, isLandscape: false, }, { type: 'navigate', url: 'https://shop.example.com/', assertedEvents: [ { type: 'navigation', url: 'https://shop.example.com/', title: 'Example Shop' }, ], }, { type: 'click', target: 'main', selectors: [ ['aria/Add to cart[role="button"]'], ['#product-42 button.add-to-cart'], ], offsetX: 64, offsetY: 18, }, { type: 'click', target: 'main', selectors: [ ['aria/Proceed to checkout[role="link"]'], ['a.checkout-btn'], ], offsetX: 50, offsetY: 14, }, { type: 'change', target: 'main', selectors: [['aria/Email address'], ['input[name="email"]']], value: 'test@example.com', }, { type: 'change', target: 'main', selectors: [['aria/Card number'], ['input[name="cardNumber"]']], value: '4111111111111111', }, { type: 'click', target: 'main', selectors: [ ['aria/Place order[role="button"]'], ['button[type="submit"].place-order'], ], offsetX: 80, offsetY: 20, }, { type: 'waitForElement', target: 'main', selectors: [['aria/Your order has been placed'], ['h1.confirmation-heading']], operator: '>=', count: 1, timeout: 10000, }, ],};
(async () => { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); const runner = await createRunner(flow, new PuppeteerRunnerExtension(browser, page));
await runner.run();
await browser.close();})();Playwright Export
Section titled “Playwright Export”Exports a script using the playwright library. Playwright’s API differs from Puppeteer’s; the export handles the translation automatically.
Example exported Playwright script:
import { test } from '@playwright/test';import { createRunner, parse } from '@puppeteer/replay';import { PlaywrightRunnerExtension } from '@puppeteer/replay/lib/main.js';
// Auto-generated by Chrome DevTools Recorder// Recording: checkout-guest-flow
const recording = { title: 'checkout-guest-flow', steps: [ { type: 'setViewport', width: 1280, height: 720, deviceScaleFactor: 1, isMobile: false, hasTouch: false, isLandscape: false, }, { type: 'navigate', url: 'https://shop.example.com/', }, { type: 'click', selectors: [['aria/Add to cart[role="button"]'], ['#product-42 button.add-to-cart']], }, { type: 'click', selectors: [['aria/Proceed to checkout[role="link"]'], ['a.checkout-btn']], }, { type: 'change', selectors: [['aria/Email address'], ['input[name="email"]']], value: 'test@example.com', }, { type: 'change', selectors: [['aria/Card number'], ['input[name="cardNumber"]']], value: '4111111111111111', }, { type: 'click', selectors: [['aria/Place order[role="button"]'], ['button[type="submit"].place-order']], }, { type: 'waitForElement', selectors: [['aria/Your order has been placed'], ['h1.confirmation-heading']], operator: '>=', count: 1, timeout: 10000, }, ],};
test('checkout-guest-flow', async ({ page, browser }) => { const runner = await createRunner( parse(recording), new PlaywrightRunnerExtension(browser, page, { timeout: 15000 }), ); await runner.run();});Run with:
npx playwright test checkout-guest-flow.spec.jsWebDriver (Selenium) — Java
Section titled “WebDriver (Selenium) — Java”Exports a Java class using the Selenium WebDriver API.
import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.WebElement;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.support.ui.ExpectedConditions;import org.openqa.selenium.support.ui.WebDriverWait;import java.time.Duration;
public class CheckoutGuestFlow { public static void main(String[] args) { System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver"); WebDriver driver = new ChromeDriver(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
try { driver.manage().window().setSize(new Dimension(1280, 720)); driver.get("https://shop.example.com/");
wait.until(ExpectedConditions.elementToBeClickable( By.cssSelector("#product-42 button.add-to-cart"))).click();
wait.until(ExpectedConditions.elementToBeClickable( By.cssSelector("a.checkout-btn"))).click();
WebElement emailInput = wait.until(ExpectedConditions.visibilityOfElementLocated( By.cssSelector("input[name=\"email\"]"))); emailInput.clear(); emailInput.sendKeys("test@example.com");
WebElement cardInput = driver.findElement(By.cssSelector("input[name=\"cardNumber\"]")); cardInput.clear(); cardInput.sendKeys("4111111111111111");
driver.findElement(By.cssSelector("button[type=\"submit\"].place-order")).click();
wait.until(ExpectedConditions.visibilityOfElementLocated( By.cssSelector("h1.confirmation-heading")));
System.out.println("Flow completed successfully."); } finally { driver.quit(); } }}The @puppeteer/replay Library
Section titled “The @puppeteer/replay Library”@puppeteer/replay is the official Node.js library that powers Recorder exports. You can use it independently of Chrome DevTools to replay JSON recordings in any Node.js environment.
Install
Section titled “Install”npm install @puppeteer/replay puppeteerReplay a JSON File in Node.js
Section titled “Replay a JSON File in Node.js”import puppeteer from 'puppeteer';import { createRunner, PuppeteerRunnerExtension, parse } from '@puppeteer/replay';import { readFileSync } from 'fs';
const json = JSON.parse(readFileSync('./checkout-guest-flow.json', 'utf8'));const flow = parse(json); // validates and parses the recording schema
(async () => { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage();
const runner = await createRunner( flow, new PuppeteerRunnerExtension(browser, page, { timeout: 10000 }), );
await runner.run(); console.log('Recording replayed successfully.');
await browser.close();})();Custom Replay Extension
Section titled “Custom Replay Extension”A custom extension lets you intercept every step — useful for adding extra assertions, logging, or metrics collection without modifying the JSON file.
import { PuppeteerRunnerExtension } from '@puppeteer/replay';
class AssertingExtension extends PuppeteerRunnerExtension { async beforeAllSteps(flow) { console.log(`Starting flow: ${flow.title}`); await super.beforeAllSteps(flow); }
async afterAllSteps(flow) { await super.afterAllSteps(flow); console.log(`Flow complete: ${flow.title}`); }
async beforeEachStep(step, flow) { await super.beforeEachStep(step, flow); console.log(` > ${step.type}`, step.selectors?.[0]?.[0] ?? step.url ?? ''); }
async afterEachStep(step, flow) { await super.afterEachStep(step, flow); // add custom assertions here — e.g. check console errors const errors = await this.page.evaluate(() => window.__errors__ ?? [], ); if (errors.length > 0) { throw new Error(`Console errors after step ${step.type}: ${errors.join(', ')}`); } }}
// Use it:const runner = await createRunner(flow, new AssertingExtension(browser, page));await runner.run();Available Extension Hooks
Section titled “Available Extension Hooks”| Hook | When It Fires |
|---|---|
beforeAllSteps(flow) | Once, before the first step |
afterAllSteps(flow) | Once, after the last step |
beforeEachStep(step, flow) | Before every individual step |
afterEachStep(step, flow) | After every individual step |
runStep(step, flow) | Override to replace default step execution entirely |
Importing Recordings
Section titled “Importing Recordings”To import a previously exported JSON recording:
- Open the Recorder panel
- Click the Import button (upload icon) in the top toolbar
- Select the
.jsonfile from disk - The recording appears in the flow list immediately
You can then replay, edit, or re-export it in any format. This makes Recorder recordings portable across machines and shareable via version control.
CI Integration
Section titled “CI Integration”GitHub Actions: Replay on Every PR
Section titled “GitHub Actions: Replay on Every PR”Store your JSON recording in the repository. Run it on every pull request using Puppeteer.
File layout:
.├── recordings/│ └── checkout-guest-flow.json├── scripts/│ └── replay.js├── package.json└── .github/ └── workflows/ └── recorder.ymlscripts/replay.js:
import puppeteer from 'puppeteer';import { createRunner, PuppeteerRunnerExtension, parse } from '@puppeteer/replay';import { readFileSync } from 'fs';import { resolve } from 'path';
const file = process.argv[2];if (!file) { console.error('Usage: node replay.js <recording.json>'); process.exit(1);}
const flow = parse(JSON.parse(readFileSync(resolve(file), 'utf8')));
(async () => { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage();
try { const runner = await createRunner( flow, new PuppeteerRunnerExtension(browser, page, { timeout: 15000 }), ); await runner.run(); console.log(`PASS: ${flow.title}`); } catch (err) { console.error(`FAIL: ${flow.title}`); console.error(err.message); process.exitCode = 1; } finally { await browser.close(); }})();.github/workflows/recorder.yml:
name: Recorder Replay
on: pull_request: branches: [main] push: branches: [main]
jobs: replay: runs-on: ubuntu-latest
steps: - name: Checkout repository uses: actions/checkout@v4
- name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
- name: Install dependencies run: npm ci
- name: Install Chrome run: npx puppeteer browsers install chrome
- name: Start application run: npm run start:ci & env: NODE_ENV: test
- name: Wait for application to be ready run: npx wait-on http://localhost:3000 --timeout 30000
- name: Replay checkout flow run: node scripts/replay.js recordings/checkout-guest-flow.json
- name: Replay login flow run: node scripts/replay.js recordings/login-flow.jsonpackage.json (relevant section):
{ "scripts": { "start:ci": "PORT=3000 node server.js" }, "dependencies": { "puppeteer": "^22.0.0", "@puppeteer/replay": "^2.0.0" }}Running Against a Staging URL
Section titled “Running Against a Staging URL”Pass the base URL as an environment variable and patch the recording at runtime:
const BASE_URL = process.env.TEST_BASE_URL ?? 'http://localhost:3000';
// Rewrite all navigate steps to use the target environmentfor (const step of flow.steps) { if (step.type === 'navigate' && step.url) { step.url = step.url.replace('https://shop.example.com', BASE_URL); if (step.assertedEvents) { for (const ev of step.assertedEvents) { if (ev.url) ev.url = ev.url.replace('https://shop.example.com', BASE_URL); } } }}Real Use Case: Detecting Regressions in the Checkout Flow
Section titled “Real Use Case: Detecting Regressions in the Checkout Flow”Step 1: Record the Flow
Section titled “Step 1: Record the Flow”- Open
https://shop.example.com/in Chrome - Open Recorder, click
+, name itcheckout-guest-flow - Click Start recording
- Add a product to cart, proceed to checkout, fill in guest details, submit
- Stop recording when the confirmation page loads
Step 2: Strengthen Selectors
Section titled “Step 2: Strengthen Selectors”In edit mode, review each step:
- Replace any
button.btn-primaryselectors witharia/Place order[role="button"] - Replace
input.field--emailwitharia/Email address - Add
waitForElementafter the submit step targetingaria/Your order has been placed
Step 3: Export as Puppeteer
Section titled “Step 3: Export as Puppeteer”Click Export > Puppeteer. Save as scripts/checkout.recording.js.
Step 4: Add Assertions to the Exported Script
Section titled “Step 4: Add Assertions to the Exported Script”Add these after the final waitForElement step in the exported Puppeteer script:
// Assert confirmation number is displayedconst confirmationText = await page.$eval( 'h1.confirmation-heading', el => el.textContent.trim(),);console.assert( confirmationText.includes('Order Confirmed'), `Expected "Order Confirmed", got: "${confirmationText}"`,);
// Assert no error banners are presentconst errorBanners = await page.$$('.error-banner');console.assert( errorBanners.length === 0, `Found ${errorBanners.length} unexpected error banners`,);
// Assert URL contains confirmationconsole.assert( page.url().includes('/confirmation'), `Expected /confirmation URL, got: ${page.url()}`,);Step 5: Commit and Add to CI
Section titled “Step 5: Commit and Add to CI”git add recordings/checkout-guest-flow.json scripts/replay.js .github/workflows/recorder.ymlgit commit -m "Add Recorder-based regression test for checkout flow"Every PR now runs the checkout flow end-to-end. If a developer’s change breaks the checkout path, the CI step fails with the exact step that failed and the selector that could not be resolved.
Limitations and Workarounds
Section titled “Limitations and Workarounds”Hover States
Section titled “Hover States”Recorder does not capture CSS :hover interactions or click-to-reveal menus triggered solely by pointer entry. Steps are only recorded when a click, change, or navigation occurs.
Workaround: Add a customStep annotation in the JSON to document the hover, then implement it in a custom runStep extension:
async runStep(step, flow) { if (step.type === 'customStep' && step.name === 'hover') { const el = await this.page.$(step.parameters.selector); await el.hover(); await this.page.waitForTimeout(step.parameters.delay ?? 300); return; } return super.runStep(step, flow);}Drag and Drop
Section titled “Drag and Drop”Native HTML5 dragstart/dragover/drop sequences are not captured by Recorder.
Workaround: Use page.mouse.move / page.mouse.down / page.mouse.up manually in the exported Puppeteer script, or use a Playwright dragTo call in the exported Playwright script.
File Upload
Section titled “File Upload”<input type="file"> interactions are not captured because the OS file picker is outside the browser’s event system.
Workaround: After export, replace the relevant step with a Puppeteer page.setInputFiles call:
await page.setInputFiles('input[type="file"]', '/path/to/test-file.pdf');In Playwright:
await page.setInputFiles('input[type="file"]', '/path/to/test-file.pdf');Canvas and WebGL Interactions
Section titled “Canvas and WebGL Interactions”Recorder cannot capture interactions with <canvas> elements because canvas events are pixel-coordinate-based and carry no semantic meaning that the DOM can surface.
Workaround: Use page.mouse.click(x, y) with hardcoded coordinates in the exported script. Document the expected canvas state with a screenshot assertion using expect(page).toHaveScreenshot() in Playwright.
iframes
Section titled “iframes”Cross-origin iframes are blocked from recording. Same-origin iframes work but you must set the target property correctly.
Workaround for cross-origin iframes: Use page.frames() in the exported Puppeteer script to locate the iframe and operate on it directly rather than relying on the recorded step.
Authentication / Cookies
Section titled “Authentication / Cookies”If your flow requires login, record it starting after authentication or include the login steps at the beginning of the recording. For CI, inject session cookies programmatically before the runner starts:
await page.setCookie({ name: 'session', value: process.env.TEST_SESSION_COOKIE, domain: 'shop.example.com',});Dynamic Content and Race Conditions
Section titled “Dynamic Content and Race Conditions”Auto-generated recordings use click events with no delay awareness. If an async operation takes longer than expected, replay fails.
Workaround: Add explicit waitForElement or waitForExpression steps after any step that triggers async work. Prefer these over fixed timeouts. Use slow playback (0.25x) to identify which step is racing.
Recorder is Chrome-Only
Section titled “Recorder is Chrome-Only”The recording format is Chrome-specific. Playwright and WebDriver exports translate to cross-browser test code, but the recording itself can only be created and replayed inside Chrome/Chromium.
Workaround: Use the Playwright export and run it against Firefox or WebKit via Playwright’s browser targets to verify cross-browser compatibility.