@assertrx/dino 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,883 @@
1
+ # Flaky Tests
2
+
3
+ > **When to use**: A test passes sometimes and fails other times. You need to diagnose the root cause, fix it, and prevent it from happening again.
4
+ > **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```bash
9
+ # Burn-in test — run 10 times to expose flakiness
10
+ npx playwright test tests/checkout.spec.ts --repeat-each=10
11
+
12
+ # Run with retries to catch intermittent failures
13
+ npx playwright test --retries=3
14
+
15
+ # Run single test in isolation to rule out state leaks
16
+ npx playwright test tests/checkout.spec.ts --grep "adds item" --workers=1
17
+
18
+ # Best Playwright 1.59+ trace mode for flaky tests
19
+ npx playwright test --retries=3 --trace=retain-on-failure-and-retries
20
+
21
+ # Run with tracing on every attempt only when you need maximum detail
22
+ npx playwright test --retries=3 --trace=on
23
+
24
+ # Run in fully parallel mode to expose isolation issues
25
+ npx playwright test --fully-parallel --workers=4
26
+
27
+ # List flaky tests (tests that failed then passed on retry)
28
+ npx playwright test --retries=2 --reporter=json | jq '.suites[].specs[] | select(.ok == true and (.tests[].results | length > 1))'
29
+ ```
30
+
31
+ ## Patterns
32
+
33
+ ### Flakiness Taxonomy
34
+
35
+ Every flaky test falls into one of four categories. Identify the category first, then apply the matching fix.
36
+
37
+ | Category | Symptom | Typical Root Cause | Diagnosis Method |
38
+ |---|---|---|---|
39
+ | **Timing / Async** | Fails intermittently everywhere | Race conditions, missing `await`, arbitrary waits | Fails locally with `--repeat-each=20` |
40
+ | **Test Isolation** | Fails only when run with other tests, passes alone | Shared mutable state, data collisions, test ordering dependency | Passes with `--workers=1 --grep "this test"`, fails in full suite |
41
+ | **Environment** | Fails only in CI, passes locally | Different OS, viewport, fonts, network latency, missing dependencies | Compare CI screenshots/traces with local; run in Docker locally |
42
+ | **Infrastructure** | Random failures unrelated to test logic | Browser crash, OOM, DNS resolution, file system race | Failures have no pattern; error messages reference browser internals |
43
+
44
+ ### Diagnosis Flowchart
45
+
46
+ Follow this decision tree to identify which category your flaky test belongs to.
47
+
48
+ ```
49
+ Test is flaky
50
+ |
51
+ +-- Does it fail locally with --repeat-each=20?
52
+ | |
53
+ | +-- YES --> TIMING / ASYNC issue
54
+ | | - Missing await
55
+ | | - Using waitForTimeout instead of assertions
56
+ | | - Race condition between action and assertion
57
+ | | - Not waiting for network response before asserting
58
+ | |
59
+ | +-- NO --> Does it fail only in CI?
60
+ | |
61
+ | +-- YES --> ENVIRONMENT issue
62
+ | | - Different viewport/screen size
63
+ | | - Missing fonts causing layout shift
64
+ | | - Slower CI machines hitting timeouts
65
+ | | - External services unavailable
66
+ | |
67
+ | +-- NO --> Does it fail only when run with other tests?
68
+ | |
69
+ | +-- YES --> ISOLATION issue
70
+ | | - Shared mutable state (module-level variables)
71
+ | | - Database/API state from previous test
72
+ | | - localStorage/cookies leaking between tests
73
+ | | - Parallel tests colliding on unique constraints
74
+ | |
75
+ | +-- NO --> INFRASTRUCTURE issue
76
+ | - Browser process crash
77
+ | - Out of memory
78
+ | - File system or network instability
79
+ | - Flaky third-party service
80
+ ```
81
+
82
+ ### Playwright 1.59 Trace Retention Strategy
83
+
84
+ For flaky tests on Playwright 1.59+, prefer `trace: 'retain-on-failure-and-retries'` over `trace: 'on'` when you are comparing failed attempts with passing retries. It keeps the runs that matter without storing every passing trace in the suite.
85
+
86
+ ```typescript
87
+ import { defineConfig } from '@playwright/test';
88
+
89
+ export default defineConfig({
90
+ retries: process.env.CI ? 2 : 0,
91
+ use: {
92
+ trace: process.env.CI
93
+ ? 'retain-on-failure-and-retries'
94
+ : 'on-first-retry',
95
+ },
96
+ });
97
+ ```
98
+
99
+ This is especially effective when a test fails once, passes on retry, and you need to diff those attempts in Trace Viewer.
100
+
101
+ ### Capture `errorContext` for Intermittent Assertion Failures (Playwright 1.60+)
102
+
103
+ When a flaky test fails only sometimes on an `expect()` matcher, the bare error message rarely explains *why*. Playwright 1.60 attaches `errorContext` to each `testInfo` error — including the aria snapshot of the element at the moment the matcher failed. Log it from `afterEach` so every flaky failure leaves a breadcrumb, even on the runs you can't reproduce locally.
104
+
105
+ ```typescript
106
+ import { test } from '@playwright/test';
107
+
108
+ test.afterEach(async ({}, testInfo) => {
109
+ if (testInfo.status !== testInfo.expectedStatus) {
110
+ for (const err of testInfo.errors) {
111
+ if (err.errorContext) {
112
+ await testInfo.attach('failure-context', {
113
+ body: err.errorContext,
114
+ contentType: 'text/plain',
115
+ });
116
+ }
117
+ }
118
+ }
119
+ });
120
+ ```
121
+
122
+ Pair this with `trace: 'retain-on-failure-and-retries'`: the trace shows the timeline, and `errorContext` captures the receiver's accessibility state at the exact failure point.
123
+
124
+ ### Use UI Mode and Trace Viewer Filters
125
+
126
+ When debugging a noisy trace or a long UI Mode run, use the newer filtering options to focus on the failing test, relevant actions, or a specific assertion sequence instead of scanning the full event stream manually.
127
+
128
+ ### Fix: Timing and Async Issues
129
+
130
+ **Use when**: The test fails locally with `--repeat-each=20`, or you see `waitForTimeout`, missing `await`, or race conditions.
131
+
132
+ The most common source of flakiness. The fix is always the same: replace arbitrary waits and manual checks with Playwright's auto-retrying mechanisms.
133
+
134
+ **TypeScript**
135
+ ```typescript
136
+ import { test, expect } from '@playwright/test';
137
+
138
+ // ---- FIX 1: Replace waitForTimeout with assertions ----
139
+
140
+ // BAD — arbitrary delay, fails on slow machines, wastes time on fast ones
141
+ test('bad: uses arbitrary wait', async ({ page }) => {
142
+ await page.goto('/dashboard');
143
+ await page.getByRole('button', { name: 'Refresh' }).click();
144
+ await page.waitForTimeout(3000); // hoping data loads in 3s
145
+ await expect(page.getByTestId('data-table')).toBeVisible();
146
+ });
147
+
148
+ // GOOD — auto-retrying assertion waits exactly as long as needed
149
+ test('good: uses auto-retrying assertion', async ({ page }) => {
150
+ await page.goto('/dashboard');
151
+ await page.getByRole('button', { name: 'Refresh' }).click();
152
+ await expect(page.getByTestId('data-table')).toBeVisible();
153
+ });
154
+
155
+ // ---- FIX 2: Wait for network responses before asserting ----
156
+
157
+ // BAD — clicks button and immediately asserts, but data comes from API
158
+ test('bad: does not wait for API response', async ({ page }) => {
159
+ await page.goto('/users');
160
+ await page.getByRole('button', { name: 'Load More' }).click();
161
+ // Flaky: API response may not have arrived yet
162
+ await expect(page.getByRole('listitem')).toHaveCount(20);
163
+ });
164
+
165
+ // GOOD — waits for the specific API response that populates the data
166
+ test('good: waits for API response', async ({ page }) => {
167
+ await page.goto('/users');
168
+
169
+ const responsePromise = page.waitForResponse(
170
+ (resp) => resp.url().includes('/api/users') && resp.status() === 200
171
+ );
172
+ await page.getByRole('button', { name: 'Load More' }).click();
173
+ await responsePromise;
174
+
175
+ await expect(page.getByRole('listitem')).toHaveCount(20);
176
+ });
177
+
178
+ // ---- FIX 3: Handle animations and transitions ----
179
+
180
+ // BAD — element exists but is mid-animation, click lands on wrong target
181
+ test('bad: clicks during animation', async ({ page }) => {
182
+ await page.goto('/modal-demo');
183
+ await page.getByRole('button', { name: 'Open' }).click();
184
+ // Modal is animating in — click may miss the button inside it
185
+ await page.getByRole('button', { name: 'Confirm' }).click();
186
+ });
187
+
188
+ // GOOD — wait for the modal to be fully stable before interacting
189
+ test('good: waits for stable state', async ({ page }) => {
190
+ await page.goto('/modal-demo');
191
+ await page.getByRole('button', { name: 'Open' }).click();
192
+ // toBeVisible auto-waits for stability (no animation in progress)
193
+ await expect(page.getByRole('dialog')).toBeVisible();
194
+ await page.getByRole('button', { name: 'Confirm' }).click();
195
+ });
196
+
197
+ // ---- FIX 4: Use toPass() for multi-step assertions that must succeed together ----
198
+
199
+ test('good: retry entire assertion block', async ({ page }) => {
200
+ await page.goto('/search');
201
+
202
+ await expect(async () => {
203
+ await page.getByLabel('Search').fill('playwright');
204
+ await page.getByRole('button', { name: 'Search' }).click();
205
+ await expect(page.getByTestId('result-count')).toHaveText('10 results');
206
+ }).toPass({
207
+ timeout: 15_000,
208
+ intervals: [1_000, 2_000, 5_000],
209
+ });
210
+ });
211
+ ```
212
+
213
+ **JavaScript**
214
+ ```javascript
215
+ const { test, expect } = require('@playwright/test');
216
+
217
+ // FIX 1: Replace waitForTimeout with assertions
218
+ test('good: uses auto-retrying assertion', async ({ page }) => {
219
+ await page.goto('/dashboard');
220
+ await page.getByRole('button', { name: 'Refresh' }).click();
221
+ await expect(page.getByTestId('data-table')).toBeVisible();
222
+ });
223
+
224
+ // FIX 2: Wait for network responses before asserting
225
+ test('good: waits for API response', async ({ page }) => {
226
+ await page.goto('/users');
227
+
228
+ const responsePromise = page.waitForResponse(
229
+ (resp) => resp.url().includes('/api/users') && resp.status() === 200
230
+ );
231
+ await page.getByRole('button', { name: 'Load More' }).click();
232
+ await responsePromise;
233
+
234
+ await expect(page.getByRole('listitem')).toHaveCount(20);
235
+ });
236
+
237
+ // FIX 3: Handle animations and transitions
238
+ test('good: waits for stable state', async ({ page }) => {
239
+ await page.goto('/modal-demo');
240
+ await page.getByRole('button', { name: 'Open' }).click();
241
+ await expect(page.getByRole('dialog')).toBeVisible();
242
+ await page.getByRole('button', { name: 'Confirm' }).click();
243
+ });
244
+
245
+ // FIX 4: Use toPass() for multi-step assertions
246
+ test('good: retry entire assertion block', async ({ page }) => {
247
+ await page.goto('/search');
248
+
249
+ await expect(async () => {
250
+ await page.getByLabel('Search').fill('playwright');
251
+ await page.getByRole('button', { name: 'Search' }).click();
252
+ await expect(page.getByTestId('result-count')).toHaveText('10 results');
253
+ }).toPass({
254
+ timeout: 15_000,
255
+ intervals: [1_000, 2_000, 5_000],
256
+ });
257
+ });
258
+ ```
259
+
260
+ ### Fix: Test Isolation Issues
261
+
262
+ **Use when**: The test passes when run alone (`--grep "test name"`) but fails when run with other tests, or fails only in parallel mode.
263
+
264
+ Isolation issues come from shared state: module-level variables, database rows, localStorage, cookies, or file system artifacts.
265
+
266
+ **TypeScript**
267
+ ```typescript
268
+ import { test as base, expect } from '@playwright/test';
269
+
270
+ // ---- FIX 1: Unique test data per test ----
271
+
272
+ // BAD — all parallel tests use the same email, causing unique constraint violations
273
+ test('bad: hardcoded data', async ({ page }) => {
274
+ await page.goto('/register');
275
+ await page.getByLabel('Email').fill('test@example.com');
276
+ await page.getByRole('button', { name: 'Register' }).click();
277
+ await expect(page.getByText('Welcome')).toBeVisible();
278
+ });
279
+
280
+ // GOOD — unique email per test run
281
+ test('good: unique data per test', async ({ page }) => {
282
+ const email = `test-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
283
+
284
+ await page.goto('/register');
285
+ await page.getByLabel('Email').fill(email);
286
+ await page.getByRole('button', { name: 'Register' }).click();
287
+ await expect(page.getByText('Welcome')).toBeVisible();
288
+ });
289
+
290
+ // ---- FIX 2: Worker-scoped fixtures for shared expensive resources ----
291
+
292
+ type WorkerFixtures = {
293
+ workerAccount: { email: string; id: string };
294
+ };
295
+
296
+ export const test = base.extend<{}, WorkerFixtures>({
297
+ workerAccount: [async ({ request }, use) => {
298
+ const email = `worker-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
299
+ const response = await request.post('/api/users', {
300
+ data: { email, password: 'TestP@ss123!' },
301
+ });
302
+ const account = await response.json();
303
+
304
+ await use({ email, id: account.id });
305
+
306
+ // Cleanup after all tests in this worker are done
307
+ await request.delete(`/api/users/${account.id}`);
308
+ }, { scope: 'worker' }],
309
+ });
310
+
311
+ // ---- FIX 3: Clean up state in fixture teardown ----
312
+
313
+ export const testWithCleanup = base.extend({
314
+ cleanPage: async ({ page }, use) => {
315
+ await use(page);
316
+
317
+ // Teardown: clear all client-side state
318
+ await page.evaluate(() => {
319
+ localStorage.clear();
320
+ sessionStorage.clear();
321
+ });
322
+ await page.context().clearCookies();
323
+ },
324
+ });
325
+
326
+ // ---- FIX 4: Isolate tests that cannot run in parallel ----
327
+
328
+ import { test } from '@playwright/test';
329
+
330
+ // Use serial mode ONLY for tests that genuinely depend on shared state
331
+ // (e.g., a multi-step wizard where each test is one step)
332
+ test.describe.serial('checkout wizard', () => {
333
+ test('step 1: add items', async ({ page }) => {
334
+ await page.goto('/shop');
335
+ await page.getByRole('button', { name: 'Add Widget' }).click();
336
+ await expect(page.getByTestId('cart-count')).toHaveText('1');
337
+ });
338
+
339
+ test('step 2: enter shipping', async ({ page }) => {
340
+ await page.goto('/checkout/shipping');
341
+ await page.getByLabel('Address').fill('123 Test St');
342
+ await page.getByRole('button', { name: 'Continue' }).click();
343
+ });
344
+ });
345
+ ```
346
+
347
+ **JavaScript**
348
+ ```javascript
349
+ const { test: base, expect } = require('@playwright/test');
350
+
351
+ // FIX 1: Unique test data per test
352
+ test('good: unique data per test', async ({ page }) => {
353
+ const email = `test-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
354
+
355
+ await page.goto('/register');
356
+ await page.getByLabel('Email').fill(email);
357
+ await page.getByRole('button', { name: 'Register' }).click();
358
+ await expect(page.getByText('Welcome')).toBeVisible();
359
+ });
360
+
361
+ // FIX 2: Worker-scoped fixtures for shared expensive resources
362
+ const test = base.extend({
363
+ workerAccount: [async ({ request }, use) => {
364
+ const email = `worker-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`;
365
+ const response = await request.post('/api/users', {
366
+ data: { email, password: 'TestP@ss123!' },
367
+ });
368
+ const account = await response.json();
369
+
370
+ await use({ email, id: account.id });
371
+
372
+ await request.delete(`/api/users/${account.id}`);
373
+ }, { scope: 'worker' }],
374
+ });
375
+
376
+ module.exports = { test, expect };
377
+ ```
378
+
379
+ ### Fix: Environment Issues
380
+
381
+ **Use when**: The test passes locally but fails in CI, or fails on certain operating systems, viewports, or machines.
382
+
383
+ Environment flakiness stems from differences in rendering, timing, available resources, or external service availability between your local machine and CI.
384
+
385
+ **TypeScript**
386
+ ```typescript
387
+ // playwright.config.ts — environment-consistent configuration
388
+ import { defineConfig, devices } from '@playwright/test';
389
+
390
+ export default defineConfig({
391
+ // ---- FIX 1: Disable animations for deterministic behavior ----
392
+ use: {
393
+ // Disable CSS animations and transitions
394
+ contextOptions: {
395
+ reducedMotion: 'reduce',
396
+ },
397
+ },
398
+
399
+ // ---- FIX 2: Consistent viewport across environments ----
400
+ projects: [
401
+ {
402
+ name: 'chromium',
403
+ use: {
404
+ ...devices['Desktop Chrome'],
405
+ // Explicit viewport prevents layout differences between local and CI
406
+ viewport: { width: 1280, height: 720 },
407
+ },
408
+ },
409
+ ],
410
+
411
+ // ---- FIX 3: Use webServer to start app in CI ----
412
+ webServer: {
413
+ command: 'npm run start',
414
+ url: 'http://localhost:3000',
415
+ reuseExistingServer: !process.env.CI,
416
+ timeout: 120_000,
417
+ },
418
+
419
+ // ---- FIX 4: Higher timeouts for slower CI machines ----
420
+ timeout: process.env.CI ? 60_000 : 30_000,
421
+ expect: {
422
+ timeout: process.env.CI ? 10_000 : 5_000,
423
+ },
424
+ });
425
+ ```
426
+
427
+ ```typescript
428
+ // tests/fixtures/stub-externals.ts — stub external services
429
+ import { test as base, expect } from '@playwright/test';
430
+
431
+ export const test = base.extend({
432
+ // Auto fixture: block all external services in every test
433
+ stubExternals: [async ({ page }, use) => {
434
+ // Block third-party scripts that vary between environments
435
+ await page.route(/google-analytics|segment|hotjar|intercom/, (route) =>
436
+ route.abort()
437
+ );
438
+
439
+ // Stub flaky external API with consistent response
440
+ await page.route('**/api.external-service.com/**', (route) =>
441
+ route.fulfill({
442
+ status: 200,
443
+ contentType: 'application/json',
444
+ body: JSON.stringify({ status: 'ok', data: [] }),
445
+ })
446
+ );
447
+
448
+ await use();
449
+ }, { auto: true }],
450
+ });
451
+
452
+ export { expect };
453
+ ```
454
+
455
+ **JavaScript**
456
+ ```javascript
457
+ // playwright.config.js
458
+ const { defineConfig, devices } = require('@playwright/test');
459
+
460
+ module.exports = defineConfig({
461
+ use: {
462
+ contextOptions: {
463
+ reducedMotion: 'reduce',
464
+ },
465
+ },
466
+ projects: [
467
+ {
468
+ name: 'chromium',
469
+ use: {
470
+ ...devices['Desktop Chrome'],
471
+ viewport: { width: 1280, height: 720 },
472
+ },
473
+ },
474
+ ],
475
+ webServer: {
476
+ command: 'npm run start',
477
+ url: 'http://localhost:3000',
478
+ reuseExistingServer: !process.env.CI,
479
+ timeout: 120_000,
480
+ },
481
+ timeout: process.env.CI ? 60_000 : 30_000,
482
+ expect: {
483
+ timeout: process.env.CI ? 10_000 : 5_000,
484
+ },
485
+ });
486
+ ```
487
+
488
+ ```javascript
489
+ // tests/fixtures/stub-externals.js
490
+ const { test: base, expect } = require('@playwright/test');
491
+
492
+ const test = base.extend({
493
+ stubExternals: [async ({ page }, use) => {
494
+ await page.route(/google-analytics|segment|hotjar|intercom/, (route) =>
495
+ route.abort()
496
+ );
497
+
498
+ await page.route('**/api.external-service.com/**', (route) =>
499
+ route.fulfill({
500
+ status: 200,
501
+ contentType: 'application/json',
502
+ body: JSON.stringify({ status: 'ok', data: [] }),
503
+ })
504
+ );
505
+
506
+ await use();
507
+ }, { auto: true }],
508
+ });
509
+
510
+ module.exports = { test, expect };
511
+ ```
512
+
513
+ ### Detection Strategies
514
+
515
+ **Use when**: You suspect flakiness but the test does not fail consistently, or you want to validate a fix actually eliminated the flakiness.
516
+
517
+ **TypeScript**
518
+ ```typescript
519
+ // ---- Strategy 1: Burn-in testing with --repeat-each ----
520
+ // Run a test 20 times. If it fails even once, it has a flakiness bug.
521
+ // npx playwright test tests/checkout.spec.ts --repeat-each=20
522
+
523
+ // ---- Strategy 2: Retry configuration to catch intermittent failures ----
524
+ // playwright.config.ts
525
+ import { defineConfig } from '@playwright/test';
526
+
527
+ export default defineConfig({
528
+ // Retries in CI surface flaky tests in the report
529
+ retries: process.env.CI ? 2 : 0,
530
+
531
+ // Reporter shows which tests needed retries
532
+ reporter: process.env.CI
533
+ ? [['html', { open: 'never' }], ['json', { outputFile: 'results.json' }]]
534
+ : [['html', { open: 'on-failure' }]],
535
+ });
536
+
537
+ // ---- Strategy 3: Custom reporter to track flaky test metrics ----
538
+ // flaky-reporter.ts
539
+ import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
540
+
541
+ class FlakyReporter implements Reporter {
542
+ private flakyTests: { name: string; file: string; retries: number }[] = [];
543
+
544
+ onTestEnd(test: TestCase, result: TestResult) {
545
+ if (result.retry > 0 && result.status === 'passed') {
546
+ this.flakyTests.push({
547
+ name: test.title,
548
+ file: test.location.file,
549
+ retries: result.retry,
550
+ });
551
+ }
552
+ }
553
+
554
+ onEnd() {
555
+ if (this.flakyTests.length > 0) {
556
+ console.log('\n--- FLAKY TESTS ---');
557
+ for (const t of this.flakyTests) {
558
+ console.log(` ${t.file} > "${t.name}" (needed ${t.retries} retries)`);
559
+ }
560
+ console.log(`Total flaky: ${this.flakyTests.length}`);
561
+ }
562
+ }
563
+ }
564
+
565
+ export default FlakyReporter;
566
+ ```
567
+
568
+ ```typescript
569
+ // playwright.config.ts — register custom flaky reporter
570
+ import { defineConfig } from '@playwright/test';
571
+
572
+ export default defineConfig({
573
+ retries: process.env.CI ? 2 : 0,
574
+ reporter: [
575
+ ['html'],
576
+ ['./flaky-reporter.ts'],
577
+ ],
578
+ });
579
+ ```
580
+
581
+ **JavaScript**
582
+ ```javascript
583
+ // flaky-reporter.js
584
+ class FlakyReporter {
585
+ constructor() {
586
+ this.flakyTests = [];
587
+ }
588
+
589
+ onTestEnd(test, result) {
590
+ if (result.retry > 0 && result.status === 'passed') {
591
+ this.flakyTests.push({
592
+ name: test.title,
593
+ file: test.location.file,
594
+ retries: result.retry,
595
+ });
596
+ }
597
+ }
598
+
599
+ onEnd() {
600
+ if (this.flakyTests.length > 0) {
601
+ console.log('\n--- FLAKY TESTS ---');
602
+ for (const t of this.flakyTests) {
603
+ console.log(` ${t.file} > "${t.name}" (needed ${t.retries} retries)`);
604
+ }
605
+ console.log(`Total flaky: ${this.flakyTests.length}`);
606
+ }
607
+ }
608
+ }
609
+
610
+ module.exports = FlakyReporter;
611
+ ```
612
+
613
+ ### Quarantine Strategy
614
+
615
+ **Use when**: A test is known-flaky and you cannot fix it immediately. Quarantine it so it does not block CI, but track it so it does not rot.
616
+
617
+ **TypeScript**
618
+ ```typescript
619
+ import { test, expect } from '@playwright/test';
620
+
621
+ // ---- Option 1: test.fixme() — skips the test with a reason ----
622
+ test.fixme('checkout with promo code applies discount', async ({ page }) => {
623
+ // TODO(JIRA-1234): Flaky due to race condition in promo service
624
+ // Fails ~10% of runs. Root cause: /api/promo responds after rendering
625
+ await page.goto('/checkout');
626
+ await page.getByLabel('Promo code').fill('SAVE20');
627
+ await page.getByRole('button', { name: 'Apply' }).click();
628
+ await expect(page.getByTestId('discount')).toHaveText('-$20.00');
629
+ });
630
+
631
+ // ---- Option 2: test.fail() — inverts: test passes only if it fails ----
632
+ // Use this when you KNOW the test fails and want CI to alert you when it starts passing
633
+ test.fail('known broken: export to PDF', async ({ page }) => {
634
+ // When this test starts passing, the .fail() annotation will make it fail,
635
+ // reminding you to remove the annotation
636
+ await page.goto('/reports');
637
+ await page.getByRole('button', { name: 'Export PDF' }).click();
638
+ await expect(page.getByText('PDF ready')).toBeVisible({ timeout: 10_000 });
639
+ });
640
+
641
+ // ---- Option 3: Skip by tag — quarantine with a grep filter ----
642
+ test('@flaky checkout race condition', async ({ page }) => {
643
+ // In CI, exclude flaky-tagged tests: npx playwright test --grep-invert @flaky
644
+ // Run ONLY flaky tests nightly: npx playwright test --grep @flaky --retries=5
645
+ await page.goto('/checkout');
646
+ await page.getByRole('button', { name: 'Place Order' }).click();
647
+ await expect(page.getByText('Order confirmed')).toBeVisible();
648
+ });
649
+ ```
650
+
651
+ **JavaScript**
652
+ ```javascript
653
+ const { test, expect } = require('@playwright/test');
654
+
655
+ // Option 1: test.fixme() — skips the test with a reason
656
+ test.fixme('checkout with promo code applies discount', async ({ page }) => {
657
+ // TODO(JIRA-1234): Flaky due to race condition in promo service
658
+ await page.goto('/checkout');
659
+ await page.getByLabel('Promo code').fill('SAVE20');
660
+ await page.getByRole('button', { name: 'Apply' }).click();
661
+ await expect(page.getByTestId('discount')).toHaveText('-$20.00');
662
+ });
663
+
664
+ // Option 2: test.fail() — inverts: test passes only if it fails
665
+ test.fail('known broken: export to PDF', async ({ page }) => {
666
+ await page.goto('/reports');
667
+ await page.getByRole('button', { name: 'Export PDF' }).click();
668
+ await expect(page.getByText('PDF ready')).toBeVisible({ timeout: 10_000 });
669
+ });
670
+
671
+ // Option 3: Skip by tag
672
+ test('@flaky checkout race condition', async ({ page }) => {
673
+ await page.goto('/checkout');
674
+ await page.getByRole('button', { name: 'Place Order' }).click();
675
+ await expect(page.getByText('Order confirmed')).toBeVisible();
676
+ });
677
+ ```
678
+
679
+ **CI configuration for quarantine:**
680
+
681
+ ```yaml
682
+ # .github/workflows/tests.yml
683
+ jobs:
684
+ e2e-tests:
685
+ steps:
686
+ - name: Run stable tests
687
+ run: npx playwright test --grep-invert @flaky
688
+
689
+ flaky-monitoring:
690
+ # Runs nightly, not on every PR
691
+ schedule:
692
+ - cron: '0 3 * * *'
693
+ steps:
694
+ - name: Run flaky tests with retries
695
+ run: npx playwright test --grep @flaky --retries=5 --reporter=json
696
+ - name: Report flaky test results
697
+ run: node scripts/report-flaky-metrics.js
698
+ ```
699
+
700
+ ### Prevention Checklist
701
+
702
+ Apply these rules from the start to prevent flakiness from entering your test suite.
703
+
704
+ **TypeScript**
705
+ ```typescript
706
+ // playwright.config.ts — flake-resistant configuration
707
+ import { defineConfig, devices } from '@playwright/test';
708
+
709
+ export default defineConfig({
710
+ // RULE 1: Run tests fully parallel to expose isolation issues early
711
+ fullyParallel: true,
712
+
713
+ // RULE 2: Fail CI if test.only() is left in code
714
+ forbidOnly: !!process.env.CI,
715
+
716
+ // RULE 3: Use retries in CI to surface (not hide) flaky tests
717
+ retries: process.env.CI ? 2 : 0,
718
+
719
+ // RULE 4: Reasonable timeouts — not too high, not too low
720
+ timeout: 30_000,
721
+ expect: { timeout: 5_000 },
722
+
723
+ use: {
724
+ // RULE 5: Always capture traces on retry for debugging
725
+ trace: 'on-first-retry',
726
+
727
+ // RULE 6: Use baseURL — never hardcode full URLs in tests
728
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
729
+
730
+ // RULE 7: Disable animations for deterministic behavior
731
+ contextOptions: {
732
+ reducedMotion: 'reduce',
733
+ },
734
+
735
+ // RULE 8: Explicit viewport — same locally and in CI
736
+ viewport: { width: 1280, height: 720 },
737
+ },
738
+
739
+ // RULE 9: Start the app automatically in CI
740
+ webServer: {
741
+ command: 'npm run start',
742
+ url: 'http://localhost:3000',
743
+ reuseExistingServer: !process.env.CI,
744
+ },
745
+ });
746
+ ```
747
+
748
+ ```typescript
749
+ // tests/example-stable-test.spec.ts — applying all rules in a test
750
+ import { test, expect } from '@playwright/test';
751
+
752
+ test.describe('user profile', () => {
753
+ test('updates display name', async ({ page }) => {
754
+ // RULE 10: Unique data per test
755
+ const newName = `User-${Date.now()}`;
756
+
757
+ // RULE 11: Use baseURL — relative paths only
758
+ await page.goto('/profile');
759
+
760
+ // RULE 12: Role-based locators — resilient to implementation changes
761
+ await page.getByRole('textbox', { name: 'Display name' }).fill(newName);
762
+ await page.getByRole('button', { name: 'Save' }).click();
763
+
764
+ // RULE 13: Auto-retrying assertions — never manual waits
765
+ await expect(page.getByRole('alert')).toHaveText('Profile updated');
766
+
767
+ // RULE 14: Assert on the result, not intermediate states
768
+ await expect(page.getByRole('textbox', { name: 'Display name' })).toHaveValue(newName);
769
+ });
770
+ });
771
+ ```
772
+
773
+ **JavaScript**
774
+ ```javascript
775
+ // playwright.config.js
776
+ const { defineConfig, devices } = require('@playwright/test');
777
+
778
+ module.exports = defineConfig({
779
+ fullyParallel: true,
780
+ forbidOnly: !!process.env.CI,
781
+ retries: process.env.CI ? 2 : 0,
782
+ timeout: 30_000,
783
+ expect: { timeout: 5_000 },
784
+ use: {
785
+ trace: 'on-first-retry',
786
+ baseURL: process.env.BASE_URL || 'http://localhost:3000',
787
+ contextOptions: {
788
+ reducedMotion: 'reduce',
789
+ },
790
+ viewport: { width: 1280, height: 720 },
791
+ },
792
+ webServer: {
793
+ command: 'npm run start',
794
+ url: 'http://localhost:3000',
795
+ reuseExistingServer: !process.env.CI,
796
+ },
797
+ });
798
+ ```
799
+
800
+ ```javascript
801
+ // tests/example-stable-test.spec.js
802
+ const { test, expect } = require('@playwright/test');
803
+
804
+ test.describe('user profile', () => {
805
+ test('updates display name', async ({ page }) => {
806
+ const newName = `User-${Date.now()}`;
807
+
808
+ await page.goto('/profile');
809
+ await page.getByRole('textbox', { name: 'Display name' }).fill(newName);
810
+ await page.getByRole('button', { name: 'Save' }).click();
811
+
812
+ await expect(page.getByRole('alert')).toHaveText('Profile updated');
813
+ await expect(page.getByRole('textbox', { name: 'Display name' })).toHaveValue(newName);
814
+ });
815
+ });
816
+ ```
817
+
818
+ ## Decision Guide
819
+
820
+ ```
821
+ My test is flaky. What do I do?
822
+ |
823
+ +-- Step 1: Reproduce locally
824
+ | |
825
+ | +-- npx playwright test <file> --repeat-each=20
826
+ | +-- Fails? --> TIMING issue. Fix with auto-retrying assertions.
827
+ | +-- Does not fail? --> Continue to Step 2.
828
+ |
829
+ +-- Step 2: Isolate from other tests
830
+ | |
831
+ | +-- npx playwright test --grep "exact test name" --workers=1
832
+ | +-- Passes alone? --> ISOLATION issue. Fix with unique data + fixtures.
833
+ | +-- Fails alone? --> Continue to Step 3.
834
+ |
835
+ +-- Step 3: Compare environments
836
+ | |
837
+ | +-- Download CI trace, compare with local trace
838
+ | +-- Different? --> ENVIRONMENT issue. Fix with explicit viewport,
839
+ | | reducedMotion, webServer config, stub externals.
840
+ | +-- Same? --> Continue to Step 4.
841
+ |
842
+ +-- Step 4: Check infrastructure
843
+ | |
844
+ | +-- Error mentions browser crash, OOM, DNS, ECONNREFUSED?
845
+ | +-- YES --> INFRASTRUCTURE issue. Fix with Docker, retry config,
846
+ | | health checks before test run.
847
+ | +-- NO --> Re-examine. Enable trace: 'on' and retries to collect
848
+ | more data. Compare passing and failing traces side by side.
849
+ ```
850
+
851
+ ## Anti-Patterns
852
+
853
+ | Don't Do This | Problem | Do This Instead |
854
+ |---|---|---|
855
+ | Increase timeout to 120s to "fix" flakiness | Masks the real issue. Tests become unbearably slow when they fail. Slows the entire CI pipeline. | Diagnose the root cause. Fix the race condition, not the timeout. |
856
+ | Use `page.waitForTimeout(N)` | Arbitrary delays are too slow on fast machines and too fast on slow ones. The #1 cause of flakiness. | Use `expect(locator).toBeVisible()`, `page.waitForResponse()`, or `expect.poll()`. |
857
+ | Ignore flaky tests ("it works if you run it again") | Flaky tests erode trust in the entire suite. People stop reading failures. Real bugs slip through. | Diagnose immediately. If you cannot fix now, quarantine with `test.fixme()` and a tracking ticket. |
858
+ | Add `--retries=3` and call it fixed | Retries do not fix flakiness, they hide it. A test that needs retries is a test with a bug. | Use retries to **detect** flakiness (check the retry count in reports), not to paper over it. |
859
+ | Use `test.describe.serial()` to fix ordering-dependent tests | Serial mode forces all tests in the block to run sequentially. It hides isolation bugs and slows the suite. | Fix the isolation issue. Each test should pass regardless of execution order. |
860
+ | Mock everything to prevent environment differences | Over-mocking removes confidence that the real system works. Tests pass but the app is broken. | Mock only external/third-party services. Test your own API for real. |
861
+ | Run `--repeat-each=100` in CI on every commit | Multiplies CI time by 100x. Wastes resources. | Run burn-in locally or in a nightly job, not on every PR. |
862
+
863
+ ## Troubleshooting
864
+
865
+ | Symptom | Category | Fix |
866
+ |---|---|---|
867
+ | Test fails with "Timeout 5000ms" intermittently | Timing | Increase `expect.timeout` to 10s, or add `page.waitForResponse()` before the assertion |
868
+ | Test passes alone, fails in full suite run | Isolation | Check for module-level `let` variables, shared database rows, or localStorage leaks |
869
+ | Test passes locally, fails in CI | Environment | Compare traces. Check viewport, fonts, `reducedMotion`, and external service availability |
870
+ | Test fails with "Target closed" or "Browser closed" | Infrastructure | Check CI memory limits. Add `--workers=50%` to reduce parallel load. Add health check in `beforeAll` |
871
+ | Test fails differently every time | Timing + Isolation | Enable `trace: 'on'` and compare multiple failing traces. The inconsistency itself is a clue |
872
+ | Flaky test passes 99/100 times | Timing (rare race) | Use `--repeat-each=200` locally. Add `page.waitForResponse()` or `expect.poll()` for the specific race |
873
+ | Visual comparison test is flaky | Environment | Use `maxDiffPixelRatio` threshold. Set explicit fonts with `@font-face`. Use Docker for consistent rendering |
874
+ | Tests flake only on WebKit | Environment | WebKit has different timing behavior. Add WebKit-specific assertions or increase timeouts per project |
875
+
876
+ ## Related
877
+
878
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- auto-retrying assertions and explicit waits
879
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- fixture teardown for test isolation
880
+ - [core/test-data-management.md](test-data-management.md) -- unique data per test, factory functions
881
+ - [core/configuration.md](configuration.md) -- retry, timeout, and trace configuration
882
+ - [core/debugging.md](debugging.md) -- trace viewer, UI mode, and Inspector for diagnosing failures
883
+ - [core/common-pitfalls.md](common-pitfalls.md) -- common mistakes that cause flakiness