debugging-troubleshooting
>
Works with
---
name: debugging-troubleshooting
description: >
license: MIT
---
# Debugging & Troubleshooting Skill
Comprehensive guide to debugging Playwright tests — using Playwright Inspector, debug mode, trace viewer, video analysis, and systematic troubleshooting techniques.
## Core Principles
1. **Reproduce first** - Confirm the failure is consistent before debugging
2. **Use built-in tools** - Playwright has excellent debugging tools; use them
3. **Narrow the scope** - Isolate the failing step before investigating
4. **Capture evidence** - Screenshots, traces, and videos are your best friends
5. **Fix root causes** - Don't add workarounds without understanding the failure
## Table of Contents
- [Playwright Inspector](#playwright-inspector)
- [Debug Mode](#debug-mode)
- [Trace Viewer](#trace-viewer)
- [Video Recording & Analysis](#video-recording--analysis)
- [Screenshot Debugging](#screenshot-debugging)
- [Console & Network Debugging](#console--network-debugging)
- [Common Failure Patterns](#common-failure-patterns)
- [CI/CD Debugging](#cicd-debugging)
- [Debugging Selectors](#debugging-selectors)
- [Systematic Troubleshooting Checklist](#systematic-troubleshooting-checklist)
---
## Playwright Inspector
### Launching the Inspector
```bash
# Run a specific test with the inspector
npx playwright test tests/checkout.spec.ts --debug
# Run all tests in debug mode
npx playwright test --debug
# Debug a specific test by title
npx playwright test -g "should complete checkout" --debug
```
### Using the Inspector Effectively
```typescript
test('debug this test with inspector', async ({ page }) => {
await page.goto('/products');
// Add page.pause() to stop execution and open inspector
await page.pause();
// Inspector opens here - you can:
// 1. Step through actions one at a time
// 2. Inspect the page's DOM
// 3. Try selectors in the Locator tab
// 4. View the action log
// 5. Resume or step over
await page.getByRole('button', { name: 'Add to Cart' }).click();
// Add another pause to inspect state after the click
await page.pause();
await expect(page.getByRole('alert')).toBeVisible();
});
```
### Inspector Tips
```typescript
// TIP 1: Use PWDEBUG environment variable
// PWDEBUG=1 npx playwright test
// This auto-opens inspector for ALL tests
// TIP 2: Use console mode in inspector
// PWDEBUG=console npx playwright test
// Opens browser DevTools console with Playwright helpers
// TIP 3: Pick locators interactively
// In the Inspector, click "Pick locator" then click any element
// on the page to get the recommended selector
```
---
## Debug Mode
### Headed Mode for Debugging
```bash
# Run tests in headed mode (see the browser)
npx playwright test --headed
# Slow down actions for visual debugging
npx playwright test --headed --slow-mo=500
# Combine with specific test
npx playwright test tests/login.spec.ts --headed --slow-mo=1000
```
### Debug Configuration in Code
```typescript
// playwright.config.ts - Debug-friendly config
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Slow down actions by 200ms (useful for visual debugging)
launchOptions: {
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
},
// Keep browser open after test failure
headless: process.env.HEADED ? false : true,
},
});
```
### Debugging a Single Test
```typescript
// Temporarily focus on one test for debugging
test.only('debug: should add item to cart', async ({ page }) => {
// Set a longer timeout while debugging
test.setTimeout(0); // No timeout during debugging
await page.goto('/products');
// Log page URL for verification
console.log('Current URL:', page.url());
// Take a screenshot at a specific point
await page.screenshot({ path: 'debug-screenshot.png' });
await page.getByRole('button', { name: 'Add to Cart' }).click();
// Check what's on the page
const content = await page.textContent('body');
console.log('Page contains:', content?.substring(0, 500));
});
// IMPORTANT: Remove test.only before committing!
```
---
## Trace Viewer
### Configuring Trace Collection
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// ✅ Recommended: Record trace on first retry
trace: 'on-first-retry',
// Other options:
// trace: 'on' - Always record (slower, more storage)
// trace: 'off' - Never record
// trace: 'retain-on-failure' - Keep only for failures
},
});
```
### Viewing Traces
```bash
# Open the trace viewer with a trace file
npx playwright show-trace test-results/checkout-should-complete/trace.zip
# Open trace from HTML report
npx playwright show-report
# Then click on a failed test → "Traces" tab
```
### What the Trace Viewer Shows
```typescript
// The trace viewer provides:
// 1. Timeline - Visual timeline of all test actions
// 2. Actions - Each step with before/after screenshots
// 3. Metadata - Test name, duration, status
// 4. Source - The test source code with current line highlighted
// 5. Network - All network requests and responses
// 6. Console - Browser console output
// 7. DOM - Snapshot of DOM at each step
// 8. Call - Detailed info about each Playwright call
// You can:
// - Click any action to see the page state
// - Filter network requests
// - Search the DOM snapshot
// - View request/response bodies
```
### Programmatic Trace Control
```typescript
test('trace specific operations', async ({ page, context }) => {
// Start tracing for a specific section
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();
// Stop and save trace
await context.tracing.stop({ path: 'traces/checkout-trace.zip' });
});
```
---
## Video Recording & Analysis
### Configuring Video Recording
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// ✅ Record video only on failure (recommended)
video: 'on-first-retry',
// Video size configuration
video: {
mode: 'on-first-retry',
size: { width: 1280, height: 720 },
},
},
});
```
### Accessing Recorded Videos
```typescript
test('video will be saved on failure', async ({ page }, testInfo) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
// After test completes, video is available at:
// test-results/<test-name>/video.webm
// You can also access video path programmatically
const video = page.video();
if (video) {
const path = await video.path();
console.log('Video saved at:', path);
// Attach video to test report
await testInfo.attach('test-video', {
path: path,
contentType: 'video/webm',
});
}
});
```
### When to Use Video vs Trace
```typescript
// Use VIDEO when:
// - You need to see the full visual flow
// - Animations or transitions are involved
// - You want to share with non-technical stakeholders
// - CI/CD failure investigation
// Use TRACE when:
// - You need to inspect DOM state at each step
// - You need network request/response details
// - You need console output
// - You need step-by-step debugging
// - Trace is more detailed but larger files
// ✅ Best practice: Use both on first retry
// video: 'on-first-retry',
// trace: 'on-first-retry',
```
---
## Screenshot Debugging
### Strategic Screenshot Placement
```typescript
test('debug with screenshots', async ({ page }, testInfo) => {
await page.goto('/checkout');
// Capture state at key points
await page.screenshot({
path: 'debug/step-1-page-loaded.png',
fullPage: true,
});
await page.getByLabel('Email').fill('user@example.com');
// Capture after form fill
await page.screenshot({
path: 'debug/step-2-form-filled.png',
});
await page.getByRole('button', { name: 'Submit' }).click();
// Capture result
await page.screenshot({
path: 'debug/step-3-after-submit.png',
fullPage: true,
});
// Attach to report for easy access
await testInfo.attach('after-submit', {
body: await page.screenshot(),
contentType: 'image/png',
});
});
```
### Element-Specific Screenshots
```typescript
test('capture specific element state', async ({ page }) => {
await page.goto('/dashboard');
// Screenshot just the problematic component
const widget = page.getByTestId('revenue-widget');
await widget.screenshot({ path: 'debug/revenue-widget.png' });
// Screenshot with element highlighted
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
```typescript
test('monitor console for errors', async ({ page }) => {
const consoleMessages: string[] = [];
const consoleErrors: string[] = [];
// Capture all console messages
page.on('console', (msg) => {
const text = `[${msg.type()}] ${msg.text()}`;
consoleMessages.push(text);
if (msg.type() === 'error') {
consoleErrors.push(text);
}
});
// Capture uncaught exceptions
page.on('pageerror', (error) => {
consoleErrors.push(`[uncaught] ${error.message}`);
});
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Load Data' }).click();
// Assert no unexpected console errors
const unexpectedErrors = consoleErrors.filter(
(err) => !err.includes('Expected warning') // Filter known warnings
);
expect(unexpectedErrors, `Unexpected console errors: ${unexpectedErrors.join('\n')}`).toHaveLength(0);
});
```
### Monitoring Network Requests
```typescript
test('debug API calls', async ({ page }) => {
const apiCalls: Array<{ method: string; url: string; status: number }> = [];
// Log all API responses
page.on('response', (response) => {
if (response.url().includes('/api/')) {
apiCalls.push({
method: response.request().method(),
url: response.url(),
status: response.status(),
});
}
});
// Log failed requests
page.on('requestfailed', (request) => {
console.error(`Request failed: ${request.method()} ${request.url()}`);
console.error(`Reason: ${request.failure()?.errorText}`);
});
await page.goto('/products');
// Verify expected API calls were made
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
```typescript
test('wait for API before asserting', async ({ page }) => {
await page.goto('/dashboard');
// ✅ Good - Wait for specific API response before asserting
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
```typescript
// SYMPTOM: "Timeout waiting for selector"
// CAUSE: Selector doesn't match any element
// Debugging steps:
test('debug element not found', async ({ page }) => {
await page.goto('/products');
// Step 1: Check if page loaded correctly
console.log('Page URL:', page.url());
console.log('Page title:', await page.title());
// Step 2: Check if element exists at all
const count = await page.getByRole('button', { name: 'Add to Cart' }).count();
console.log('Matching elements:', count);
// Step 3: Check what's actually on the page
const buttons = await page.getByRole('button').allTextContents();
console.log('All buttons:', buttons);
// Step 4: Try a broader selector
const allText = await page.textContent('body');
console.log('Page text includes "Add":', allText?.includes('Add'));
});
```
### Pattern 2: Timing / Race Condition
```typescript
// SYMPTOM: Test passes locally but fails in CI
// CAUSE: Race condition between UI update and assertion
// ✅ Fix: Use proper Playwright waiting
test('fix race condition', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
// ✅ Wait for the specific response that triggers UI update
await page.waitForResponse('**/api/cart');
// ✅ Then assert with auto-waiting
await expect(page.getByRole('alert')).toHaveText('Added to cart');
});
```
### Pattern 3: Stale Element
```typescript
// SYMPTOM: "Element is not attached to the DOM"
// CAUSE: Page re-rendered and element reference is stale
// ✅ Fix: Use locators (not element handles)
test('avoid stale element', async ({ page }) => {
await page.goto('/products');
// ✅ Locators always re-query the DOM
const addButton = page.getByRole('button', { name: 'Add to Cart' });
await addButton.click(); // Always finds the latest element
// ❌ Element handles can become stale
// const handle = await page.$('button.add-to-cart');
// await handle.click(); // May fail if DOM re-rendered
});
```
### Pattern 4: Navigation Not Complete
```typescript
// SYMPTOM: "Target page, context or browser has been closed"
// CAUSE: Navigation triggered but test didn't wait for it
// ✅ Fix: Wait for navigation explicitly
test('handle navigation', async ({ page }) => {
await page.goto('/login');
// ✅ Wait for navigation triggered by form submit
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
```typescript
// playwright.config.ts - CI-specific configuration
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Retry failed tests in CI
retries: process.env.CI ? 2 : 0,
use: {
// Capture artifacts on failure in CI
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'on-first-retry',
},
// Output directory for artifacts
outputDir: 'test-results/',
// HTML report
reporter: process.env.CI
? [['html', { open: 'never' }], ['github']]
: [['html', { open: 'on-failure' }]],
});
```
### CI Debugging Commands
```bash
# Download CI artifacts and view the HTML report
npx playwright show-report path/to/downloaded/report
# View trace from CI artifacts
npx playwright show-trace path/to/trace.zip
# Run specific failing test locally with CI-like settings
CI=true npx playwright test tests/failing-test.spec.ts --retries=0 --headed
```
### Comparing Local vs CI
```typescript
// Add environment info to test output for comparison
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
```bash
# Open a browser to test selectors
npx playwright open https://your-app.com
# Use the Locator picker in the toolbar to find selectors
# Type selectors in the console to test them:
# > page.getByRole('button', { name: 'Submit' })
# > page.getByLabel('Email')
```
### Selector Debugging in Code
```typescript
test('debug selectors', async ({ page }) => {
await page.goto('/products');
// List all role-based matches
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());
}
// Check if element is hidden vs missing
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}`);
// Use evaluate to inspect element properties
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
```bash
# Generate test code by interacting with the app
npx playwright codegen https://your-app.com
# Codegen will:
# 1. Open a browser with your app
# 2. Record your interactions
# 3. Generate Playwright test code with recommended selectors
# 4. You can copy selectors directly into your tests
```
---
## Systematic Troubleshooting Checklist
### Step-by-Step Debugging Process
1. **Reproduce the failure**
- [ ] Run the failing test in isolation
- [ ] Run it multiple times (is it flaky?)
- [ ] Run in headed mode: `npx playwright test --headed`
2. **Gather evidence**
- [ ] Check screenshots in `test-results/`
- [ ] View trace: `npx playwright show-trace <trace.zip>`
- [ ] Check video recording
- [ ] Review console output
3. **Narrow the scope**
- [ ] Add `page.pause()` before the failing step
- [ ] Use Playwright Inspector to step through
- [ ] Check the selector matches elements
- [ ] Verify the page state is correct
4. **Identify root cause**
- [ ] Is it a selector issue? (element not found)
- [ ] Is it a timing issue? (race condition)
- [ ] Is it a data issue? (missing test data)
- [ ] Is it an environment issue? (CI vs local)
- [ ] Is it a dependency issue? (test order dependent)
5. **Apply the fix**
- [ ] Fix the root cause (not a workaround)
- [ ] Add a descriptive error message
- [ ] Verify the fix works in CI
- [ ] Remove any `test.only`, `page.pause()`, or `console.log`
### Self-Validation: Verify Tests Aren't Flaky
Before considering a test complete, run it multiple times to confirm stability:
```bash
# Run a specific test 5 times to check for flakiness
npx playwright test -g "Should display product details" --repeat-each=5 --reporter=line
# Run a specific file 5 times
npx playwright test tests/checkout/cart.spec.ts --repeat-each=5 --reporter=line
# Use the provided hook script for convenience
./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
```typescript
// ✅ Good - Test is deterministic and passes every time
test('Should display product details', async ({ page }) => {
// Set up via API (not dependent on UI state)
const product = await createTestProduct(page, { name: 'Laptop' });
await page.goto(`/products/${product.id}`);
// Wait for specific API response, not arbitrary timeout
await page.waitForResponse('**/api/products/*');
await expect(page.getByRole('heading', { name: 'Laptop' })).toBeVisible();
});
// ❌ Bad - Test has timing issues that cause flakiness
test('display products', async ({ page }) => {
await page.goto('/products');
await page.waitForTimeout(2000); // Flaky!
await expect(page.getByText('Laptop')).toBeVisible();
});
```
### Quick Reference Commands
```bash
# Debug a specific test
npx playwright test tests/my-test.spec.ts --debug
# Run headed with slow motion
npx playwright test --headed --slow-mo=500
# Generate test with codegen
npx playwright codegen http://localhost:3000
# View HTML report
npx playwright show-report
# View trace file
npx playwright show-trace test-results/trace.zip
# Run with full trace
npx playwright test --trace on
# List available tests
npx playwright test --list
```
## Related Resources
- [Playwright Best Practices](../playwright-best-practices/SKILL.md)
- [Error Handling](../error-handling/SKILL.md)
- [Selector Strategies](../selector-strategies/SKILL.md)
- [Troubleshooting Guide](../../docs/TROUBLESHOOTING.md)
- [Playwright Docs: Debugging](https://playwright.dev/docs/debug)
- [Playwright Docs: Trace Viewer](https://playwright.dev/docs/trace-viewer)More Debugging skills
diagnosing-bugs
mattpocock/skills
Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
explore-code
lllllllama/rigorpilot-skills
Rigor Improve implementation leaf skill for auditable candidate implementation in deep learning research repositories. Use when the researcher explicitly authorizes exploratory work on an isolated branch or worktree to transplant modules, adapt a backbone, add LoRA or adapter layers, replace a head, or stitch together meaningful low-risk migration ideas with rollback-aware records in `explore_outputs/`. Do not use for end-to-end exploration orchestration on top of `current_research`, trusted baseline reproduction, conservative debugging, environment setup, verified contribution claims, or default repository analysis.
safe-debug
lllllllama/rigorpilot-skills
Rigor Debug / Rigor Audit skill for deep learning research work. Use when the user pastes a traceback, terminal error, CUDA OOM, checkpoint load failure, shape mismatch, NaN loss symptom, or training failure and wants conservative diagnosis before any patching, with debug fixes clearly separated from research contributions. Do not use for broad refactoring, speculative adaptation, automatic exploratory patching, or general repository familiarization.

