| name | debugging-troubleshooting |
| description | Debugging and troubleshooting techniques for Playwright tests. Use when debugging flaky or failing tests using Playwright Inspector, trace viewer, video analysis, console/network debugging, or systematic troubleshooting.
|
Debugging & Troubleshooting Skill
Comprehensive guide to debugging Playwright tests — using Playwright Inspector, debug mode, trace viewer, video analysis, and systematic troubleshooting techniques.
Core Principles
- Reproduce first - Confirm the failure is consistent before debugging
- Use built-in tools - Playwright has excellent debugging tools; use them
- Narrow the scope - Isolate the failing step before investigating
- Capture evidence - Screenshots, traces, and videos are your best friends
- Fix root causes - Don't add workarounds without understanding the failure
Table of Contents
Playwright Inspector
Launching the Inspector
npx playwright test tests/checkout.spec.ts --debug
npx playwright test --debug
npx playwright test -g "should complete checkout" --debug
Using the Inspector Effectively
test('debug this test with inspector', async ({ page }) => {
await page.goto('/products');
await page.pause();
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.pause();
await expect(page.getByRole('alert')).toBeVisible();
});
Inspector Tips
Debug Mode
Headed Mode for Debugging
npx playwright test --headed
npx playwright test --headed --slow-mo=500
npx playwright test tests/login.spec.ts --headed --slow-mo=1000
Debug Configuration in Code
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
launchOptions: {
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
},
headless: process.env.HEADED ? false : true,
},
});
Debugging a Single Test
test.only('debug: should add item to cart', async ({ page }) => {
test.setTimeout(0);
await page.goto('/products');
console.log('Current URL:', page.url());
await page.screenshot({ path: 'debug-screenshot.png' });
await page.getByRole('button', { name: 'Add to Cart' }).click();
const content = await page.textContent('body');
console.log('Page contains:', content?.substring(0, 500));
});
Trace Viewer
Configuring Trace Collection
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
trace: 'on-first-retry',
},
});
Viewing Traces
npx playwright show-trace test-results/checkout-should-complete/trace.zip
npx playwright show-report
What the Trace Viewer Shows
Programmatic Trace Control
test('trace specific operations', async ({ page, context }) => {
await context.tracing.start({ screenshots: true, snapshots: true });
await page.goto('/checkout');
await page.getByLabel('Email').fill('user@example.com');
await page.getByRole('button', { name: 'Place Order' }).click();
await context.tracing.stop({ path: 'traces/checkout-trace.zip' });
});
Video Recording & Analysis
Configuring Video Recording
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
video: 'on-first-retry',
video: {
mode: 'on-first-retry',
size: { width: 1280, height: 720 },
},
},
});
Accessing Recorded Videos
test('video will be saved on failure', async ({ page }, testInfo) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
const video = page.video();
if (video) {
const path = await video.path();
console.log('Video saved at:', path);
await testInfo.attach('test-video', {
path: path,
contentType: 'video/webm',
});
}
});
When to Use Video vs Trace
Screenshot Debugging
Strategic Screenshot Placement
test('debug with screenshots', async ({ page }, testInfo) => {
await page.goto('/checkout');
await page.screenshot({
path: 'debug/step-1-page-loaded.png',
fullPage: true,
});
await page.getByLabel('Email').fill('user@example.com');
await page.screenshot({
path: 'debug/step-2-form-filled.png',
});
await page.getByRole('button', { name: 'Submit' }).click();
await page.screenshot({
path: 'debug/step-3-after-submit.png',
fullPage: true,
});
await testInfo.attach('after-submit', {
body: await page.screenshot(),
contentType: 'image/png',
});
});
Element-Specific Screenshots
test('capture specific element state', async ({ page }) => {
await page.goto('/dashboard');
const widget = page.getByTestId('revenue-widget');
await widget.screenshot({ path: 'debug/revenue-widget.png' });
await page.evaluate(() => {
const el = document.querySelector('[data-testid="revenue-widget"]');
if (el) {
(el as HTMLElement).style.border = '3px solid red';
}
});
await page.screenshot({ path: 'debug/highlighted-widget.png' });
});
Console & Network Debugging
Capturing Console Output
test('monitor console for errors', async ({ page }) => {
const consoleMessages: string[] = [];
const consoleErrors: string[] = [];
page.on('console', (msg) => {
const text = `[${msg.type()}] ${msg.text()}`;
consoleMessages.push(text);
if (msg.type() === 'error') {
consoleErrors.push(text);
}
});
page.on('pageerror', (error) => {
consoleErrors.push(`[uncaught] ${error.message}`);
});
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Load Data' }).click();
const unexpectedErrors = consoleErrors.filter(
(err) => !err.includes('Expected warning')
);
expect(unexpectedErrors, `Unexpected console errors: ${unexpectedErrors.join('\n')}`).toHaveLength(0);
});
Monitoring Network Requests
test('debug API calls', async ({ page }) => {
const apiCalls: Array<{ method: string; url: string; status: number }> = [];
page.on('response', (response) => {
if (response.url().includes('/api/')) {
apiCalls.push({
method: response.request().method(),
url: response.url(),
status: response.status(),
});
}
});
page.on('requestfailed', (request) => {
console.error(`Request failed: ${request.method()} ${request.url()}`);
console.error(`Reason: ${request.failure()?.errorText}`);
});
await page.goto('/products');
console.log('API calls made:', JSON.stringify(apiCalls, null, 2));
expect(apiCalls.some((call) => call.url.includes('/api/products'))).toBeTruthy();
});
Waiting for Specific Network Requests
test('wait for API before asserting', async ({ page }) => {
await page.goto('/dashboard');
const [response] = await Promise.all([
page.waitForResponse('**/api/analytics'),
page.getByRole('button', { name: 'Refresh' }).click(),
]);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.totalRevenue).toBeGreaterThan(0);
});
Common Failure Patterns
Pattern 1: Element Not Found
test('debug element not found', async ({ page }) => {
await page.goto('/products');
console.log('Page URL:', page.url());
console.log('Page title:', await page.title());
const count = await page.getByRole('button', { name: 'Add to Cart' }).count();
console.log('Matching elements:', count);
const buttons = await page.getByRole('button').allTextContents();
console.log('All buttons:', buttons);
const allText = await page.textContent('body');
console.log('Page text includes "Add":', allText?.includes('Add'));
});
Pattern 2: Timing / Race Condition
test('fix race condition', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.waitForResponse('**/api/cart');
await expect(page.getByRole('alert')).toHaveText('Added to cart');
});
Pattern 3: Stale Element
test('avoid stale element', async ({ page }) => {
await page.goto('/products');
const addButton = page.getByRole('button', { name: 'Add to Cart' });
await addButton.click();
});
Pattern 4: Navigation Not Complete
test('handle navigation', async ({ page }) => {
await page.goto('/login');
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Login' }).click(),
]);
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
CI/CD Debugging
Getting Debug Info from CI
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'on-first-retry',
},
outputDir: 'test-results/',
reporter: process.env.CI
? [['html', { open: 'never' }], ['github']]
: [['html', { open: 'on-failure' }]],
});
CI Debugging Commands
npx playwright show-report path/to/downloaded/report
npx playwright show-trace path/to/trace.zip
CI=true npx playwright test tests/failing-test.spec.ts --retries=0 --headed
Comparing Local vs CI
test.beforeAll(async () => {
console.log('Environment:', {
ci: process.env.CI,
os: process.platform,
nodeVersion: process.version,
pwVersion: require('@playwright/test/package.json').version,
});
});
Debugging Selectors
Testing Selectors Interactively
npx playwright open https://your-app.com
Selector Debugging in Code
test('debug selectors', async ({ page }) => {
await page.goto('/products');
const buttons = page.getByRole('button');
console.log('Total buttons:', await buttons.count());
for (let i = 0; i < await buttons.count(); i++) {
console.log(`Button ${i}:`, await buttons.nth(i).textContent());
}
const submitBtn = page.getByRole('button', { name: 'Submit' });
const isVisible = await submitBtn.isVisible();
const count = await submitBtn.count();
console.log(`Submit button - count: ${count}, visible: ${isVisible}`);
const element = page.getByTestId('product-card');
if (await element.count() > 0) {
const classes = await element.first().getAttribute('class');
console.log('Element classes:', classes);
}
});
Playwright Codegen for Selectors
npx playwright codegen https://your-app.com
Systematic Troubleshooting Checklist
Step-by-Step Debugging Process
-
Reproduce the failure
-
Gather evidence
-
Narrow the scope
-
Identify root cause
-
Apply the fix
Self-Validation: Verify Tests Aren't Flaky
Before considering a test complete, run it multiple times to confirm stability:
npx playwright test -g "Should display product details" --repeat-each=5 --reporter=line
npx playwright test tests/checkout/cart.spec.ts --repeat-each=5 --reporter=line
./hooks/validate-test.sh "Should display product details"
./hooks/validate-test.sh "tests/checkout/cart.spec.ts"
Why 5 times?
- A test that passes once might be flaky
- Running 5 times catches most intermittent failures
- If it passes 5/5, you can be reasonably confident it's stable
- If it fails even once, investigate before committing
test('Should display product details', async ({ page }) => {
const product = await createTestProduct(page, { name: 'Laptop' });
await page.goto(`/products/${product.id}`);
await page.waitForResponse('**/api/products/*');
await expect(page.getByRole('heading', { name: 'Laptop' })).toBeVisible();
});
test('display products', async ({ page }) => {
await page.goto('/products');
await page.waitForTimeout(2000);
await expect(page.getByText('Laptop')).toBeVisible();
});
Quick Reference Commands
npx playwright test tests/my-test.spec.ts --debug
npx playwright test --headed --slow-mo=500
npx playwright codegen http://localhost:3000
npx playwright show-report
npx playwright show-trace test-results/trace.zip
npx playwright test --trace on
npx playwright test --list
Related Resources