[SKILL] playwright-recording-processor

Author: JeffallanCreated Mar 20, 2026Updated Mar 20, 2026
Labelsenhancement

Skill Name

playwright-recording-processor

Skill Domain

Quality

Overview

Recording processing specialist that converts raw npx playwright codegen output into production-ready E2E tests. Companion to playwright-expert — handles the specific workflow of trimming, classifying, and enriching codegen recordings rather than general Playwright authoring. Target audience: QA engineers and developers who use Playwright's codegen recorder to capture browser sessions and need structured, assertion-rich test files.

Trigger Conditions

Processing codegen session recordings, trimming recorded browser sessions to a marker URL, classifying recordings as happy-path or bug-repro flows, injecting diagnostic capture into reproduction tests, or converting a raw codegen recording into a maintainable test file.

Keywords

codegen, recording, trim, marker URL, bug repro, diagnostic capture, session recording, Playwright codegen, recording processing, test generation

Overlapping Skills to Check

playwright-expert, test-master

Overlap Analysis

  • From playwright-expert (cross-ref in prose, not duplicated): locator priority/hardening (selectors-locators.md), Page Object Model and fixtures (page-object-model.md), debugging flaky tests (debugging-flaky.md)
  • Unique to this skill (local references): trim codegen recordings to marker URL, flow classification (happy-path vs bug-repro), assertion patterns for recordings, bug repro diagnostic injection, trim-recording.ts script, diagnostic report attachment

Core Topics for Reference Files

assertion-patterns, trim-recording, bug-repro-diagnostics


SKILL.md Draft

Full SKILL.md (~90 lines)
yaml
---
name: playwright-recording-processor
description: "Transforms raw Playwright codegen recordings into production-ready E2E tests through trimming, classification, and assertion injection. Use when processing codegen session recordings, trimming recorded browser sessions to a marker URL, classifying recordings as happy-path or bug-repro flows, or injecting diagnostic capture into reproduction tests. Invoke to convert a raw codegen recording into a maintainable test file or to build a bug reproduction test with console/network/state diagnostics."
license: MIT
metadata:
  author: https://github.com/Jeffallan
  version: "1.0.0"
  domain: quality
  triggers: codegen, recording, trim, marker URL, bug repro, diagnostic capture, session recording, Playwright codegen, recording processing, test generation
  role: specialist
  scope: implementation
  output-format: code
  related-skills: playwright-expert, test-master
---

Recording processing specialist that converts raw Playwright codegen output into structured, maintainable E2E tests.

Core Workflow (5 steps):

  1. Trim — Run trim-recording.ts to cut the raw recording at the marker URL
  2. Classify — Determine flow type: happy path (feature coverage) or bug reproduction (diagnostic capture)
  3. Swap fixtures — Replace raw page references with project-specific custom fixtures
  4. Harden — Replace brittle selectors with role-based locators (cross-ref playwright-expert)
  5. Assert — Inject assertions appropriate to flow type: state verification (happy path) or diagnostic capture (bug repro)

Inputs Required:

  1. Raw recording file — the .spec.ts output from npx playwright codegen
  2. Marker URL — the path where the feature under test begins (e.g. /invoices/export)
  3. Flow type — happy path or bug reproduction
  4. Auth fixture name — the existing fixture that handles authenticated sessions
  5. Output target — e2e/ or integration/ depending on scope

If any of these are missing, ask before proceeding.

Flow Classification:

Happy Path Flow — Use when:

  • Testing a new feature for the first time
  • Adding coverage for a release
  • Tester followed a structured checklist

Output: A clean spec with the full user journey from marker URL to completion, using the existing auth fixture, with assertions added at key checkpoints.

Bug Reproduction Flow — Use when:

  • A nontechnical user reported "the app broke"
  • You need to capture what they actually did, in what state, on what browser
  • The goal is a reproducible failing test to anchor a fix

Output: A spec with full diagnostic metadata injected — browser, viewport, console errors, network failures, element states at time of failure — plus a structured comment block at the top summarizing the reported issue.

Reference Guide:

Topic Reference Load When
Assertion Patterns references/assertion-patterns.md Adding assertions by UI element type
Trim Recording references/trim-recording.md Trimming raw codegen output, marker URL
Bug Repro Diagnostics references/bug-repro-diagnostics.md Diagnostic injection, console/network/state capture

Cross-References (from playwright-expert, not duplicated):

  • For locator hardening patterns, see playwright-expert (selectors-locators.md)
  • For Page Object Model and fixture patterns, see playwright-expert (page-object-model.md)
  • For debugging flaky processed tests, see playwright-expert (debugging-flaky.md)

Constraints:

MUST DO:

  • Always trim to marker URL before processing
  • Classify every recording before adding assertions
  • Use role-based selectors from playwright-expert patterns when hardening
  • Attach diagnostic reports to test results for bug repro flows
  • Keep trimmed recordings as source-of-truth comments in generated tests

MUST NOT DO:

  • Process untrimmed recordings (noise from manual navigation pollutes tests)
  • Mix happy-path assertions with diagnostic capture in a single test
  • Duplicate locator/POM patterns already covered by playwright-expert
  • Use waitForTimeout() in generated tests (use proper waits)
  • Generate tests without assertion injection (bare action-only tests are not useful)

Output File Placement:

  • e2e/: Full journeys crossing multiple pages or system boundaries
  • integration/: Scoped to feature area; bug repros usually land here
  • recordings/: Archive only — raw files are never run directly

Checklist Before Handing Off:

  • Trimmed to marker URL — no auth or pre-feature navigation
  • Auth fixture swapped in correctly
  • All locators are semantic (role, label, text) not positional
  • At least one assertion per logical section (happy path)
  • Diagnostic collection injected (bug repro)
  • Dynamic values parameterized or noted for follow-up
  • Tagged correctly (@happy-path or @bug-repro)
  • Placed in correct output directory
  • Raw recording archived untouched in /recordings

Reference File Drafts

references/assertion-patterns.md (~200 lines)

Assertion Patterns Reference

Common expect() patterns by UI element type. Use during the optimization pass to add meaningful assertions to trimmed recordings.

URL / Navigation

typescript
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/\/invoices\/\d+/);  // regex for dynamic IDs
await expect(page).not.toHaveURL('/login');        // confirm not redirected out

Visibility

typescript
await expect(page.getByText('Submission successful')).toBeVisible();
await expect(page.getByRole('alert')).toBeVisible();
await expect(page.getByTestId('loading-spinner')).toBeHidden();
await expect(page.getByRole('dialog')).not.toBeVisible();

Form State

typescript
await expect(page.getByLabel('Email')).toHaveValue('[email protected]');
await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();

Text Content

typescript
await expect(page.getByRole('heading')).toHaveText('Invoice Export');
await expect(page.getByRole('status')).toContainText('3 items');
await expect(page.locator('table')).toContainText('Q1 2024');

Count / List

typescript
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.locator('tr')).toHaveCount(11); // 10 rows + header

Download

typescript
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);

Toast / Notification (transient)

typescript
// Must catch before it disappears
const toast = page.getByRole('status');
await expect(toast).toBeVisible();
await expect(toast).toContainText('Saved');

Table Cell

typescript
const row = page.getByRole('row', { name: /Invoice #1042/ });
await expect(row.getByRole('cell', { name: 'Paid' })).toBeVisible();

Locator Priority (Highest to Lowest)

Use these in order — stop at the first one that works cleanly:

  1. getByRole() — most semantic, tied to ARIA
  2. getByLabel() — for form fields
  3. getByPlaceholder() — fallback for unlabeled inputs
  4. getByText() — for non-interactive content
  5. getByTestId() — when data-testid is available
  6. locator('css') — last resort, flag for dev to add testid

Never use nth-child, positional, or deeply nested CSS selectors in optimized tests.

Bug Repro: State Capture Snippets

Console errors

typescript
const errors: string[] = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });

Failed requests

typescript
const failed: string[] = [];
page.on('requestfailed', r => failed.push(`${r.method()} ${r.url()}`));

Element state at failure

typescript
const state = await page.locator('#submit-btn').evaluate(el => ({
  disabled: (el as HTMLButtonElement).disabled,
  visible: (el as HTMLElement).offsetParent !== null,
  text: el.textContent?.trim(),
  ariaLabel: el.getAttribute('aria-label'),
}));

Full diagnostic attach

typescript
await test.info().attach('session-diagnostics', {
  body: JSON.stringify({ errors, failed, state, ts: new Date().toISOString() }, null, 2),
  contentType: 'application/json',
});
references/trim-recording.md (~200 lines)

Trim Recording Reference

Trims raw Playwright codegen output to a marker URL entry point, removing pre-feature navigation noise (login, dashboard clicks, etc.).

Why Trim?

npx playwright codegen captures everything from the moment the browser opens — login flows, sidebar navigation, page loads that precede the actual feature under test. Running these raw recordings as tests creates:

  • Fragile tests that break when unrelated UI changes
  • Slow tests that repeat auth flows already handled by fixtures
  • Noisy tests that obscure the actual feature coverage

Trimming isolates the feature-specific actions starting from the marker URL.

Marker URL Convention

A marker URL is the route where the feature under test begins. Examples:

  • /invoices/export — the export feature page
  • /settings/billing — billing settings
  • /api/v2/docs — API documentation page

Choose the most specific URL that marks the start of the user flow you want to test.

Script Usage

bash
npx ts-node scripts/trim-recording.ts <input> <marker-url> <output> [fixture-name]

Arguments:

  • input — Path to raw codegen recording (.spec.ts)
  • marker-url — URL path to trim to (e.g. /invoices/export)
  • output — Path for trimmed output file
  • fixture-name — Auth fixture name (default: authenticatedPage)

Example:

bash
npx ts-node scripts/trim-recording.ts \
  recordings/raw-session-20240315.spec.ts \
  /invoices/export \
  e2e/invoice-export.spec.ts \
  authenticatedPage

trim-recording.ts Script

typescript
#!/usr/bin/env ts-node
/**
 * trim-recording.ts
 * Trims a raw Playwright codegen recording to a marker URL entry point.
 *
 * Usage:
 *   ts-node trim-recording.ts <input-file> <marker-url> <output-file> [fixture-name]
 *
 * Example:
 *   ts-node trim-recording.ts recordings/raw.spec.ts /invoices/export e2e/invoice-export.spec.ts authenticatedPage
 */
import * as fs from 'fs';
import * as path from 'path';

const [inputFile, markerUrl, outputFile, fixtureName = 'authenticatedPage'] = process.argv.slice(2);

if (!inputFile || !markerUrl || !outputFile) {
  console.error('Usage: ts-node trim-recording.ts <input> <markerUrl> <output> [fixtureName]');
  console.error('');
  console.error('  input       Path to raw codegen recording (.spec.ts)');
  console.error('  markerUrl   URL path to trim to, e.g. /invoices/export');
  console.error('  output      Path for trimmed output file');
  console.error('  fixtureName Auth fixture name (default: authenticatedPage)');
  process.exit(1);
}

if (!fs.existsSync(inputFile)) {
  console.error(`Input file not found: ${inputFile}`);
  process.exit(1);
}

const raw = fs.readFileSync(inputFile, 'utf-8');
const lines = raw.split('\n');

// Find the line where the marker URL first appears in a goto() call
const markerIndex = lines.findIndex(line =>
  line.includes('page.goto(') && line.includes(markerUrl)
);

if (markerIndex === -1) {
  console.error(`Marker URL "${markerUrl}" not found in any page.goto() call.`);
  console.error('');
  console.error('URLs found in recording:');
  lines
    .filter(l => l.includes('page.goto('))
    .forEach(l => console.error(' ', l.trim()));
  process.exit(1);
}

// Extract imports from top of file
const imports = lines.filter(line =>
  line.startsWith('import') || line.startsWith('const {')
);

// Everything from the marker line down becomes the test body
const body = lines.slice(markerIndex);

// Derive a test name from the output filename
const testName = path.basename(outputFile, '.spec.ts')
  .replace(/-/g, ' ')
  .replace(/\b\w/g, c => c.toUpperCase());

// Build output
const output = [
  imports.join('\n'),
  '',
  `test('${testName}', async ({ ${fixtureName}: page }) => {`,
  body.join('\n'),
].join('\n').trimEnd();

// Ensure output directory exists
const outputDir = path.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
  fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFileSync(outputFile, output + '\n');

const linesRemoved = markerIndex;
const linesKept = body.length;

console.log('');
console.log('Recording trimmed successfully');
console.log(`  Input:         ${inputFile}`);
console.log(`  Output:        ${outputFile}`);
console.log(`  Marker URL:    ${markerUrl} (line ${markerIndex + 1})`);
console.log(`  Auth fixture:  ${fixtureName}`);
console.log(`  Lines removed: ${linesRemoved} (pre-feature activity)`);
console.log(`  Lines kept:    ${linesKept}`);
console.log('');
console.log('Next steps:');
console.log('  1. Review output -- add assertions at key checkpoints');
console.log('  2. Harden any positional or CSS locators');
console.log('  3. Parameterize hardcoded dynamic values');
console.log('  4. Tag: @happy-path or @bug-repro');

Example: Before and After

Raw recording (input):

typescript
import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('https://app.example.com/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.getByRole('link', { name: 'Invoices' }).click();
  await page.goto('https://app.example.com/invoices/export');
  await page.getByLabel('Date Range').fill('last-30-days');
  await page.getByRole('button', { name: 'Download' }).click();
});

Trimmed output:

typescript
import { test, expect } from '@playwright/test';

test('Invoice Export', async ({ authenticatedPage: page }) => {
  await page.goto('https://app.example.com/invoices/export');
  await page.getByLabel('Date Range').fill('last-30-days');
  await page.getByRole('button', { name: 'Download' }).click();
});

Lines 3–8 (login + navigation) are removed. The auth fixture handles session setup.

references/bug-repro-diagnostics.md (~200 lines)

Bug Repro Diagnostics Reference

Diagnostic injection patterns for converting codegen recordings into bug reproduction tests with full environment and state capture.

When to Use Bug Repro Flow

Use bug repro (not happy path) when:

  • A nontechnical user reported "the app broke" and you have their recorded session
  • The goal is a reproducible failing test, not feature coverage
  • You need to capture browser state, console errors, and network failures at the point of failure

Diagnostic Injection Workflow

Step 1: Add Comment Header

typescript
/**
 * BUG REPRODUCTION TEST
 * Reported: 2024-03-15
 * Symptom: Export button does nothing after selecting date range
 * Browser: captured at runtime (see diagnosticReport)
 * Steps to reproduce: trimmed from user session recording
 * Expected: CSV download starts
 * Actual: Button click has no effect, no error shown
 */

Step 2: Inject Environment Capture (before marker URL line)

typescript
// Capture environment at session start
const browserName = page.context().browser()?.browserType().name();
const viewport = page.viewportSize();
const userAgent = await page.evaluate(() => navigator.userAgent);

Step 3: Set Up Console Error Listener

typescript
const consoleErrors: string[] = [];
page.on('console', msg => {
  if (msg.type() === 'error') consoleErrors.push(msg.text());
});

// Also catch uncaught exceptions
const pageErrors: string[] = [];
page.on('pageerror', err => pageErrors.push(err.message));

Step 4: Set Up Network Failure Capture

typescript
const failedRequests: { method: string; url: string; error: string }[] = [];
page.on('requestfailed', req => {
  failedRequests.push({
    method: req.method(),
    url: req.url(),
    error: req.failure()?.errorText ?? 'unknown',
  });
});

// Also capture non-2xx responses
const badResponses: { status: number; url: string }[] = [];
page.on('response', res => {
  if (res.status() >= 400) {
    badResponses.push({ status: res.status(), url: res.url() });
  }
});

Step 5: Element State Snapshot at Failure Point

Insert this immediately before the action that the user reported as failing:

typescript
const elementSnapshot = await page.locator('[data-testid="submit-btn"]').evaluate(el => ({
  disabled: (el as HTMLButtonElement).disabled,
  visible: (el as HTMLElement).offsetParent !== null,
  text: el.textContent,
  classes: el.className,
  ariaLabel: el.getAttribute('aria-label'),
  boundingBox: el.getBoundingClientRect().toJSON(),
}));

Step 6: Attach Diagnostic Report

At the end of the test, attach all captured data to the test results:

typescript
await test.info().attach('diagnostic-report', {
  body: JSON.stringify({
    browser: browserName,
    viewport,
    userAgent,
    consoleErrors,
    pageErrors,
    failedRequests,
    badResponses,
    elementSnapshot,
    timestamp: new Date().toISOString(),
  }, null, 2),
  contentType: 'application/json',
});

Complete Example: Raw Recording to Bug Repro Test

Input: Raw codegen recording of a user who reported "export button doesn't work"

Output:

typescript
import { test, expect } from '@playwright/test';

/**
 * BUG REPRODUCTION TEST
 * Reported: 2024-03-15
 * Symptom: Export button does nothing after selecting date range
 * Expected: CSV download starts
 * Actual: Button click has no effect
 */
test('invoice-export-bug-repro @bug-repro', async ({ authenticatedPage: page }) => {
  // -- Diagnostic setup --
  const browserName = page.context().browser()?.browserType().name();
  const viewport = page.viewportSize();
  const userAgent = await page.evaluate(() => navigator.userAgent);

  const consoleErrors: string[] = [];
  page.on('console', m