Answer
Handling Flaky Tests in Playwright
A flaky test is one that passes and fails inconsistently without any code change. Playwright has excellent tools to identify and fix them.
Step 1 — Identify Flaky Tests
TypeScript
// playwright.config.ts
export default defineConfig({
retries: 2,
use: {
trace: 'on-first-retry', // Capture trace when test needs to retry
},
reporter: [
['html'],
['json', { outputFile: 'results.json' }],
],
});
After a run, tests that "passed on retry" are flaky. Check the HTML report for these.
Common Causes and Fixes
Cause 1 — Hard-coded Sleep
TypeScript
// BAD — fixed wait doesn't adapt
await page.waitForTimeout(3000);
await page.click('#submit');
// GOOD — wait for actionable state
await page.getByRole('button', { name: 'Submit' }).click();
// Playwright auto-waits for visible, enabled, stable
Cause 2 — Race Condition with Animation
TypeScript
// BAD — clicking during animation
await page.locator('.animated-btn').click();
// GOOD — wait for animation to finish
await page.locator('.animated-btn').waitFor({ state: 'stable' });
await page.locator('.animated-btn').click();
// OR disable animations in config
use: {
launchOptions: {
args: ['--disable-animations']
}
}
Cause 3 — Data Dependency Between Tests
TypeScript
// BAD — test B depends on test A's data
test('A: create user', async ({ page }) => { /* creates user with id=1 */ });
test('B: check user 1', async ({ page }) => { /* assumes user 1 exists */ });
// GOOD — each test creates its own data
test('check user', async ({ page, request }) => {
// Create data needed for THIS test
const user = await request.post('/api/users', {
data: { email: `test-${Date.now()}@test.com` }
});
const { id } = await user.json();
await page.goto(`/users/${id}`);
});
Cause 4 — Shared State Between Tests
TypeScript
// BAD — localStorage persists between tests (if not using fresh context)
// GOOD — each test gets a fresh BrowserContext automatically
// Or explicitly clear state
test.beforeEach(async ({ context }) => {
await context.clearCookies();
await page.evaluate(() => localStorage.clear());
});
Cause 5 — Strict Mode Violation (Multiple Elements)
TypeScript
// BAD — fails if more than one element matches
await page.locator('.btn').click(); // Error if multiple .btn found
// GOOD — be specific
await page.locator('.btn').first().click();
await page.locator('.modal .btn').click(); // Scope to parent
Cause 6 — Network Timing Issues
TypeScript
// BAD — navigate before API response arrives
await page.click('#load-data');
await expect(page.locator('.data-table')).toBeVisible();
// GOOD — wait for the network response
const [response] = await Promise.all([
page.waitForResponse('**/api/data'),
page.click('#load-data'),
]);
await expect(page.locator('.data-table')).toBeVisible();
Tools to Debug Flaky Tests
Bash
# Run a test many times to reproduce flakiness
for i in {1..20}; do npx playwright test login.spec.ts; done
# Run with trace for debugging
npx playwright test --trace on
# UI Mode — watch test run interactively
npx playwright test --ui
