@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,673 @@
1
+ # Browser APIs
2
+
3
+ > **When to use**: When testing features that depend on browser-native APIs -- geolocation, permissions, clipboard, notifications, camera/microphone, localStorage, sessionStorage, IndexedDB.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Geolocation — set via context options
10
+ const context = await browser.newContext({
11
+ geolocation: { latitude: 40.7128, longitude: -74.0060 },
12
+ permissions: ['geolocation'],
13
+ });
14
+
15
+ // Permissions — grant at context level
16
+ const context = await browser.newContext({
17
+ permissions: ['clipboard-read', 'clipboard-write', 'notifications'],
18
+ });
19
+
20
+ // localStorage / sessionStorage — access via page.evaluate
21
+ await page.evaluate(() => localStorage.setItem('theme', 'dark'));
22
+ const value = await page.evaluate(() => localStorage.getItem('theme'));
23
+
24
+ // Clipboard — read/write in page context
25
+ await page.evaluate(() => navigator.clipboard.writeText('copied text'));
26
+ ```
27
+
28
+ ## Patterns
29
+
30
+ ### Geolocation
31
+
32
+ **Use when**: Your app uses `navigator.geolocation` for maps, store locators, delivery tracking, or location-based features.
33
+ **Avoid when**: Your app never reads the user's location.
34
+
35
+ Geolocation is set at the context level. You can also update it mid-test with `context.setGeolocation()`.
36
+
37
+ **TypeScript**
38
+ ```typescript
39
+ import { test, expect } from '@playwright/test';
40
+
41
+ test('shows nearest store based on geolocation', async ({ browser }) => {
42
+ const context = await browser.newContext({
43
+ geolocation: { latitude: 40.7128, longitude: -74.0060 }, // New York
44
+ permissions: ['geolocation'],
45
+ });
46
+ const page = await context.newPage();
47
+
48
+ await page.goto('/store-locator');
49
+ await page.getByRole('button', { name: 'Find nearby stores' }).click();
50
+
51
+ await expect(page.getByText('Manhattan Store')).toBeVisible();
52
+ await context.close();
53
+ });
54
+
55
+ test('update location mid-test for moving user', async ({ browser }) => {
56
+ const context = await browser.newContext({
57
+ geolocation: { latitude: 37.7749, longitude: -122.4194 }, // San Francisco
58
+ permissions: ['geolocation'],
59
+ });
60
+ const page = await context.newPage();
61
+
62
+ await page.goto('/delivery-tracker');
63
+ await expect(page.getByTestId('current-city')).toHaveText('San Francisco');
64
+
65
+ // Simulate user moving to a new location
66
+ await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 }); // Los Angeles
67
+
68
+ // Trigger location refresh
69
+ await page.getByRole('button', { name: 'Update location' }).click();
70
+ await expect(page.getByTestId('current-city')).toHaveText('Los Angeles');
71
+
72
+ await context.close();
73
+ });
74
+ ```
75
+
76
+ **JavaScript**
77
+ ```javascript
78
+ const { test, expect } = require('@playwright/test');
79
+
80
+ test('shows nearest store based on geolocation', async ({ browser }) => {
81
+ const context = await browser.newContext({
82
+ geolocation: { latitude: 40.7128, longitude: -74.0060 },
83
+ permissions: ['geolocation'],
84
+ });
85
+ const page = await context.newPage();
86
+
87
+ await page.goto('/store-locator');
88
+ await page.getByRole('button', { name: 'Find nearby stores' }).click();
89
+
90
+ await expect(page.getByText('Manhattan Store')).toBeVisible();
91
+ await context.close();
92
+ });
93
+
94
+ test('update location mid-test', async ({ browser }) => {
95
+ const context = await browser.newContext({
96
+ geolocation: { latitude: 37.7749, longitude: -122.4194 },
97
+ permissions: ['geolocation'],
98
+ });
99
+ const page = await context.newPage();
100
+
101
+ await page.goto('/delivery-tracker');
102
+ await expect(page.getByTestId('current-city')).toHaveText('San Francisco');
103
+
104
+ await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
105
+ await page.getByRole('button', { name: 'Update location' }).click();
106
+ await expect(page.getByTestId('current-city')).toHaveText('Los Angeles');
107
+
108
+ await context.close();
109
+ });
110
+ ```
111
+
112
+ You can also set geolocation globally in `playwright.config`:
113
+
114
+ ```typescript
115
+ // playwright.config.ts
116
+ import { defineConfig } from '@playwright/test';
117
+
118
+ export default defineConfig({
119
+ use: {
120
+ geolocation: { latitude: 51.5074, longitude: -0.1278 }, // London
121
+ permissions: ['geolocation'],
122
+ },
123
+ });
124
+ ```
125
+
126
+ ### Permissions
127
+
128
+ **Use when**: Your app requests browser permissions -- notifications, camera, microphone, geolocation, clipboard.
129
+ **Avoid when**: Your app does not use the Permissions API.
130
+
131
+ Grant permissions at context creation. Playwright does not show permission dialogs; you pre-grant or deny them.
132
+
133
+ **TypeScript**
134
+ ```typescript
135
+ import { test, expect } from '@playwright/test';
136
+
137
+ test('notification permission granted shows notification UI', async ({ browser }) => {
138
+ const context = await browser.newContext({
139
+ permissions: ['notifications'],
140
+ });
141
+ const page = await context.newPage();
142
+
143
+ await page.goto('/settings/notifications');
144
+
145
+ // App checks Notification.permission and shows the toggle
146
+ await expect(page.getByRole('switch', { name: 'Enable notifications' })).toBeEnabled();
147
+
148
+ await context.close();
149
+ });
150
+
151
+ test('notification permission denied shows upgrade prompt', async ({ browser }) => {
152
+ // No 'notifications' in permissions = denied
153
+ const context = await browser.newContext({
154
+ permissions: [],
155
+ });
156
+ const page = await context.newPage();
157
+
158
+ await page.goto('/settings/notifications');
159
+
160
+ await expect(page.getByText('Notifications are blocked')).toBeVisible();
161
+ await context.close();
162
+ });
163
+
164
+ test('grant permissions mid-test', async ({ browser }) => {
165
+ const context = await browser.newContext();
166
+ const page = await context.newPage();
167
+ await page.goto('/camera-app');
168
+
169
+ // Initially no camera permission
170
+ await expect(page.getByText('Camera access needed')).toBeVisible();
171
+
172
+ // Grant permission dynamically
173
+ await context.grantPermissions(['camera'], { origin: 'https://localhost:3000' });
174
+
175
+ await page.getByRole('button', { name: 'Enable camera' }).click();
176
+ await expect(page.getByTestId('camera-preview')).toBeVisible();
177
+
178
+ // Revoke all permissions
179
+ await context.clearPermissions();
180
+ await context.close();
181
+ });
182
+ ```
183
+
184
+ **JavaScript**
185
+ ```javascript
186
+ const { test, expect } = require('@playwright/test');
187
+
188
+ test('notification permission granted shows notification UI', async ({ browser }) => {
189
+ const context = await browser.newContext({
190
+ permissions: ['notifications'],
191
+ });
192
+ const page = await context.newPage();
193
+
194
+ await page.goto('/settings/notifications');
195
+ await expect(page.getByRole('switch', { name: 'Enable notifications' })).toBeEnabled();
196
+ await context.close();
197
+ });
198
+
199
+ test('grant permissions mid-test', async ({ browser }) => {
200
+ const context = await browser.newContext();
201
+ const page = await context.newPage();
202
+ await page.goto('/camera-app');
203
+
204
+ await context.grantPermissions(['camera'], { origin: 'https://localhost:3000' });
205
+ await page.getByRole('button', { name: 'Enable camera' }).click();
206
+ await expect(page.getByTestId('camera-preview')).toBeVisible();
207
+
208
+ await context.clearPermissions();
209
+ await context.close();
210
+ });
211
+ ```
212
+
213
+ ### Clipboard API
214
+
215
+ **Use when**: Testing copy/paste functionality, "Copy to clipboard" buttons, or paste-from-clipboard features.
216
+ **Avoid when**: Your app does not interact with the clipboard.
217
+
218
+ **TypeScript**
219
+ ```typescript
220
+ import { test, expect } from '@playwright/test';
221
+
222
+ test('copy button puts text on clipboard', async ({ browser }) => {
223
+ const context = await browser.newContext({
224
+ permissions: ['clipboard-read', 'clipboard-write'],
225
+ });
226
+ const page = await context.newPage();
227
+
228
+ await page.goto('/share');
229
+ await page.getByRole('button', { name: 'Copy link' }).click();
230
+
231
+ // Read clipboard content
232
+ const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
233
+ expect(clipboardText).toContain('https://example.com/share/');
234
+
235
+ await context.close();
236
+ });
237
+
238
+ test('paste from clipboard into editor', async ({ browser }) => {
239
+ const context = await browser.newContext({
240
+ permissions: ['clipboard-read', 'clipboard-write'],
241
+ });
242
+ const page = await context.newPage();
243
+
244
+ await page.goto('/editor');
245
+
246
+ // Write to clipboard programmatically
247
+ await page.evaluate(() => navigator.clipboard.writeText('Pasted content from clipboard'));
248
+
249
+ // Focus the editor and trigger paste
250
+ const editor = page.getByRole('textbox', { name: 'Editor' });
251
+ await editor.focus();
252
+ await page.keyboard.press('ControlOrMeta+v');
253
+
254
+ await expect(editor).toContainText('Pasted content from clipboard');
255
+
256
+ await context.close();
257
+ });
258
+ ```
259
+
260
+ **JavaScript**
261
+ ```javascript
262
+ const { test, expect } = require('@playwright/test');
263
+
264
+ test('copy button puts text on clipboard', async ({ browser }) => {
265
+ const context = await browser.newContext({
266
+ permissions: ['clipboard-read', 'clipboard-write'],
267
+ });
268
+ const page = await context.newPage();
269
+
270
+ await page.goto('/share');
271
+ await page.getByRole('button', { name: 'Copy link' }).click();
272
+
273
+ const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
274
+ expect(clipboardText).toContain('https://example.com/share/');
275
+ await context.close();
276
+ });
277
+ ```
278
+
279
+ ### Camera and Microphone Mocking
280
+
281
+ **Use when**: Testing video calls, QR code scanners, voice recording, or any feature using `getUserMedia`.
282
+ **Avoid when**: Your app does not use camera or microphone.
283
+
284
+ Chromium can use fake media devices. This is not available in Firefox or WebKit.
285
+
286
+ **TypeScript**
287
+ ```typescript
288
+ import { test, expect, chromium } from '@playwright/test';
289
+
290
+ test('video call shows local preview with fake camera', async () => {
291
+ // Launch Chromium with fake media streams
292
+ const browser = await chromium.launch({
293
+ args: [
294
+ '--use-fake-device-for-media-stream',
295
+ '--use-fake-ui-for-media-stream',
296
+ ],
297
+ });
298
+
299
+ const context = await browser.newContext({
300
+ permissions: ['camera', 'microphone'],
301
+ });
302
+ const page = await context.newPage();
303
+
304
+ await page.goto('/video-call');
305
+ await page.getByRole('button', { name: 'Start camera' }).click();
306
+
307
+ // Verify the video element is playing
308
+ const isPlaying = await page.evaluate(() => {
309
+ const video = document.querySelector('video#local-preview') as HTMLVideoElement;
310
+ return video && !video.paused && video.readyState >= 2;
311
+ });
312
+ expect(isPlaying).toBe(true);
313
+
314
+ await context.close();
315
+ await browser.close();
316
+ });
317
+ ```
318
+
319
+ **JavaScript**
320
+ ```javascript
321
+ const { test, expect, chromium } = require('@playwright/test');
322
+
323
+ test('video call shows local preview with fake camera', async () => {
324
+ const browser = await chromium.launch({
325
+ args: [
326
+ '--use-fake-device-for-media-stream',
327
+ '--use-fake-ui-for-media-stream',
328
+ ],
329
+ });
330
+
331
+ const context = await browser.newContext({
332
+ permissions: ['camera', 'microphone'],
333
+ });
334
+ const page = await context.newPage();
335
+
336
+ await page.goto('/video-call');
337
+ await page.getByRole('button', { name: 'Start camera' }).click();
338
+
339
+ const isPlaying = await page.evaluate(() => {
340
+ const video = document.querySelector('video#local-preview');
341
+ return video && !video.paused && video.readyState >= 2;
342
+ });
343
+ expect(isPlaying).toBe(true);
344
+
345
+ await context.close();
346
+ await browser.close();
347
+ });
348
+ ```
349
+
350
+ ### localStorage and sessionStorage
351
+
352
+ **Use when**: Your app persists state, tokens, preferences, or feature flags in web storage.
353
+ **Avoid when**: You can set the state through the UI or API instead. Prefer those approaches for realism.
354
+
355
+ Playwright 1.61 adds a first-class Web Storage API — `page.localStorage` and `page.sessionStorage` — with `getItem`, `setItem`, `removeItem`, `clear`, and `items()`. Prefer it over `page.evaluate()`: no serialization boilerplate, and it works even when the page has a strict CSP that blocks script evaluation.
356
+
357
+ **TypeScript**
358
+ ```typescript
359
+ import { test, expect } from '@playwright/test';
360
+
361
+ test('app loads dark theme from localStorage preference', async ({ page }) => {
362
+ // Set localStorage before the app reads it (Playwright 1.61+)
363
+ await page.goto('/');
364
+ await page.localStorage.setItem('theme', 'dark');
365
+
366
+ // Reload to pick up the stored preference
367
+ await page.reload();
368
+
369
+ await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
370
+ });
371
+
372
+ test('clear localStorage between scenarios', async ({ page }) => {
373
+ await page.goto('/');
374
+
375
+ // Seed some data
376
+ await page.localStorage.setItem('cart', JSON.stringify([{ id: 1, qty: 2 }]));
377
+ await page.localStorage.setItem('user_prefs', JSON.stringify({ currency: 'EUR' }));
378
+
379
+ // Read and verify
380
+ const cart = JSON.parse((await page.localStorage.getItem('cart')) ?? '[]');
381
+ expect(cart).toHaveLength(1);
382
+
383
+ // List everything stored
384
+ const all = await page.localStorage.items();
385
+
386
+ // Clear specific keys
387
+ await page.localStorage.removeItem('cart');
388
+
389
+ // Or clear everything
390
+ await page.localStorage.clear();
391
+ });
392
+
393
+ test('sessionStorage survives navigations within the session', async ({ page }) => {
394
+ await page.goto('/step-1');
395
+ await page.sessionStorage.setItem('wizard_step', '1');
396
+
397
+ await page.goto('/step-2');
398
+ expect(await page.sessionStorage.getItem('wizard_step')).toBe('1');
399
+ });
400
+ ```
401
+
402
+ **JavaScript**
403
+ ```javascript
404
+ const { test, expect } = require('@playwright/test');
405
+
406
+ test('app loads dark theme from localStorage preference', async ({ page }) => {
407
+ await page.goto('/');
408
+ await page.localStorage.setItem('theme', 'dark');
409
+ await page.reload();
410
+
411
+ await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
412
+ });
413
+
414
+ test('sessionStorage survives navigations within the session', async ({ page }) => {
415
+ await page.goto('/step-1');
416
+ await page.sessionStorage.setItem('wizard_step', '1');
417
+
418
+ await page.goto('/step-2');
419
+ expect(await page.sessionStorage.getItem('wizard_step')).toBe('1');
420
+ });
421
+ ```
422
+
423
+ **On Playwright < 1.61**, fall back to `page.evaluate()`:
424
+
425
+ ```typescript
426
+ await page.evaluate(() => localStorage.setItem('theme', 'dark'));
427
+ const step = await page.evaluate(() => sessionStorage.getItem('wizard_step'));
428
+ ```
429
+
430
+ ### IndexedDB Testing
431
+
432
+ **Use when**: Your app uses IndexedDB for offline storage, caching, or large datasets (progressive web apps, offline-first apps).
433
+ **Avoid when**: Your app only uses localStorage or server-side storage.
434
+
435
+ **TypeScript**
436
+ ```typescript
437
+ import { test, expect } from '@playwright/test';
438
+
439
+ test('offline-first app stores data in IndexedDB', async ({ page }) => {
440
+ await page.goto('/notes');
441
+
442
+ // Create a note through the UI
443
+ await page.getByRole('button', { name: 'New note' }).click();
444
+ await page.getByRole('textbox', { name: 'Title' }).fill('Test Note');
445
+ await page.getByRole('textbox', { name: 'Content' }).fill('This is stored in IndexedDB');
446
+ await page.getByRole('button', { name: 'Save' }).click();
447
+
448
+ // Verify it is stored in IndexedDB
449
+ const storedNotes = await page.evaluate(() => {
450
+ return new Promise((resolve, reject) => {
451
+ const request = indexedDB.open('NotesDB', 1);
452
+ request.onsuccess = () => {
453
+ const db = request.result;
454
+ const tx = db.transaction('notes', 'readonly');
455
+ const store = tx.objectStore('notes');
456
+ const getAll = store.getAll();
457
+ getAll.onsuccess = () => resolve(getAll.result);
458
+ getAll.onerror = () => reject(getAll.error);
459
+ };
460
+ request.onerror = () => reject(request.error);
461
+ });
462
+ });
463
+
464
+ expect(storedNotes).toHaveLength(1);
465
+ expect(storedNotes[0]).toMatchObject({
466
+ title: 'Test Note',
467
+ content: 'This is stored in IndexedDB',
468
+ });
469
+ });
470
+
471
+ test('clear IndexedDB for a clean test state', async ({ page }) => {
472
+ await page.goto('/');
473
+
474
+ // Delete the entire database
475
+ await page.evaluate(() => {
476
+ return new Promise((resolve, reject) => {
477
+ const request = indexedDB.deleteDatabase('NotesDB');
478
+ request.onsuccess = () => resolve(undefined);
479
+ request.onerror = () => reject(request.error);
480
+ });
481
+ });
482
+
483
+ await page.reload();
484
+ await expect(page.getByText('No notes yet')).toBeVisible();
485
+ });
486
+ ```
487
+
488
+ **JavaScript**
489
+ ```javascript
490
+ const { test, expect } = require('@playwright/test');
491
+
492
+ test('offline-first app stores data in IndexedDB', async ({ page }) => {
493
+ await page.goto('/notes');
494
+
495
+ await page.getByRole('button', { name: 'New note' }).click();
496
+ await page.getByRole('textbox', { name: 'Title' }).fill('Test Note');
497
+ await page.getByRole('textbox', { name: 'Content' }).fill('This is stored in IndexedDB');
498
+ await page.getByRole('button', { name: 'Save' }).click();
499
+
500
+ const storedNotes = await page.evaluate(() => {
501
+ return new Promise((resolve, reject) => {
502
+ const request = indexedDB.open('NotesDB', 1);
503
+ request.onsuccess = () => {
504
+ const db = request.result;
505
+ const tx = db.transaction('notes', 'readonly');
506
+ const store = tx.objectStore('notes');
507
+ const getAll = store.getAll();
508
+ getAll.onsuccess = () => resolve(getAll.result);
509
+ getAll.onerror = () => reject(getAll.error);
510
+ };
511
+ request.onerror = () => reject(request.error);
512
+ });
513
+ });
514
+
515
+ expect(storedNotes).toHaveLength(1);
516
+ expect(storedNotes[0]).toMatchObject({
517
+ title: 'Test Note',
518
+ content: 'This is stored in IndexedDB',
519
+ });
520
+ });
521
+ ```
522
+
523
+ ### Notifications
524
+
525
+ **Use when**: Your app uses the browser Notification API to show desktop notifications.
526
+ **Avoid when**: Notifications are purely server-side (push without Notification API).
527
+
528
+ Playwright cannot capture the actual system notification. Instead, mock the Notification constructor and verify the app calls it correctly.
529
+
530
+ **TypeScript**
531
+ ```typescript
532
+ import { test, expect } from '@playwright/test';
533
+
534
+ test('app triggers a browser notification on new message', async ({ browser }) => {
535
+ const context = await browser.newContext({
536
+ permissions: ['notifications'],
537
+ });
538
+ const page = await context.newPage();
539
+
540
+ // Intercept Notification constructor to capture calls
541
+ await page.evaluate(() => {
542
+ (window as any).__notifications = [];
543
+ const OriginalNotification = window.Notification;
544
+ (window as any).Notification = class MockNotification {
545
+ constructor(title: string, options?: NotificationOptions) {
546
+ (window as any).__notifications.push({ title, ...options });
547
+ }
548
+ static get permission() { return 'granted'; }
549
+ static requestPermission() { return Promise.resolve('granted' as NotificationPermission); }
550
+ };
551
+ });
552
+
553
+ await page.goto('/chat');
554
+
555
+ // Simulate receiving a message that triggers a notification
556
+ await page.evaluate(() => {
557
+ window.dispatchEvent(new CustomEvent('new-message', {
558
+ detail: { from: 'Alice', text: 'Hey there!' },
559
+ }));
560
+ });
561
+
562
+ // Check the notification was created with correct content
563
+ const notifications = await page.evaluate(() => (window as any).__notifications);
564
+ expect(notifications).toHaveLength(1);
565
+ expect(notifications[0].title).toBe('New message from Alice');
566
+ expect(notifications[0].body).toBe('Hey there!');
567
+
568
+ await context.close();
569
+ });
570
+ ```
571
+
572
+ **JavaScript**
573
+ ```javascript
574
+ const { test, expect } = require('@playwright/test');
575
+
576
+ test('app triggers a browser notification on new message', async ({ browser }) => {
577
+ const context = await browser.newContext({
578
+ permissions: ['notifications'],
579
+ });
580
+ const page = await context.newPage();
581
+
582
+ await page.evaluate(() => {
583
+ window.__notifications = [];
584
+ window.Notification = class MockNotification {
585
+ constructor(title, options) {
586
+ window.__notifications.push({ title, ...options });
587
+ }
588
+ static get permission() { return 'granted'; }
589
+ static requestPermission() { return Promise.resolve('granted'); }
590
+ };
591
+ });
592
+
593
+ await page.goto('/chat');
594
+
595
+ await page.evaluate(() => {
596
+ window.dispatchEvent(new CustomEvent('new-message', {
597
+ detail: { from: 'Alice', text: 'Hey there!' },
598
+ }));
599
+ });
600
+
601
+ const notifications = await page.evaluate(() => window.__notifications);
602
+ expect(notifications).toHaveLength(1);
603
+ expect(notifications[0].title).toBe('New message from Alice');
604
+ await context.close();
605
+ });
606
+ ```
607
+
608
+ ### Preserving a Real Browser's Behavior on Attach (`connectOverCDP({ noDefaults })`, Playwright 1.60+)
609
+
610
+ **Use when**: You attach to an existing, user-owned browser over CDP and need it to keep behaving exactly as the user left it — Playwright otherwise overrides the default context's download, focus, and media emulation.
611
+ **Avoid when**: You launched the browser for testing. The default overrides give you deterministic downloads and focus/media emulation, which is usually what tests want.
612
+
613
+ Playwright 1.60 adds `noDefaults` to `browserType.connectOverCDP()` to disable those default-context overrides:
614
+
615
+ ```typescript
616
+ import { chromium } from 'playwright';
617
+
618
+ test('drive a real browser without changing its focus/media behavior', async () => {
619
+ const browser = await chromium.connectOverCDP('http://localhost:9222', {
620
+ noDefaults: true, // don't override download/focus/media emulation
621
+ });
622
+
623
+ const context = browser.contexts()[0];
624
+ const page = context.pages()[0] ?? (await context.newPage());
625
+ await page.goto('https://example.com');
626
+
627
+ await browser.close(); // detaches without closing the user's browser
628
+ });
629
+ ```
630
+
631
+ See [playwright-cli/session-management.md](../playwright-cli/session-management.md#attaching-to-a-real-browser-without-overrides-connectovercdp-nodefaults-playwright-160) for the CLI/agent attach workflow.
632
+
633
+ ## Decision Guide
634
+
635
+ | Browser API | How to Test | Key Configuration |
636
+ |---|---|---|
637
+ | Geolocation | `geolocation` context option + `permissions: ['geolocation']` | `context.setGeolocation()` for mid-test changes |
638
+ | Permissions (any) | `permissions` array in context options | `context.grantPermissions()` / `context.clearPermissions()` |
639
+ | Clipboard | Grant `clipboard-read`/`clipboard-write` + `page.evaluate(navigator.clipboard...)` | Requires secure context (HTTPS or localhost) |
640
+ | Notifications | Mock `Notification` constructor via `page.evaluate` | Grant `notifications` permission; capture constructor calls |
641
+ | Camera / Microphone | Chromium `--use-fake-device-for-media-stream` launch arg | Only works in Chromium; grant `camera`/`microphone` permissions |
642
+ | localStorage | `page.evaluate(() => localStorage.getItem/setItem(...))` | Set before navigation or reload to take effect |
643
+ | sessionStorage | `page.evaluate(() => sessionStorage.getItem/setItem(...))` | Scoped to the browsing session; cleared on context close |
644
+ | IndexedDB | `page.evaluate` with `indexedDB.open()` | Wrap in Promises for async operations |
645
+
646
+ ## Anti-Patterns
647
+
648
+ | Don't Do This | Problem | Do This Instead |
649
+ |---|---|---|
650
+ | Set `geolocation` without granting the permission | `getCurrentPosition` returns permission error | Always pair `geolocation` with `permissions: ['geolocation']` |
651
+ | Test clipboard without secure context | `navigator.clipboard` throws in non-HTTPS contexts | Use `localhost` or configure HTTPS in your test server |
652
+ | Access localStorage before navigating to the origin | `page.evaluate` runs in `about:blank` context initially | `page.goto('/')` first, then set localStorage, then reload |
653
+ | Store test tokens in localStorage directly | Bypasses auth flow; may mask real login bugs | Use `storageState` or proper auth fixtures |
654
+ | Rely on IndexedDB state from a previous test | Tests must be independent | Clear or delete the database in test setup |
655
+ | Skip cleanup of injected mocks (Notification, etc.) | Mock leaks into subsequent tests if using the same context | Each test gets a fresh context by default; only a concern with shared contexts |
656
+
657
+ ## Troubleshooting
658
+
659
+ | Symptom | Cause | Fix |
660
+ |---|---|---|
661
+ | `geolocation` returns `undefined` in the app | Permission not granted | Add `permissions: ['geolocation']` to context options |
662
+ | Clipboard `readText()` throws `DOMException` | Missing clipboard permissions or non-secure context | Grant `clipboard-read`; ensure HTTPS or localhost |
663
+ | `localStorage.getItem()` returns `null` after `setItem` | Set was done on a different origin or before navigation | Verify you navigated to the correct origin before calling `setItem` |
664
+ | Camera not working in Firefox/WebKit | Fake media devices are Chromium-only | Skip camera tests on non-Chromium browsers or mock `getUserMedia` via `page.evaluate` |
665
+ | IndexedDB `evaluate` returns `undefined` | Forgot to return the Promise | Ensure the `page.evaluate` callback returns `new Promise(...)` |
666
+ | Permissions change not reflected | App caches permission state on load | Reload the page after `grantPermissions()` or `clearPermissions()` |
667
+
668
+ ## Related
669
+
670
+ - [core/configuration.md](configuration.md) -- global geolocation/permissions in config
671
+ - [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- offline and cache testing using IndexedDB and service workers
672
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrap browser API setup in reusable fixtures
673
+ - [core/debugging.md](debugging.md) -- inspecting storage and permissions in Playwright traces