@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.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- 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
|