AgentSkillsCN

debugging-troubleshooting

掌握 Playwright 测试调试与故障排除技巧。当您需要借助 Playwright Inspector、追踪查看器、视频分析、控制台/网络调试,或采用系统化的排查方法来调试不稳定或失败的测试时,可选用此技能。

SKILL.md
--- frontmatter
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

  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

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