Answer
Scenario: Debugging a Flaky Playwright Test
This is one of the most common real-world QA challenges. Here is a systematic approach.
Step 1 — Enable Trace on First Retry
TypeScript
// playwright.config.ts
export default defineConfig({
retries: 2,
use: {
trace: 'on-first-retry', // Capture trace when flakiness occurs
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
Now when the test fails and retries, you get a full trace.
Step 2 — Reproduce Locally
Bash
# Run the test 10 times to reproduce the flakiness
for i in {1..10}; do npx playwright test flaky.spec.ts; done
# OR use UI mode to watch it fail
npx playwright test flaky.spec.ts --ui
Step 3 — Analyze the Trace
Bash
npx playwright show-report
# Click on the failed test → open Trace
Look at:
- ✓Which step failed (the red step in the timeline)
- ✓DOM at that moment (did the element exist? Was it visible?)
- ✓Network tab (was an API call slow or failing?)
- ✓Console tab (JavaScript errors?)
- ✓Timing (was there an animation or transition?)
Step 4 — Common Causes and Fixes
Cause A — Hard-coded Timeout
TypeScript
// BAD
await page.waitForTimeout(2000);
await page.click('#submit');
// GOOD — wait for actual state
await page.locator('#submit').waitFor({ state: 'visible' });
await page.locator('#submit').click();
Cause B — Strict Mode: Multiple Elements Match
TypeScript
// BAD — fails if page has more than one '.btn'
await page.locator('.btn').click();
// Error: "strict mode violation: locator('.btn') resolved to 3 elements"
// GOOD — be specific
await page.locator('.modal .btn.primary').click();
await page.getByRole('button', { name: 'Submit' }).click();
Cause C — Race Condition (No Wait for Network)
TypeScript
// BAD — clicks before dynamic content loads
await page.goto('/products');
await page.locator('.product-card').first().click();
// Cards may not have loaded yet!
// GOOD — wait for dynamic content
await page.goto('/products');
await page.locator('.product-card').first().waitFor();
await page.locator('.product-card').first().click();
Cause D — Shared Test Data (Tests Not Isolated)
TypeScript
// BAD — test B assumes test A created this user
// If tests run in different order, it fails
// GOOD — each test creates its own independent data
test('edit user', async ({ request, page }) => {
const res = await request.post('/api/users', {
data: { email: `user-${Date.now()}@test.com` }
});
const { id } = await res.json();
await page.goto(`/users/${id}/edit`);
// Now test is fully self-contained
});
Cause E — Animation Interference
TypeScript
// Button slides in with CSS animation — not immediately clickable
await page.locator('.animated-cta').waitFor({ state: 'visible' });
// Still fails because it's mid-animation
// Fix: disable animations globally or wait for stable
await page.addStyleTag({ content: '* { animation: none !important; transition: none !important; }' });
Step 5 — Verify the Fix
Bash
# Run 20 times to confirm it's no longer flaky
for i in {1..20}; do npx playwright test fixed.spec.ts --retries=0; done
Zero failures = flakiness resolved.
