Flaky Waits and Timing in Playwright
Short answer
waitForTimeout(5000) masks bugs and still flakes under load. Use locator auto-wait, expect.poll on probes, and network/event signals—fix Arrange if timing never stabilizes.
Part of Common E2E testing gotchas.
Symptom
- Test fails at 29s, passes on retry
- “Element not visible” intermittent on CI only
- Suite slower every month from accumulated sleeps
Root cause
Arbitrary sleeps race real system latency (webhooks, indexing, animation). CI workers are slower than laptops—fixed sleeps fail both ways.
Fix patterns
1. Locator auto-wait (default)
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByTestId('order-confirmation')).toBeVisible();
Playwright retries until timeout—no manual sleep.
2. Poll authoritative state
await expect.poll(async () => {
const res = await request.get(`/api/test/probe-order/${runId}`);
return (await res.json()).status;
}, { timeout: 30_000, intervals: [500, 1000, 2000] }).toBe('paid');
Use after checkout, webhooks, async jobs (Stripe guide).
3. Wait on app signal
await page.locator('[data-stream-complete="true"]').last().waitFor();
await page.waitForResponse(resp => resp.url().includes('/api/search') && resp.ok());
4. Playwright clock (time-bound promos)
await page.clock.install({ time: new Date('2025-06-01T12:00:00Z') });
See dates and timezones.
When waitForTimeout is never OK in CI
Replace with poll or explicit condition. If only sleep “fixes” a test, the Arrange is wrong—shared data, missing seed, or missing webhook (world-state gotcha).
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
waitForTimeout after click | Still racing | expect.poll probe |
networkidle everywhere | SPAs never idle | Wait specific response |
| Huge global timeout | Hides slow leaks | Targeted poll timeout |
| Retry whole spec 3x | Masks Arrange bug | Fix data + waits |
CI note
Parallel workers amplify timing races—fix isolation before raising timeouts (GitHub Actions guide).
TestChimp workflow
/testchimp test replaces sleep-based fixes with probe polls when agents repair flaky specs—scenario markdown documents the async boundary to wait on.
Related
Frequently asked questions
Is networkidle deprecated for a reason?
Modern SPAs keep connections open—networkidle may never fire. Wait for specific API responses or probe state instead.
How long should expect.poll timeout be?
Match business SLA—webhooks often need 15-45s in test env. Use intervals array to avoid hammering probes.
Tests pass locally, fail in GitHub Actions—timing?
Often shared data or slower CI—check world-state first, then replace sleeps with polls.
Should we increase test timeout globally?
Fix root cause first. Global 120s timeouts slow every failure debug cycle.
Can ExploreChimp detect slow UX?
Yes—non-functional explorations on SmartTest paths surface latency regressions functional asserts miss.
Does TestChimp auto-remove waitForTimeout?
/testchimp test on PRs targets scenario-linked specs—agents refactor to polls when markdown plans specify probe Assert.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.