@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,703 @@
1
+ # Assertions and Waiting
2
+
3
+ > **When to use**: Every time you write an `expect()` call, wait for a condition, or wonder why a test is flaky due to timing.
4
+ > **Prerequisites**: [core/locators.md](locators.md) for locator strategies used in examples.
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Web-first (auto-retry) — ALWAYS prefer these
10
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
11
+ await expect(page.getByRole('heading')).toHaveText('Dashboard');
12
+ await expect(page.getByRole('listitem')).toHaveCount(5);
13
+
14
+ // Negative — auto-retries until condition is met
15
+ await expect(page.getByRole('dialog')).not.toBeVisible();
16
+
17
+ // Soft — collect failures, don't stop test
18
+ await expect.soft(page.getByRole('heading')).toHaveText('Title');
19
+
20
+ // Polling — non-DOM async conditions
21
+ await expect.poll(() => getUserCount()).toBe(10);
22
+
23
+ // Retry a block — multiple assertions that must pass together
24
+ await expect(async () => { /* assertions */ }).toPass();
25
+ ```
26
+
27
+ ## Patterns
28
+
29
+ ### Web-First Assertions (Auto-Retry)
30
+
31
+ **Use when**: Asserting anything about a locator — visibility, text, attributes, CSS, count, values.
32
+ **Avoid when**: Asserting on an already-resolved JavaScript value (use non-retrying assertions instead).
33
+
34
+ Web-first assertions automatically retry until the condition is met or the timeout expires. They are the backbone of reliable Playwright tests.
35
+
36
+ **TypeScript**
37
+ ```typescript
38
+ import { test, expect } from '@playwright/test';
39
+
40
+ test('web-first assertions demo', async ({ page }) => {
41
+ await page.goto('/products');
42
+
43
+ // Visibility
44
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
45
+
46
+ // Text — exact match
47
+ await expect(page.getByTestId('total')).toHaveText('Total: $99.00');
48
+
49
+ // Text — partial match (substring or regex)
50
+ await expect(page.getByTestId('total')).toContainText('$99');
51
+ await expect(page.getByTestId('total')).toHaveText(/Total: \$\d+\.\d{2}/);
52
+
53
+ // Element count
54
+ await expect(page.getByRole('listitem')).toHaveCount(5);
55
+
56
+ // Attribute
57
+ await expect(page.getByRole('link', { name: 'Docs' })).toHaveAttribute('href', '/docs');
58
+
59
+ // CSS property
60
+ await expect(page.getByTestId('alert')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
61
+
62
+ // Input value
63
+ await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
64
+
65
+ // Class (use toHaveClass for full match, regex for partial)
66
+ await expect(page.getByTestId('card')).toHaveClass(/active/);
67
+
68
+ // Enabled / disabled / checked
69
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
70
+ await expect(page.getByRole('checkbox')).toBeChecked();
71
+
72
+ // Editable / focused / attached
73
+ await expect(page.getByLabel('Name')).toBeEditable();
74
+ await expect(page.getByLabel('Name')).toBeFocused();
75
+ });
76
+ ```
77
+
78
+ **JavaScript**
79
+ ```javascript
80
+ const { test, expect } = require('@playwright/test');
81
+
82
+ test('web-first assertions demo', async ({ page }) => {
83
+ await page.goto('/products');
84
+
85
+ await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
86
+ await expect(page.getByTestId('total')).toHaveText('Total: $99.00');
87
+ await expect(page.getByTestId('total')).toContainText('$99');
88
+ await expect(page.getByTestId('total')).toHaveText(/Total: \$\d+\.\d{2}/);
89
+ await expect(page.getByRole('listitem')).toHaveCount(5);
90
+ await expect(page.getByRole('link', { name: 'Docs' })).toHaveAttribute('href', '/docs');
91
+ await expect(page.getByTestId('alert')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
92
+ await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
93
+ await expect(page.getByTestId('card')).toHaveClass(/active/);
94
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
95
+ await expect(page.getByRole('checkbox')).toBeChecked();
96
+ await expect(page.getByLabel('Name')).toBeEditable();
97
+ await expect(page.getByLabel('Name')).toBeFocused();
98
+ });
99
+ ```
100
+
101
+ #### Asserting Pseudo-Element Styles (`toHaveCSS({ pseudo })`, Playwright 1.60+)
102
+
103
+ **Use when**: The style you care about lives on a `::before` or `::after` pseudo-element — icon glyphs (`content`), decorative bars, required-field asterisks, tooltips.
104
+ **Avoid when**: The style is on the element itself; pass no `pseudo` option.
105
+
106
+ Playwright 1.60 adds a `pseudo` option to `toHaveCSS()`, so you can assert computed styles of pseudo-elements directly instead of reaching into `getComputedStyle` via `evaluate`.
107
+
108
+ **TypeScript**
109
+ ```typescript
110
+ import { test, expect } from '@playwright/test';
111
+
112
+ test('required field shows a red asterisk via ::after', async ({ page }) => {
113
+ await page.goto('/register');
114
+
115
+ const label = page.getByText('Email', { exact: true });
116
+
117
+ // Verify the ::after content and color of the "required" marker
118
+ await expect(label).toHaveCSS('content', '"*"', { pseudo: '::after' });
119
+ await expect(label).toHaveCSS('color', 'rgb(220, 38, 38)', { pseudo: '::after' });
120
+ });
121
+ ```
122
+
123
+ **JavaScript**
124
+ ```javascript
125
+ const { test, expect } = require('@playwright/test');
126
+
127
+ test('required field shows a red asterisk via ::after', async ({ page }) => {
128
+ await page.goto('/register');
129
+
130
+ const label = page.getByText('Email', { exact: true });
131
+ await expect(label).toHaveCSS('content', '"*"', { pseudo: '::after' });
132
+ });
133
+ ```
134
+
135
+ ### Non-Retrying Assertions
136
+
137
+ **Use when**: The value is already resolved — a JavaScript variable, an API response body, a page title from `page.title()`, or a URL from `page.url()`.
138
+ **Avoid when**: Asserting on anything that might change asynchronously in the DOM. Use web-first assertions instead.
139
+
140
+ Non-retrying assertions run once. If they fail, they fail immediately.
141
+
142
+ **TypeScript**
143
+ ```typescript
144
+ import { test, expect } from '@playwright/test';
145
+
146
+ test('non-retrying assertions for resolved values', async ({ page }) => {
147
+ await page.goto('/api/health');
148
+
149
+ // Already-resolved values — no retry needed
150
+ const title = await page.title();
151
+ expect(title).toBe('Health Check');
152
+
153
+ const url = page.url();
154
+ expect(url).toContain('/api/health');
155
+
156
+ // API response body
157
+ const response = await page.request.get('/api/users');
158
+ const body = await response.json();
159
+ expect(body.users).toHaveLength(3);
160
+ expect(response.status()).toBe(200);
161
+
162
+ // Snapshot (value comparison, not retrying)
163
+ expect(body).toMatchObject({ status: 'healthy', users: expect.any(Array) });
164
+ });
165
+ ```
166
+
167
+ **JavaScript**
168
+ ```javascript
169
+ const { test, expect } = require('@playwright/test');
170
+
171
+ test('non-retrying assertions for resolved values', async ({ page }) => {
172
+ await page.goto('/api/health');
173
+
174
+ const title = await page.title();
175
+ expect(title).toBe('Health Check');
176
+
177
+ const url = page.url();
178
+ expect(url).toContain('/api/health');
179
+
180
+ const response = await page.request.get('/api/users');
181
+ const body = await response.json();
182
+ expect(body.users).toHaveLength(3);
183
+ expect(response.status()).toBe(200);
184
+
185
+ expect(body).toMatchObject({ status: 'healthy', users: expect.any(Array) });
186
+ });
187
+ ```
188
+
189
+ ### Negative Assertions
190
+
191
+ **Use when**: Verifying something has disappeared, been removed, or is not present.
192
+ **Avoid when**: Never.
193
+
194
+ Negative web-first assertions auto-retry until the condition is met. This is critical: `expect(locator).not.toBeVisible()` correctly waits for the element to disappear. It does not just check once.
195
+
196
+ **TypeScript**
197
+ ```typescript
198
+ import { test, expect } from '@playwright/test';
199
+
200
+ test('verify element disappears after action', async ({ page }) => {
201
+ await page.goto('/notifications');
202
+
203
+ // Dismiss a notification
204
+ await page.getByRole('button', { name: 'Dismiss' }).click();
205
+
206
+ // Auto-retries until the notification is gone — correct
207
+ await expect(page.getByRole('alert')).not.toBeVisible();
208
+
209
+ // Verify text is not present
210
+ await expect(page.getByText('Error occurred')).not.toBeVisible();
211
+
212
+ // Verify element is detached from DOM entirely
213
+ await expect(page.getByTestId('modal')).not.toBeAttached();
214
+
215
+ // Verify count dropped to zero
216
+ await expect(page.getByRole('alert')).toHaveCount(0);
217
+ });
218
+ ```
219
+
220
+ **JavaScript**
221
+ ```javascript
222
+ const { test, expect } = require('@playwright/test');
223
+
224
+ test('verify element disappears after action', async ({ page }) => {
225
+ await page.goto('/notifications');
226
+
227
+ await page.getByRole('button', { name: 'Dismiss' }).click();
228
+
229
+ await expect(page.getByRole('alert')).not.toBeVisible();
230
+ await expect(page.getByText('Error occurred')).not.toBeVisible();
231
+ await expect(page.getByTestId('modal')).not.toBeAttached();
232
+ await expect(page.getByRole('alert')).toHaveCount(0);
233
+ });
234
+ ```
235
+
236
+ **Gotcha**: `not.toBeVisible()` passes for elements that exist but are hidden AND for elements not in the DOM. If you specifically need to assert the element is removed from the DOM entirely (not just hidden), use `not.toBeAttached()`.
237
+
238
+ ### Soft Assertions
239
+
240
+ **Use when**: You want to collect multiple failures in a single test without stopping at the first one. Common for form validation checks, dashboard content audits, or visual checklists.
241
+ **Avoid when**: Subsequent assertions depend on the result of earlier ones (if the first fails, later assertions may be meaningless).
242
+
243
+ **TypeScript**
244
+ ```typescript
245
+ import { test, expect } from '@playwright/test';
246
+
247
+ test('dashboard shows all expected widgets', async ({ page }) => {
248
+ await page.goto('/dashboard');
249
+
250
+ // All checks run even if earlier ones fail
251
+ await expect.soft(page.getByTestId('revenue-widget')).toBeVisible();
252
+ await expect.soft(page.getByTestId('users-widget')).toBeVisible();
253
+ await expect.soft(page.getByTestId('orders-widget')).toBeVisible();
254
+ await expect.soft(page.getByTestId('revenue-widget')).toContainText('$');
255
+ await expect.soft(page.getByTestId('users-widget')).toContainText('active');
256
+
257
+ // Test still fails if any soft assertion failed, but you see ALL failures in the report
258
+ });
259
+ ```
260
+
261
+ **JavaScript**
262
+ ```javascript
263
+ const { test, expect } = require('@playwright/test');
264
+
265
+ test('dashboard shows all expected widgets', async ({ page }) => {
266
+ await page.goto('/dashboard');
267
+
268
+ await expect.soft(page.getByTestId('revenue-widget')).toBeVisible();
269
+ await expect.soft(page.getByTestId('users-widget')).toBeVisible();
270
+ await expect.soft(page.getByTestId('orders-widget')).toBeVisible();
271
+ await expect.soft(page.getByTestId('revenue-widget')).toContainText('$');
272
+ await expect.soft(page.getByTestId('users-widget')).toContainText('active');
273
+ });
274
+ ```
275
+
276
+ **Tip**: Guard subsequent actions against soft-assertion failures when those actions would throw confusing errors.
277
+
278
+ ```typescript
279
+ await expect.soft(page.getByRole('button', { name: 'Next' })).toBeVisible();
280
+ if (test.info().errors.length > 0) return; // bail out — no point continuing
281
+ await page.getByRole('button', { name: 'Next' }).click();
282
+ ```
283
+
284
+ **Soft polling (Playwright 1.61+)**: `expect.soft.poll()` combines both — poll an async condition, record a failure without stopping the test:
285
+
286
+ ```typescript
287
+ // Audit several async conditions; see every failure in one run
288
+ await expect.soft.poll(() => getQueueDepth()).toBe(0);
289
+ await expect.soft.poll(() => getFailedJobCount()).toBe(0);
290
+ ```
291
+
292
+ ### Polling Assertions
293
+
294
+ **Use when**: Waiting for a non-DOM, non-locator async condition: API readiness, database state, file existence, polling a service.
295
+ **Avoid when**: The condition is about a DOM element. Use web-first assertions on locators.
296
+
297
+ `expect.poll()` repeatedly calls a function until the assertion passes.
298
+
299
+ **TypeScript**
300
+ ```typescript
301
+ import { test, expect } from '@playwright/test';
302
+
303
+ test('wait for background job to complete', async ({ page }) => {
304
+ await page.goto('/jobs');
305
+ await page.getByRole('button', { name: 'Start Export' }).click();
306
+
307
+ // Poll an API endpoint until the job finishes
308
+ await expect.poll(async () => {
309
+ const response = await page.request.get('/api/jobs/latest');
310
+ const job = await response.json();
311
+ return job.status;
312
+ }, {
313
+ message: 'Expected export job to complete',
314
+ timeout: 30_000,
315
+ intervals: [1_000, 2_000, 5_000], // backoff: 1s, 2s, then every 5s
316
+ }).toBe('completed');
317
+ });
318
+ ```
319
+
320
+ **JavaScript**
321
+ ```javascript
322
+ const { test, expect } = require('@playwright/test');
323
+
324
+ test('wait for background job to complete', async ({ page }) => {
325
+ await page.goto('/jobs');
326
+ await page.getByRole('button', { name: 'Start Export' }).click();
327
+
328
+ await expect.poll(async () => {
329
+ const response = await page.request.get('/api/jobs/latest');
330
+ const job = await response.json();
331
+ return job.status;
332
+ }, {
333
+ message: 'Expected export job to complete',
334
+ timeout: 30_000,
335
+ intervals: [1_000, 2_000, 5_000],
336
+ }).toBe('completed');
337
+ });
338
+ ```
339
+
340
+ ### Retrying Assertion Blocks with `toPass()`
341
+
342
+ **Use when**: Multiple assertions or actions must pass together as a group, and the whole block should be retried if any part fails. Common for race conditions where data appears incrementally.
343
+ **Avoid when**: A single web-first assertion suffices.
344
+
345
+ **TypeScript**
346
+ ```typescript
347
+ import { test, expect } from '@playwright/test';
348
+
349
+ test('search results update correctly', async ({ page }) => {
350
+ await page.goto('/search');
351
+
352
+ await expect(async () => {
353
+ await page.getByLabel('Search').fill('playwright');
354
+ await page.getByRole('button', { name: 'Search' }).click();
355
+
356
+ // Both must pass together — retries the whole block
357
+ await expect(page.getByRole('listitem')).toHaveCount(10);
358
+ await expect(page.getByRole('listitem').first()).toContainText('Playwright');
359
+ }).toPass({
360
+ timeout: 15_000,
361
+ intervals: [1_000, 2_000, 5_000],
362
+ });
363
+ });
364
+ ```
365
+
366
+ **JavaScript**
367
+ ```javascript
368
+ const { test, expect } = require('@playwright/test');
369
+
370
+ test('search results update correctly', async ({ page }) => {
371
+ await page.goto('/search');
372
+
373
+ await expect(async () => {
374
+ await page.getByLabel('Search').fill('playwright');
375
+ await page.getByRole('button', { name: 'Search' }).click();
376
+
377
+ await expect(page.getByRole('listitem')).toHaveCount(10);
378
+ await expect(page.getByRole('listitem').first()).toContainText('Playwright');
379
+ }).toPass({
380
+ timeout: 15_000,
381
+ intervals: [1_000, 2_000, 5_000],
382
+ });
383
+ });
384
+ ```
385
+
386
+ ### Custom Matchers
387
+
388
+ **Use when**: Domain-specific assertions you repeat across many tests — valid price format, date range, accessible form, etc.
389
+ **Avoid when**: The assertion is only used in one test. Inline it.
390
+
391
+ **TypeScript**
392
+ ```typescript
393
+ // fixtures/custom-matchers.ts
394
+ import { expect, type Locator } from '@playwright/test';
395
+
396
+ expect.extend({
397
+ async toHaveValidPrice(locator: Locator) {
398
+ const assertionName = 'toHaveValidPrice';
399
+ let pass: boolean;
400
+ let matcherResult: any;
401
+
402
+ try {
403
+ await expect(locator).toHaveText(/^\$\d{1,3}(,\d{3})*\.\d{2}$/);
404
+ pass = true;
405
+ } catch (e: any) {
406
+ matcherResult = e.matcherResult;
407
+ pass = false;
408
+ }
409
+
410
+ const message = pass
411
+ ? () => `${this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot })}\n\nLocator: ${locator}\nExpected: not a valid price format\nReceived: ${matcherResult?.actual || 'valid price'}`
412
+ : () => `${this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot })}\n\nLocator: ${locator}\nExpected: valid price format ($X,XXX.XX)\nReceived: ${matcherResult?.actual || 'no text'}`;
413
+
414
+ return { message, pass, name: assertionName, expected: 'valid price format', actual: matcherResult?.actual };
415
+ },
416
+ });
417
+
418
+ // Declare types for TypeScript
419
+ export {};
420
+ declare global {
421
+ namespace PlaywrightTest {
422
+ interface Matchers<R, T> {
423
+ toHaveValidPrice(): R;
424
+ }
425
+ }
426
+ }
427
+ ```
428
+
429
+ ```typescript
430
+ // tests/products.spec.ts
431
+ import { test, expect } from '@playwright/test';
432
+ import '../fixtures/custom-matchers';
433
+
434
+ test('product prices are valid', async ({ page }) => {
435
+ await page.goto('/products');
436
+ await expect(page.getByTestId('price-tag').first()).toHaveValidPrice();
437
+ });
438
+ ```
439
+
440
+ **JavaScript**
441
+ ```javascript
442
+ // fixtures/custom-matchers.js
443
+ const { expect } = require('@playwright/test');
444
+
445
+ expect.extend({
446
+ async toHaveValidPrice(locator) {
447
+ const assertionName = 'toHaveValidPrice';
448
+ let pass;
449
+ let matcherResult;
450
+
451
+ try {
452
+ await expect(locator).toHaveText(/^\$\d{1,3}(,\d{3})*\.\d{2}$/);
453
+ pass = true;
454
+ } catch (e) {
455
+ matcherResult = e.matcherResult;
456
+ pass = false;
457
+ }
458
+
459
+ const message = pass
460
+ ? () => `Expected locator not to have valid price format`
461
+ : () => `Expected locator to have valid price format ($X,XXX.XX), received: ${matcherResult?.actual || 'no text'}`;
462
+
463
+ return { message, pass, name: assertionName };
464
+ },
465
+ });
466
+ ```
467
+
468
+ ```javascript
469
+ // tests/products.spec.js
470
+ const { test, expect } = require('@playwright/test');
471
+ require('../fixtures/custom-matchers');
472
+
473
+ test('product prices are valid', async ({ page }) => {
474
+ await page.goto('/products');
475
+ await expect(page.getByTestId('price-tag').first()).toHaveValidPrice();
476
+ });
477
+ ```
478
+
479
+ ### Auto-Waiting (Actionability)
480
+
481
+ **Use when**: You don't need to "use" this — understand it. Every Playwright action (`click`, `fill`, `check`, `selectOption`, etc.) auto-waits for the target element to be actionable before proceeding.
482
+
483
+ Playwright checks before acting:
484
+ | Action | Waits for |
485
+ |---|---|
486
+ | `click()` | Attached, visible, stable (no animation), enabled, not obscured by another element |
487
+ | `fill()` | Attached, visible, enabled, editable |
488
+ | `check()` | Attached, visible, stable, enabled |
489
+ | `selectOption()` | Attached, visible, enabled |
490
+ | `hover()` | Attached, visible, stable |
491
+ | `type()` | Attached, visible, enabled, editable |
492
+
493
+ This means you almost never need explicit waits before actions. Do NOT write `await expect(button).toBeVisible()` before `await button.click()` — the click already waits for visibility.
494
+
495
+ ### Explicit Waits
496
+
497
+ **Use when**: Waiting for navigation, network responses, or page load states that are not tied to a specific locator.
498
+ **Avoid when**: A web-first assertion on a locator would suffice.
499
+
500
+ **TypeScript**
501
+ ```typescript
502
+ import { test, expect } from '@playwright/test';
503
+
504
+ test('explicit waits for non-locator conditions', async ({ page }) => {
505
+ await page.goto('/login');
506
+
507
+ // Wait for navigation after form submit
508
+ await page.getByLabel('Email').fill('user@test.com');
509
+ await page.getByLabel('Password').fill('password123');
510
+ await page.getByRole('button', { name: 'Sign In' }).click();
511
+ await page.waitForURL('/dashboard');
512
+ // can also use glob: await page.waitForURL('**/dashboard');
513
+ // or regex: await page.waitForURL(/.*dashboard/);
514
+
515
+ // Wait for a specific API response
516
+ const responsePromise = page.waitForResponse(
517
+ (resp) => resp.url().includes('/api/user') && resp.status() === 200
518
+ );
519
+ await page.getByRole('button', { name: 'Refresh' }).click();
520
+ const response = await responsePromise;
521
+ const data = await response.json();
522
+ expect(data.name).toBe('Test User');
523
+
524
+ // Wait for a network request to be sent
525
+ const requestPromise = page.waitForRequest('**/api/analytics');
526
+ await page.getByRole('button', { name: 'Track' }).click();
527
+ const request = await requestPromise;
528
+ expect(request.method()).toBe('POST');
529
+
530
+ // Wait for load state
531
+ await page.waitForLoadState('networkidle'); // use sparingly — only for legacy apps
532
+ });
533
+ ```
534
+
535
+ **JavaScript**
536
+ ```javascript
537
+ const { test, expect } = require('@playwright/test');
538
+
539
+ test('explicit waits for non-locator conditions', async ({ page }) => {
540
+ await page.goto('/login');
541
+
542
+ await page.getByLabel('Email').fill('user@test.com');
543
+ await page.getByLabel('Password').fill('password123');
544
+ await page.getByRole('button', { name: 'Sign In' }).click();
545
+ await page.waitForURL('/dashboard');
546
+
547
+ const responsePromise = page.waitForResponse(
548
+ (resp) => resp.url().includes('/api/user') && resp.status() === 200
549
+ );
550
+ await page.getByRole('button', { name: 'Refresh' }).click();
551
+ const response = await responsePromise;
552
+ const data = await response.json();
553
+ expect(data.name).toBe('Test User');
554
+
555
+ const requestPromise = page.waitForRequest('**/api/analytics');
556
+ await page.getByRole('button', { name: 'Track' }).click();
557
+ const request = await requestPromise;
558
+ expect(request.method()).toBe('POST');
559
+
560
+ await page.waitForLoadState('networkidle');
561
+ });
562
+ ```
563
+
564
+ **Critical pattern**: Always set up `waitForResponse` / `waitForRequest` BEFORE the action that triggers it. Otherwise you have a race condition.
565
+
566
+ ```typescript
567
+ // CORRECT — promise registered before the click
568
+ const responsePromise = page.waitForResponse('**/api/data');
569
+ await page.getByRole('button', { name: 'Load' }).click();
570
+ const response = await responsePromise;
571
+
572
+ // WRONG — response may already have arrived before waitForResponse is registered
573
+ await page.getByRole('button', { name: 'Load' }).click();
574
+ const response = await page.waitForResponse('**/api/data'); // race condition!
575
+ ```
576
+
577
+ ### Assertion Timeouts
578
+
579
+ **Use when**: A specific assertion needs more or less time than the global default.
580
+
581
+ **TypeScript**
582
+ ```typescript
583
+ import { test, expect } from '@playwright/test';
584
+
585
+ // Per-assertion timeout
586
+ test('slow element appears eventually', async ({ page }) => {
587
+ await page.goto('/slow-dashboard');
588
+
589
+ await expect(page.getByTestId('heavy-chart')).toBeVisible({
590
+ timeout: 30_000, // override for this one assertion
591
+ });
592
+ });
593
+
594
+ // Per-test timeout
595
+ test('long-running flow', async ({ page }) => {
596
+ test.setTimeout(120_000);
597
+ await page.goto('/import');
598
+ await page.getByRole('button', { name: 'Import CSV' }).click();
599
+ await expect(page.getByText('Import complete')).toBeVisible({ timeout: 60_000 });
600
+ });
601
+ ```
602
+
603
+ **JavaScript**
604
+ ```javascript
605
+ const { test, expect } = require('@playwright/test');
606
+
607
+ test('slow element appears eventually', async ({ page }) => {
608
+ await page.goto('/slow-dashboard');
609
+
610
+ await expect(page.getByTestId('heavy-chart')).toBeVisible({
611
+ timeout: 30_000,
612
+ });
613
+ });
614
+
615
+ test('long-running flow', async ({ page }) => {
616
+ test.setTimeout(120_000);
617
+ await page.goto('/import');
618
+ await page.getByRole('button', { name: 'Import CSV' }).click();
619
+ await expect(page.getByText('Import complete')).toBeVisible({ timeout: 60_000 });
620
+ });
621
+ ```
622
+
623
+ **Global timeout** in `playwright.config.ts`:
624
+ ```typescript
625
+ export default defineConfig({
626
+ expect: {
627
+ timeout: 10_000, // default is 5_000 — increase for slow apps
628
+ },
629
+ timeout: 30_000, // per-test timeout (default 30_000)
630
+ });
631
+ ```
632
+
633
+ ## Decision Guide
634
+
635
+ | Scenario | Recommended Approach | Why |
636
+ |---|---|---|
637
+ | Element visible / hidden | `expect(locator).toBeVisible()` / `.not.toBeVisible()` | Auto-retries, handles timing |
638
+ | Text content check | `expect(locator).toHaveText()` or `.toContainText()` | Auto-retries; use `toContainText` for substring |
639
+ | Element count | `expect(locator).toHaveCount(n)` | Retries until count matches |
640
+ | Input value | `expect(locator).toHaveValue('x')` | Auto-retries on the locator |
641
+ | Element attribute | `expect(locator).toHaveAttribute('href', '/x')` | Auto-retries |
642
+ | CSS property | `expect(locator).toHaveCSS('color', 'rgb(0,0,0)')` | Auto-retries; use computed RGB values. Add `{ pseudo: '::after' }` (1.60+) for pseudo-element styles |
643
+ | Element gone from DOM | `expect(locator).not.toBeAttached()` | Distinguishes hidden vs. removed |
644
+ | URL changed | `page.waitForURL('/path')` or `expect(page).toHaveURL('/path')` | `toHaveURL` auto-retries; `waitForURL` blocks |
645
+ | Page title | `expect(page).toHaveTitle('Title')` | Auto-retries |
646
+ | API response status | `expect(response.status()).toBe(200)` | Already resolved — non-retrying |
647
+ | Background job / polling | `expect.poll(() => fetchStatus())` | Retries a function, not a locator |
648
+ | Multiple assertions as one | `expect(async () => { ... }).toPass()` | Retries the entire block |
649
+ | Multiple independent checks | `expect.soft(locator)` | Collects all failures |
650
+ | Resolved JS value | `expect(value).toBe(x)` | No retry needed |
651
+
652
+ ## Anti-Patterns
653
+
654
+ | Don't Do This | Problem | Do This Instead |
655
+ |---|---|---|
656
+ | `await page.waitForTimeout(2000)` | Arbitrary delay. Too slow when fast, too short when slow. Flaky. | Use a web-first assertion: `await expect(locator).toBeVisible()` |
657
+ | `const visible = await el.isVisible(); expect(visible).toBe(true)` | `isVisible()` resolves once — no retry. Race condition. | `await expect(el).toBeVisible()` |
658
+ | `try { await expect(el).toBeVisible() } catch { /* ignore */ }` | Swallows real failures. Masks bugs. | Use `expect.soft()` or restructure the test |
659
+ | `expect(locator).toBeVisible().then(...)` | Missing `await`. Assertion runs detached, test passes before it resolves. | Always `await expect(locator).toBeVisible()` |
660
+ | `await expect(btn).toBeVisible(); await btn.click()` | Redundant. `click()` auto-waits for visibility. | Just `await btn.click()` |
661
+ | `await page.waitForLoadState('networkidle')` before every action | `networkidle` is fragile (long-poll, analytics, websockets break it). Slows tests. | Wait for a specific element or URL instead |
662
+ | `expect(await el.textContent()).toBe('X')` | Resolves text once — no retry. | `await expect(el).toHaveText('X')` |
663
+ | `expect(await page.locator('.item').count()).toBe(5)` | Resolves count once — no retry. | `await expect(page.locator('.item')).toHaveCount(5)` |
664
+ | Using `toPass()` for a single assertion | Unnecessary complexity. | Use the web-first assertion directly |
665
+ | Huge timeout per assertion (>60s) | Hides real performance problems. Tests become unbearably slow on failure. | Fix the app or split the test. Use 10-30s max. |
666
+
667
+ ## Troubleshooting
668
+
669
+ ### "Timed out 5000ms waiting for expect(...).toBeVisible()"
670
+
671
+ **Cause**: The element never appeared within the assertion timeout. Common reasons:
672
+ 1. Wrong locator — element exists but locator doesn't match.
673
+ 2. Element is behind a loading spinner or inside a collapsed section.
674
+ 3. Network request that populates the element is slow.
675
+
676
+ **Fix**:
677
+ - Run with `--ui` or `--debug` to visually inspect the page state at failure time.
678
+ - Check the locator matches in the browser console: `playwright.$(selector)`.
679
+ - Increase timeout for genuinely slow operations: `{ timeout: 15_000 }`.
680
+ - Verify the locator targets the right element: `await expect(locator).toHaveCount(1)` first.
681
+
682
+ ### "expect.soft: Test finished with X failed assertions"
683
+
684
+ **Cause**: Soft assertions collected failures but you have no immediate visibility into which ones.
685
+
686
+ **Fix**: Check the HTML report (`npx playwright show-report`). Each soft failure is listed with its locator, expected value, and actual value. Group related soft assertions under `test.step()` for better readability.
687
+
688
+ ### "Expected ' Dashboard ' to have text 'Dashboard'"
689
+
690
+ **Cause**: `toHaveText()` performs full text match including normalization, but whitespace mismatch still trips people up when elements have unusual rendering.
691
+
692
+ **Fix**:
693
+ - Use `toContainText('Dashboard')` for a substring match that is more resilient to whitespace.
694
+ - Use regex: `toHaveText(/Dashboard/)`.
695
+ - Check for zero-width spaces or special Unicode characters with `--debug`.
696
+
697
+ ## Related
698
+
699
+ - [core/locators.md](locators.md) — locator strategies used in assertion targets
700
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) — custom fixtures for reusable assertion setup
701
+ - [core/debugging.md](debugging.md) — debugging assertion failures with UI mode and traces
702
+ - [core/flaky-tests.md](flaky-tests.md) — fixing timing-related flakiness
703
+ - [core/error-index.md](error-index.md) — specific error messages and fixes