[SKILL] playwright-recording-processor
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.tsscript, diagnostic report attachment
Core Topics for Reference Files
assertion-patterns, trim-recording, bug-repro-diagnostics
SKILL.md Draft
Full SKILL.md (~90 lines)---
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):
- Trim — Run
trim-recording.tsto cut the raw recording at the marker URL - Classify — Determine flow type: happy path (feature coverage) or bug reproduction (diagnostic capture)
- Swap fixtures — Replace raw
pagereferences with project-specific custom fixtures - Harden — Replace brittle selectors with role-based locators (cross-ref
playwright-expert) - Assert — Inject assertions appropriate to flow type: state verification (happy path) or diagnostic capture (bug repro)
Inputs Required:
- Raw recording file — the
.spec.tsoutput fromnpx playwright codegen - Marker URL — the path where the feature under test begins (e.g.
/invoices/export) - Flow type — happy path or bug reproduction
- Auth fixture name — the existing fixture that handles authenticated sessions
- Output target —
e2e/orintegration/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-expertpatterns 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 boundariesintegration/: Scoped to feature area; bug repros usually land hererecordings/: 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-pathor@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
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveURL(/\/invoices\/\d+/); // regex for dynamic IDs
await expect(page).not.toHaveURL('/login'); // confirm not redirected outVisibility
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
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
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
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.locator('tr')).toHaveCount(11); // 10 rows + headerDownload
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.csv$/);Toast / Notification (transient)
// Must catch before it disappears
const toast = page.getByRole('status');
await expect(toast).toBeVisible();
await expect(toast).toContainText('Saved');Table Cell
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:
getByRole()— most semantic, tied to ARIAgetByLabel()— for form fieldsgetByPlaceholder()— fallback for unlabeled inputsgetByText()— for non-interactive contentgetByTestId()— whendata-testidis availablelocator('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
const errors: string[] = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });Failed requests
const failed: string[] = [];
page.on('requestfailed', r => failed.push(`${r.method()} ${r.url()}`));Element state at failure
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
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
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 filefixture-name— Auth fixture name (default:authenticatedPage)
Example:
npx ts-node scripts/trim-recording.ts \
recordings/raw-session-20240315.spec.ts \
/invoices/export \
e2e/invoice-export.spec.ts \
authenticatedPagetrim-recording.ts Script
#!/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):
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:
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
/**
* 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)
// 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
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
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:
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:
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:
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', mSource: Jeffallan/claude-skills