@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,524 @@
1
+ # Service Workers and PWA Testing
2
+
3
+ > **When to use**: When your application is a Progressive Web App (PWA) or uses service workers for offline support, caching, push notifications, or background sync.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Service workers require a persistent context — use launchPersistentContext
10
+ const context = await chromium.launchPersistentContext(userDataDir, {
11
+ baseURL: 'https://localhost:3000',
12
+ });
13
+
14
+ // Enable/disable offline mode
15
+ await context.setOffline(true); // Go offline
16
+ await context.setOffline(false); // Come back online
17
+
18
+ // Wait for service worker to register
19
+ const sw = await context.waitForEvent('serviceworker');
20
+ console.log('SW URL:', sw.url());
21
+ ```
22
+
23
+ ## Patterns
24
+
25
+ ### Service Worker Registration
26
+
27
+ **Use when**: You need to verify that your service worker registers successfully, activates, and controls the page.
28
+ **Avoid when**: Your app does not use service workers.
29
+
30
+ Service workers require a persistent browser context because they are tied to a profile. The default `page` fixture uses a temporary context that does not persist service worker registrations across navigations reliably.
31
+
32
+ **TypeScript**
33
+ ```typescript
34
+ import { test as base, expect, chromium, BrowserContext } from '@playwright/test';
35
+ import path from 'path';
36
+ import fs from 'fs';
37
+
38
+ // Create a fixture with a persistent context for SW testing
39
+ const test = base.extend<{ swContext: BrowserContext }>({
40
+ swContext: async ({}, use) => {
41
+ const userDataDir = path.join(__dirname, '.tmp-profile');
42
+ const context = await chromium.launchPersistentContext(userDataDir, {
43
+ baseURL: 'https://localhost:3000',
44
+ });
45
+ await use(context);
46
+ await context.close();
47
+ fs.rmSync(userDataDir, { recursive: true, force: true });
48
+ },
49
+ });
50
+
51
+ test('service worker registers and activates', async ({ swContext }) => {
52
+ const page = await swContext.newPage();
53
+
54
+ // Listen for the service worker event
55
+ const swPromise = swContext.waitForEvent('serviceworker');
56
+
57
+ await page.goto('/');
58
+
59
+ const sw = await swPromise;
60
+ expect(sw.url()).toContain('service-worker.js');
61
+
62
+ // Verify the SW is controlling the page
63
+ const isControlled = await page.evaluate(() => {
64
+ return navigator.serviceWorker.controller !== null;
65
+ });
66
+ expect(isControlled).toBe(true);
67
+ });
68
+ ```
69
+
70
+ **JavaScript**
71
+ ```javascript
72
+ const { test: base, expect, chromium } = require('@playwright/test');
73
+ const path = require('path');
74
+ const fs = require('fs');
75
+
76
+ const test = base.extend({
77
+ swContext: async ({}, use) => {
78
+ const userDataDir = path.join(__dirname, '.tmp-profile');
79
+ const context = await chromium.launchPersistentContext(userDataDir, {
80
+ baseURL: 'https://localhost:3000',
81
+ });
82
+ await use(context);
83
+ await context.close();
84
+ fs.rmSync(userDataDir, { recursive: true, force: true });
85
+ },
86
+ });
87
+
88
+ test('service worker registers and activates', async ({ swContext }) => {
89
+ const page = await swContext.newPage();
90
+ const swPromise = swContext.waitForEvent('serviceworker');
91
+
92
+ await page.goto('/');
93
+
94
+ const sw = await swPromise;
95
+ expect(sw.url()).toContain('service-worker.js');
96
+
97
+ const isControlled = await page.evaluate(() => {
98
+ return navigator.serviceWorker.controller !== null;
99
+ });
100
+ expect(isControlled).toBe(true);
101
+ });
102
+ ```
103
+
104
+ ### Offline Mode Testing
105
+
106
+ **Use when**: Your PWA should work offline -- serving cached pages, showing offline indicators, queuing data for sync.
107
+ **Avoid when**: Your app has no offline support.
108
+
109
+ **TypeScript**
110
+ ```typescript
111
+ import { test, expect } from '@playwright/test';
112
+
113
+ test('app serves cached page when offline', async ({ swContext }) => {
114
+ const page = await swContext.newPage();
115
+
116
+ // First visit — caches the page and assets
117
+ await page.goto('/');
118
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
119
+
120
+ // Wait for the service worker to finish caching
121
+ await page.evaluate(async () => {
122
+ const registration = await navigator.serviceWorker.ready;
123
+ // Wait for the SW to be in the 'activated' state
124
+ return registration.active?.state === 'activated';
125
+ });
126
+
127
+ // Go offline
128
+ await swContext.setOffline(true);
129
+
130
+ // Reload — should serve from cache
131
+ await page.reload();
132
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
133
+
134
+ // Verify offline indicator appears
135
+ await expect(page.getByText('You are offline')).toBeVisible();
136
+
137
+ // Come back online
138
+ await swContext.setOffline(false);
139
+ await expect(page.getByText('You are offline')).toBeHidden();
140
+ });
141
+
142
+ test('form submission queues when offline', async ({ swContext }) => {
143
+ const page = await swContext.newPage();
144
+ await page.goto('/feedback');
145
+
146
+ // Ensure SW is active
147
+ await page.evaluate(() => navigator.serviceWorker.ready);
148
+
149
+ // Go offline
150
+ await swContext.setOffline(true);
151
+
152
+ // Submit a form while offline
153
+ await page.getByLabel('Feedback').fill('Great product!');
154
+ await page.getByRole('button', { name: 'Submit' }).click();
155
+
156
+ // App should show a "queued" message, not an error
157
+ await expect(page.getByText('Saved offline. Will sync when connected.')).toBeVisible();
158
+
159
+ // Come back online — the queued data should sync
160
+ await swContext.setOffline(false);
161
+ await expect(page.getByText('Feedback submitted successfully')).toBeVisible({ timeout: 10000 });
162
+ });
163
+ ```
164
+
165
+ **JavaScript**
166
+ ```javascript
167
+ const { test, expect } = require('@playwright/test');
168
+
169
+ test('app serves cached page when offline', async ({ swContext }) => {
170
+ const page = await swContext.newPage();
171
+
172
+ await page.goto('/');
173
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
174
+
175
+ await page.evaluate(async () => {
176
+ const registration = await navigator.serviceWorker.ready;
177
+ return registration.active?.state === 'activated';
178
+ });
179
+
180
+ await swContext.setOffline(true);
181
+ await page.reload();
182
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
183
+ await expect(page.getByText('You are offline')).toBeVisible();
184
+
185
+ await swContext.setOffline(false);
186
+ await expect(page.getByText('You are offline')).toBeHidden();
187
+ });
188
+ ```
189
+
190
+ ### Cache Verification
191
+
192
+ **Use when**: You need to confirm that specific resources are cached by the service worker.
193
+ **Avoid when**: You trust the framework's caching strategy and only need to verify the offline UX (test offline mode instead).
194
+
195
+ **TypeScript**
196
+ ```typescript
197
+ import { test, expect } from '@playwright/test';
198
+
199
+ test('critical assets are cached by the service worker', async ({ swContext }) => {
200
+ const page = await swContext.newPage();
201
+ await page.goto('/');
202
+
203
+ // Wait for SW to be ready
204
+ await page.evaluate(() => navigator.serviceWorker.ready);
205
+
206
+ // Check the cache contents
207
+ const cachedUrls = await page.evaluate(async () => {
208
+ const cacheNames = await caches.keys();
209
+ const allUrls: string[] = [];
210
+ for (const name of cacheNames) {
211
+ const cache = await caches.open(name);
212
+ const keys = await cache.keys();
213
+ allUrls.push(...keys.map((req) => new URL(req.url).pathname));
214
+ }
215
+ return allUrls;
216
+ });
217
+
218
+ // Verify critical resources are cached
219
+ expect(cachedUrls).toContain('/');
220
+ expect(cachedUrls).toContain('/offline.html');
221
+ expect(cachedUrls.some((url) => url.endsWith('.js'))).toBe(true);
222
+ expect(cachedUrls.some((url) => url.endsWith('.css'))).toBe(true);
223
+ });
224
+
225
+ test('cache is invalidated after service worker update', async ({ swContext }) => {
226
+ const page = await swContext.newPage();
227
+ await page.goto('/');
228
+
229
+ // Get initial cache version
230
+ const initialCaches = await page.evaluate(() => caches.keys());
231
+
232
+ // Simulate a new SW version by clearing and reloading
233
+ await page.evaluate(async () => {
234
+ const registration = await navigator.serviceWorker.getRegistration();
235
+ await registration?.update();
236
+ });
237
+
238
+ // After update, old caches should be cleaned up
239
+ await page.reload();
240
+ await page.evaluate(() => navigator.serviceWorker.ready);
241
+
242
+ const updatedCaches = await page.evaluate(() => caches.keys());
243
+
244
+ // The app should manage cache names with versioning
245
+ expect(updatedCaches.length).toBeGreaterThanOrEqual(1);
246
+ });
247
+ ```
248
+
249
+ **JavaScript**
250
+ ```javascript
251
+ const { test, expect } = require('@playwright/test');
252
+
253
+ test('critical assets are cached by the service worker', async ({ swContext }) => {
254
+ const page = await swContext.newPage();
255
+ await page.goto('/');
256
+ await page.evaluate(() => navigator.serviceWorker.ready);
257
+
258
+ const cachedUrls = await page.evaluate(async () => {
259
+ const cacheNames = await caches.keys();
260
+ const allUrls = [];
261
+ for (const name of cacheNames) {
262
+ const cache = await caches.open(name);
263
+ const keys = await cache.keys();
264
+ allUrls.push(...keys.map((req) => new URL(req.url).pathname));
265
+ }
266
+ return allUrls;
267
+ });
268
+
269
+ expect(cachedUrls).toContain('/');
270
+ expect(cachedUrls).toContain('/offline.html');
271
+ });
272
+ ```
273
+
274
+ ### PWA Install Prompt
275
+
276
+ **Use when**: You need to test the "Add to Home Screen" / PWA install flow.
277
+ **Avoid when**: Your app is not a PWA or does not handle the `beforeinstallprompt` event.
278
+
279
+ The `beforeinstallprompt` event cannot be synthetically triggered by Playwright. Instead, verify that your app captures and handles the event correctly.
280
+
281
+ **TypeScript**
282
+ ```typescript
283
+ import { test, expect } from '@playwright/test';
284
+
285
+ test('app captures install prompt and shows install button', async ({ swContext }) => {
286
+ const page = await swContext.newPage();
287
+
288
+ // Mock the beforeinstallprompt event
289
+ await page.addInitScript(() => {
290
+ let deferredPrompt: Event | null = null;
291
+
292
+ // Simulate the browser firing beforeinstallprompt
293
+ window.addEventListener('load', () => {
294
+ const event = new Event('beforeinstallprompt');
295
+ (event as any).preventDefault = () => {};
296
+ (event as any).prompt = () => Promise.resolve();
297
+ (event as any).userChoice = Promise.resolve({ outcome: 'accepted' });
298
+
299
+ window.dispatchEvent(event);
300
+ });
301
+ });
302
+
303
+ await page.goto('/');
304
+
305
+ // Your app should show an install button when it captures the prompt
306
+ await expect(page.getByRole('button', { name: 'Install app' })).toBeVisible();
307
+ });
308
+
309
+ test('web app manifest is valid', async ({ swContext }) => {
310
+ const page = await swContext.newPage();
311
+ await page.goto('/');
312
+
313
+ // Verify manifest link exists
314
+ const manifestUrl = await page.evaluate(() => {
315
+ const link = document.querySelector('link[rel="manifest"]') as HTMLLinkElement;
316
+ return link?.href;
317
+ });
318
+ expect(manifestUrl).toBeTruthy();
319
+
320
+ // Fetch and validate the manifest
321
+ const response = await page.request.get(manifestUrl!);
322
+ expect(response.ok()).toBe(true);
323
+
324
+ const manifest = await response.json();
325
+ expect(manifest.name).toBeTruthy();
326
+ expect(manifest.icons).toHaveLength(expect.any(Number));
327
+ expect(manifest.start_url).toBeTruthy();
328
+ expect(manifest.display).toBe('standalone');
329
+ });
330
+ ```
331
+
332
+ **JavaScript**
333
+ ```javascript
334
+ const { test, expect } = require('@playwright/test');
335
+
336
+ test('app captures install prompt and shows install button', async ({ swContext }) => {
337
+ const page = await swContext.newPage();
338
+
339
+ await page.addInitScript(() => {
340
+ window.addEventListener('load', () => {
341
+ const event = new Event('beforeinstallprompt');
342
+ event.preventDefault = () => {};
343
+ event.prompt = () => Promise.resolve();
344
+ event.userChoice = Promise.resolve({ outcome: 'accepted' });
345
+ window.dispatchEvent(event);
346
+ });
347
+ });
348
+
349
+ await page.goto('/');
350
+ await expect(page.getByRole('button', { name: 'Install app' })).toBeVisible();
351
+ });
352
+ ```
353
+
354
+ ### Push Notification Testing
355
+
356
+ **Use when**: Your PWA uses the Push API to receive push notifications from a server.
357
+ **Avoid when**: Notifications are handled entirely client-side without the Push API.
358
+
359
+ **TypeScript**
360
+ ```typescript
361
+ import { test, expect } from '@playwright/test';
362
+
363
+ test('push notification triggers UI update', async ({ swContext }) => {
364
+ const page = await swContext.newPage();
365
+ await page.goto('/');
366
+
367
+ // Wait for SW to be ready
368
+ const swURL = await page.evaluate(async () => {
369
+ const reg = await navigator.serviceWorker.ready;
370
+ return reg.active?.scriptURL;
371
+ });
372
+ expect(swURL).toBeTruthy();
373
+
374
+ // Get the service worker and evaluate inside it
375
+ const sw = swContext.serviceWorkers()[0];
376
+
377
+ // Simulate a push event inside the service worker
378
+ await sw.evaluate(async () => {
379
+ const event = new PushEvent('push', {
380
+ data: new TextEncoder().encode(JSON.stringify({
381
+ title: 'New Message',
382
+ body: 'You have a new message from Alice',
383
+ url: '/messages',
384
+ })),
385
+ });
386
+ // @ts-ignore — dispatchEvent works in SW context
387
+ self.dispatchEvent(event);
388
+ });
389
+
390
+ // Verify the app reacts (e.g., shows a badge or notification banner)
391
+ await expect(page.getByTestId('notification-badge')).toHaveText('1');
392
+ });
393
+ ```
394
+
395
+ **JavaScript**
396
+ ```javascript
397
+ const { test, expect } = require('@playwright/test');
398
+
399
+ test('push notification triggers UI update', async ({ swContext }) => {
400
+ const page = await swContext.newPage();
401
+ await page.goto('/');
402
+
403
+ const swURL = await page.evaluate(async () => {
404
+ const reg = await navigator.serviceWorker.ready;
405
+ return reg.active?.scriptURL;
406
+ });
407
+ expect(swURL).toBeTruthy();
408
+
409
+ const sw = swContext.serviceWorkers()[0];
410
+
411
+ await sw.evaluate(async () => {
412
+ const event = new PushEvent('push', {
413
+ data: new TextEncoder().encode(JSON.stringify({
414
+ title: 'New Message',
415
+ body: 'You have a new message from Alice',
416
+ url: '/messages',
417
+ })),
418
+ });
419
+ self.dispatchEvent(event);
420
+ });
421
+
422
+ await expect(page.getByTestId('notification-badge')).toHaveText('1');
423
+ });
424
+ ```
425
+
426
+ ### Background Sync Testing
427
+
428
+ **Use when**: Your app uses the Background Sync API to defer network requests until the user has connectivity.
429
+ **Avoid when**: Your app does not use background sync.
430
+
431
+ **TypeScript**
432
+ ```typescript
433
+ import { test, expect } from '@playwright/test';
434
+
435
+ test('background sync retries failed requests when online', async ({ swContext }) => {
436
+ const page = await swContext.newPage();
437
+ await page.goto('/tasks');
438
+
439
+ // Ensure SW is active
440
+ await page.evaluate(() => navigator.serviceWorker.ready);
441
+
442
+ // Go offline
443
+ await swContext.setOffline(true);
444
+
445
+ // Create a task (will fail to sync)
446
+ await page.getByLabel('New task').fill('Buy groceries');
447
+ await page.getByRole('button', { name: 'Add' }).click();
448
+
449
+ // Task shows as "pending sync"
450
+ await expect(page.getByText('Buy groceries')).toBeVisible();
451
+ await expect(page.getByTestId('sync-status')).toHaveText('Pending');
452
+
453
+ // Come back online — background sync should fire
454
+ await swContext.setOffline(false);
455
+
456
+ // Wait for sync to complete
457
+ await expect(page.getByTestId('sync-status')).toHaveText('Synced', { timeout: 15000 });
458
+ });
459
+ ```
460
+
461
+ **JavaScript**
462
+ ```javascript
463
+ const { test, expect } = require('@playwright/test');
464
+
465
+ test('background sync retries failed requests when online', async ({ swContext }) => {
466
+ const page = await swContext.newPage();
467
+ await page.goto('/tasks');
468
+ await page.evaluate(() => navigator.serviceWorker.ready);
469
+
470
+ await swContext.setOffline(true);
471
+
472
+ await page.getByLabel('New task').fill('Buy groceries');
473
+ await page.getByRole('button', { name: 'Add' }).click();
474
+
475
+ await expect(page.getByText('Buy groceries')).toBeVisible();
476
+ await expect(page.getByTestId('sync-status')).toHaveText('Pending');
477
+
478
+ await swContext.setOffline(false);
479
+ await expect(page.getByTestId('sync-status')).toHaveText('Synced', { timeout: 15000 });
480
+ });
481
+ ```
482
+
483
+ ## Decision Guide
484
+
485
+ | Scenario | Approach | Why |
486
+ |---|---|---|
487
+ | Verify SW registers | `context.waitForEvent('serviceworker')` | Confirms the SW file was fetched and registered |
488
+ | Test offline page serving | `context.setOffline(true)` + reload | Simulates network loss at the browser level |
489
+ | Verify specific assets are cached | `page.evaluate(() => caches.keys())` | Directly queries the Cache API |
490
+ | Test install prompt | `addInitScript` to mock `beforeinstallprompt` | Browser does not fire this event in automation |
491
+ | Validate web manifest | `page.request.get(manifestUrl)` | Fetch and parse the JSON manifest |
492
+ | Test push notifications | `sw.evaluate()` to dispatch PushEvent | Simulates a push message inside the SW |
493
+ | Test background sync | Go offline, perform action, come back online | Verifies the sync queue processes correctly |
494
+ | Test SW update flow | `registration.update()` via `page.evaluate` | Triggers a manual SW update check |
495
+
496
+ ## Anti-Patterns
497
+
498
+ | Don't Do This | Problem | Do This Instead |
499
+ |---|---|---|
500
+ | Test service workers with the default `page` fixture | Default context is temporary; SWs may not persist | Use `launchPersistentContext` for reliable SW testing |
501
+ | Skip `navigator.serviceWorker.ready` before going offline | SW may not have finished caching | Always `await page.evaluate(() => navigator.serviceWorker.ready)` first |
502
+ | Use `page.route()` to simulate offline | Only intercepts Playwright-visible requests; SW fetch events are not intercepted | Use `context.setOffline(true)` for true network-level offline |
503
+ | Test the SW JavaScript file directly with unit tests only | Misses integration with the page and caching behavior | Combine unit tests with Playwright end-to-end SW tests |
504
+ | Assume caches are clean at test start | Previous test runs may leave stale caches | Clear caches in fixture setup or use a fresh `userDataDir` |
505
+ | `waitForTimeout` after `setOffline(false)` | Background sync timing is unpredictable | Use `expect(...).toHaveText('Synced', { timeout: 15000 })` |
506
+
507
+ ## Troubleshooting
508
+
509
+ | Symptom | Cause | Fix |
510
+ |---|---|---|
511
+ | `context.waitForEvent('serviceworker')` never resolves | SW already registered before listener attached | Register listener before `page.goto()`, or check `context.serviceWorkers()` |
512
+ | SW does not activate | Old SW is still controlling the page | Use `skipWaiting()` in the SW or clear the `userDataDir` |
513
+ | Offline test still loads fresh content | SW cache was not populated before going offline | Wait for `navigator.serviceWorker.ready` and verify cache contents first |
514
+ | `context.setOffline(true)` has no effect | Using a non-persistent context or the wrong context | Ensure offline is set on the same context that owns the page |
515
+ | Push event does nothing | SW is not the active controller | Ensure `sw.evaluate()` targets the correct service worker from `context.serviceWorkers()` |
516
+ | Tests interfere with each other | Shared `userDataDir` between tests | Use a unique temp directory per test, cleaned in fixture teardown |
517
+ | Service worker tests only work in Chromium | Firefox and WebKit have limited SW support in Playwright | Run SW tests in Chromium only via `test.skip` or project config |
518
+
519
+ ## Related
520
+
521
+ - [core/browser-apis.md](browser-apis.md) -- IndexedDB and localStorage used alongside service workers
522
+ - [core/configuration.md](configuration.md) -- project-level config for PWA testing
523
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrapping persistent context in fixtures
524
+ - [core/websockets-and-realtime.md](websockets-and-realtime.md) -- real-time features that interact with offline/online state