You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # Timing Debugging and Race Condition Fixes
  2. ## Principle
  3. Race conditions arise when tests make assumptions about asynchronous timing (network, animations, state updates). **Deterministic waiting** eliminates flakiness by explicitly waiting for observable events (network responses, element state changes) instead of arbitrary timeouts.
  4. ## Rationale
  5. **The Problem**: Tests pass locally but fail in CI (different timing), or pass/fail randomly (race conditions). Hard waits (`waitForTimeout`, `sleep`) mask timing issues without solving them.
  6. **The Solution**: Replace all hard waits with event-based waits (`waitForResponse`, `waitFor({ state })`). Implement network-first pattern (intercept before navigate). Use explicit state checks (loading spinner detached, data loaded). This makes tests deterministic regardless of network speed or system load.
  7. **Why This Matters**:
  8. - Eliminates flaky tests (0 tolerance for timing-based failures)
  9. - Works consistently across environments (local, CI, production-like)
  10. - Faster test execution (no unnecessary waits)
  11. - Clearer test intent (explicit about what we're waiting for)
  12. ## Pattern Examples
  13. ### Example 1: Race Condition Identification (Network-First Pattern)
  14. **Context**: Prevent race conditions by intercepting network requests before navigation
  15. **Implementation**:
  16. ```typescript
  17. // tests/timing/race-condition-prevention.spec.ts
  18. import { test, expect } from '@playwright/test';
  19. test.describe('Race Condition Prevention Patterns', () => {
  20. test('❌ Anti-Pattern: Navigate then intercept (race condition)', async ({ page, context }) => {
  21. // BAD: Navigation starts before interception ready
  22. await page.goto('/products'); // ⚠️ Race! API might load before route is set
  23. await context.route('**/api/products', (route) => {
  24. route.fulfill({ status: 200, body: JSON.stringify({ products: [] }) });
  25. });
  26. // Test may see real API response or mock (non-deterministic)
  27. });
  28. test('✅ Pattern: Intercept BEFORE navigate (deterministic)', async ({ page, context }) => {
  29. // GOOD: Interception ready before navigation
  30. await context.route('**/api/products', (route) => {
  31. route.fulfill({
  32. status: 200,
  33. contentType: 'application/json',
  34. body: JSON.stringify({
  35. products: [
  36. { id: 1, name: 'Product A', price: 29.99 },
  37. { id: 2, name: 'Product B', price: 49.99 },
  38. ],
  39. }),
  40. });
  41. });
  42. const responsePromise = page.waitForResponse('**/api/products');
  43. await page.goto('/products'); // Navigation happens AFTER route is ready
  44. await responsePromise; // Explicit wait for network
  45. // Test sees mock response reliably (deterministic)
  46. await expect(page.getByText('Product A')).toBeVisible();
  47. });
  48. test('✅ Pattern: Wait for element state change (loading → loaded)', async ({ page }) => {
  49. await page.goto('/dashboard');
  50. // Wait for loading indicator to appear (confirms load started)
  51. await page.getByTestId('loading-spinner').waitFor({ state: 'visible' });
  52. // Wait for loading indicator to disappear (confirms load complete)
  53. await page.getByTestId('loading-spinner').waitFor({ state: 'detached' });
  54. // Content now reliably visible
  55. await expect(page.getByTestId('dashboard-data')).toBeVisible();
  56. });
  57. test('✅ Pattern: Explicit visibility check (not just presence)', async ({ page }) => {
  58. await page.goto('/modal-demo');
  59. await page.getByRole('button', { name: 'Open Modal' }).click();
  60. // ❌ Bad: Element exists but may not be visible yet
  61. // await expect(page.getByTestId('modal')).toBeAttached()
  62. // ✅ Good: Wait for visibility (accounts for animations)
  63. await expect(page.getByTestId('modal')).toBeVisible();
  64. await expect(page.getByRole('heading', { name: 'Modal Title' })).toBeVisible();
  65. });
  66. test('❌ Anti-Pattern: waitForLoadState("networkidle") in SPAs', async ({ page }) => {
  67. // ⚠️ Deprecated for SPAs (WebSocket connections never idle)
  68. // await page.goto('/dashboard')
  69. // await page.waitForLoadState('networkidle') // May timeout in SPAs
  70. // ✅ Better: Wait for specific API response
  71. const responsePromise = page.waitForResponse('**/api/dashboard');
  72. await page.goto('/dashboard');
  73. await responsePromise;
  74. await expect(page.getByText('Dashboard loaded')).toBeVisible();
  75. });
  76. });
  77. ```
  78. **Key Points**:
  79. - Network-first: ALWAYS intercept before navigate (prevents race conditions)
  80. - State changes: Wait for loading spinner detached (explicit load completion)
  81. - Visibility vs presence: `toBeVisible()` accounts for animations, `toBeAttached()` doesn't
  82. - Avoid networkidle: Unreliable in SPAs (WebSocket, polling connections)
  83. - Explicit waits: Document exactly what we're waiting for
  84. ---
  85. ### Example 2: Deterministic Waiting Patterns (Event-Based, Not Time-Based)
  86. **Context**: Replace all hard waits with observable event waits
  87. **Implementation**:
  88. ```typescript
  89. // tests/timing/deterministic-waits.spec.ts
  90. import { test, expect } from '@playwright/test';
  91. test.describe('Deterministic Waiting Patterns', () => {
  92. test('waitForResponse() with URL pattern', async ({ page }) => {
  93. const responsePromise = page.waitForResponse('**/api/products');
  94. await page.goto('/products');
  95. await responsePromise; // Deterministic (waits for exact API call)
  96. await expect(page.getByText('Products loaded')).toBeVisible();
  97. });
  98. test('waitForResponse() with predicate function', async ({ page }) => {
  99. const responsePromise = page.waitForResponse((resp) => resp.url().includes('/api/search') && resp.status() === 200);
  100. await page.goto('/search');
  101. await page.getByPlaceholder('Search').fill('laptop');
  102. await page.getByRole('button', { name: 'Search' }).click();
  103. await responsePromise; // Wait for successful search response
  104. await expect(page.getByTestId('search-results')).toBeVisible();
  105. });
  106. test('waitForFunction() for custom conditions', async ({ page }) => {
  107. await page.goto('/dashboard');
  108. // Wait for custom JavaScript condition
  109. await page.waitForFunction(() => {
  110. const element = document.querySelector('[data-testid="user-count"]');
  111. return element && parseInt(element.textContent || '0') > 0;
  112. });
  113. // User count now loaded
  114. await expect(page.getByTestId('user-count')).not.toHaveText('0');
  115. });
  116. test('waitFor() element state (attached, visible, hidden, detached)', async ({ page }) => {
  117. await page.goto('/products');
  118. // Wait for element to be attached to DOM
  119. await page.getByTestId('product-list').waitFor({ state: 'attached' });
  120. // Wait for element to be visible (animations complete)
  121. await page.getByTestId('product-list').waitFor({ state: 'visible' });
  122. // Perform action
  123. await page.getByText('Product A').click();
  124. // Wait for modal to be hidden (close animation complete)
  125. await page.getByTestId('modal').waitFor({ state: 'hidden' });
  126. });
  127. test('Cypress: cy.wait() with aliased intercepts', async () => {
  128. // Cypress example (not Playwright)
  129. /*
  130. cy.intercept('GET', '/api/products').as('getProducts')
  131. cy.visit('/products')
  132. cy.wait('@getProducts') // Deterministic wait for specific request
  133. cy.get('[data-testid="product-list"]').should('be.visible')
  134. */
  135. });
  136. });
  137. ```
  138. **Key Points**:
  139. - `waitForResponse()`: Wait for specific API calls (URL pattern or predicate)
  140. - `waitForFunction()`: Wait for custom JavaScript conditions
  141. - `waitFor({ state })`: Wait for element state changes (attached, visible, hidden, detached)
  142. - Cypress `cy.wait('@alias')`: Deterministic wait for aliased intercepts
  143. - All waits are event-based (not time-based)
  144. ---
  145. ### Example 3: Timing Anti-Patterns (What NEVER to Do)
  146. **Context**: Common timing mistakes that cause flakiness
  147. **Problem Examples**:
  148. ```typescript
  149. // tests/timing/anti-patterns.spec.ts
  150. import { test, expect } from '@playwright/test';
  151. test.describe('Timing Anti-Patterns to Avoid', () => {
  152. test('❌ NEVER: page.waitForTimeout() (arbitrary delay)', async ({ page }) => {
  153. await page.goto('/dashboard');
  154. // ❌ Bad: Arbitrary 3-second wait (flaky)
  155. // await page.waitForTimeout(3000)
  156. // Problem: Might be too short (CI slower) or too long (wastes time)
  157. // ✅ Good: Wait for observable event
  158. await page.waitForResponse('**/api/dashboard');
  159. await expect(page.getByText('Dashboard loaded')).toBeVisible();
  160. });
  161. test('❌ NEVER: cy.wait(number) without alias (arbitrary delay)', async () => {
  162. // Cypress example
  163. /*
  164. // ❌ Bad: Arbitrary delay
  165. cy.visit('/products')
  166. cy.wait(2000) // Flaky!
  167. // ✅ Good: Wait for specific request
  168. cy.intercept('GET', '/api/products').as('getProducts')
  169. cy.visit('/products')
  170. cy.wait('@getProducts') // Deterministic
  171. */
  172. });
  173. test('❌ NEVER: Multiple hard waits in sequence (compounding delays)', async ({ page }) => {
  174. await page.goto('/checkout');
  175. // ❌ Bad: Stacked hard waits (6+ seconds wasted)
  176. // await page.waitForTimeout(2000) // Wait for form
  177. // await page.getByTestId('email').fill('test@example.com')
  178. // await page.waitForTimeout(1000) // Wait for validation
  179. // await page.getByTestId('submit').click()
  180. // await page.waitForTimeout(3000) // Wait for redirect
  181. // ✅ Good: Event-based waits (no wasted time)
  182. await page.getByTestId('checkout-form').waitFor({ state: 'visible' });
  183. await page.getByTestId('email').fill('test@example.com');
  184. await page.waitForResponse('**/api/validate-email');
  185. await page.getByTestId('submit').click();
  186. await page.waitForURL('**/confirmation');
  187. });
  188. test('❌ NEVER: waitForLoadState("networkidle") in SPAs', async ({ page }) => {
  189. // ❌ Bad: Unreliable in SPAs (WebSocket connections never idle)
  190. // await page.goto('/dashboard')
  191. // await page.waitForLoadState('networkidle') // Timeout in SPAs!
  192. // ✅ Good: Wait for specific API responses
  193. await page.goto('/dashboard');
  194. await page.waitForResponse('**/api/dashboard');
  195. await page.waitForResponse('**/api/user');
  196. await expect(page.getByTestId('dashboard-content')).toBeVisible();
  197. });
  198. test('❌ NEVER: Sleep/setTimeout in tests', async ({ page }) => {
  199. await page.goto('/products');
  200. // ❌ Bad: Node.js sleep (blocks test thread)
  201. // await new Promise(resolve => setTimeout(resolve, 2000))
  202. // ✅ Good: Playwright auto-waits for element
  203. await expect(page.getByText('Products loaded')).toBeVisible();
  204. });
  205. });
  206. ```
  207. **Why These Fail**:
  208. - **Hard waits**: Arbitrary timeouts (too short → flaky, too long → slow)
  209. - **Stacked waits**: Compound delays (wasteful, unreliable)
  210. - **networkidle**: Broken in SPAs (WebSocket/polling never idle)
  211. - **Sleep**: Blocks execution (wastes time, doesn't solve race conditions)
  212. **Better Approach**: Use event-based waits from examples above
  213. ---
  214. ## Async Debugging Techniques
  215. ### Technique 1: Promise Chain Analysis
  216. ```typescript
  217. test('debug async waterfall with console logs', async ({ page }) => {
  218. console.log('1. Starting navigation...');
  219. await page.goto('/products');
  220. console.log('2. Waiting for API response...');
  221. const response = await page.waitForResponse('**/api/products');
  222. console.log('3. API responded:', response.status());
  223. console.log('4. Waiting for UI update...');
  224. await expect(page.getByText('Products loaded')).toBeVisible();
  225. console.log('5. Test complete');
  226. // Console output shows exactly where timing issue occurs
  227. });
  228. ```
  229. ### Technique 2: Network Waterfall Inspection (DevTools)
  230. ```typescript
  231. test('inspect network timing with trace viewer', async ({ page }) => {
  232. await page.goto('/dashboard');
  233. // Generate trace for analysis
  234. // npx playwright test --trace on
  235. // npx playwright show-trace trace.zip
  236. // In trace viewer:
  237. // 1. Check Network tab for API call timing
  238. // 2. Identify slow requests (>1s response time)
  239. // 3. Find race conditions (overlapping requests)
  240. // 4. Verify request order (dependencies)
  241. });
  242. ```
  243. ### Technique 3: Trace Viewer for Timing Visualization
  244. ```typescript
  245. test('use trace viewer to debug timing', async ({ page }) => {
  246. // Run with trace: npx playwright test --trace on
  247. await page.goto('/checkout');
  248. await page.getByTestId('submit').click();
  249. // In trace viewer, examine:
  250. // - Timeline: See exact timing of each action
  251. // - Snapshots: Hover to see DOM state at each moment
  252. // - Network: Identify slow/failed requests
  253. // - Console: Check for async errors
  254. await expect(page.getByText('Success')).toBeVisible();
  255. });
  256. ```
  257. ---
  258. ## Race Condition Checklist
  259. Before deploying tests:
  260. - [ ] **Network-first pattern**: All routes intercepted BEFORE navigation (no race conditions)
  261. - [ ] **Explicit waits**: Every navigation followed by `waitForResponse()` or state check
  262. - [ ] **No hard waits**: Zero instances of `waitForTimeout()`, `cy.wait(number)`, `sleep()`
  263. - [ ] **Element state waits**: Loading spinners use `waitFor({ state: 'detached' })`
  264. - [ ] **Visibility checks**: Use `toBeVisible()` (accounts for animations), not just `toBeAttached()`
  265. - [ ] **Response validation**: Wait for successful responses (`resp.ok()` or `status === 200`)
  266. - [ ] **Trace viewer analysis**: Generate traces to identify timing issues (network waterfall, console errors)
  267. - [ ] **CI/local parity**: Tests pass reliably in both environments (no timing assumptions)
  268. ## Integration Points
  269. - **Used in workflows**: `*automate` (healing timing failures), `*test-review` (detect hard wait anti-patterns), `*framework` (configure timeout standards)
  270. - **Related fragments**: `test-healing-patterns.md` (race condition diagnosis), `network-first.md` (interception patterns), `playwright-config.md` (timeout configuration), `visual-debugging.md` (trace viewer analysis)
  271. - **Tools**: Playwright Inspector (`--debug`), Trace Viewer (`--trace on`), DevTools Network tab
  272. _Source: Playwright timing best practices, network-first pattern from test-resources-for-ai, production race condition debugging_