@arcadialdev/arcality 2.2.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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# Service Worker Testing
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Service Worker Basics](#service-worker-basics)
|
|
6
|
+
2. [Registration & Lifecycle](#registration--lifecycle)
|
|
7
|
+
3. [Cache Testing](#cache-testing)
|
|
8
|
+
4. [Offline Testing](#offline-testing)
|
|
9
|
+
5. [Push Notifications](#push-notifications)
|
|
10
|
+
6. [Background Sync](#background-sync)
|
|
11
|
+
|
|
12
|
+
## Service Worker Basics
|
|
13
|
+
|
|
14
|
+
### Waiting for Service Worker Registration
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
test("service worker registers", async ({ page }) => {
|
|
18
|
+
await page.goto("/pwa-app");
|
|
19
|
+
|
|
20
|
+
// Wait for SW to register
|
|
21
|
+
const swRegistered = await page.evaluate(async () => {
|
|
22
|
+
if (!("serviceWorker" in navigator)) return false;
|
|
23
|
+
|
|
24
|
+
const registration = await navigator.serviceWorker.ready;
|
|
25
|
+
return !!registration.active;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(swRegistered).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Getting Service Worker State
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
test("check SW state", async ({ page }) => {
|
|
36
|
+
await page.goto("/");
|
|
37
|
+
|
|
38
|
+
const swState = await page.evaluate(async () => {
|
|
39
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
40
|
+
if (!registration) return null;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
installing: !!registration.installing,
|
|
44
|
+
waiting: !!registration.waiting,
|
|
45
|
+
active: !!registration.active,
|
|
46
|
+
scope: registration.scope,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(swState?.active).toBe(true);
|
|
51
|
+
expect(swState?.scope).toContain(page.url());
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Service Worker Context
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
test("access service worker", async ({ context, page }) => {
|
|
59
|
+
await page.goto("/pwa-app");
|
|
60
|
+
|
|
61
|
+
// Get all service workers in context
|
|
62
|
+
const workers = context.serviceWorkers();
|
|
63
|
+
|
|
64
|
+
// Wait for service worker if not yet available
|
|
65
|
+
if (workers.length === 0) {
|
|
66
|
+
await context.waitForEvent("serviceworker");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sw = context.serviceWorkers()[0];
|
|
70
|
+
expect(sw.url()).toContain("sw.js");
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Registration & Lifecycle
|
|
75
|
+
|
|
76
|
+
### Testing SW Update Flow
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
test("service worker updates", async ({ page }) => {
|
|
80
|
+
await page.goto("/pwa-app");
|
|
81
|
+
|
|
82
|
+
// Check for update
|
|
83
|
+
const hasUpdate = await page.evaluate(async () => {
|
|
84
|
+
const registration = await navigator.serviceWorker.ready;
|
|
85
|
+
await registration.update();
|
|
86
|
+
|
|
87
|
+
return new Promise<boolean>((resolve) => {
|
|
88
|
+
if (registration.waiting) {
|
|
89
|
+
resolve(true);
|
|
90
|
+
} else {
|
|
91
|
+
registration.addEventListener("updatefound", () => {
|
|
92
|
+
resolve(true);
|
|
93
|
+
});
|
|
94
|
+
// Timeout if no update
|
|
95
|
+
setTimeout(() => resolve(false), 5000);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// If update found, test skip waiting flow
|
|
101
|
+
if (hasUpdate) {
|
|
102
|
+
await page.evaluate(async () => {
|
|
103
|
+
const registration = await navigator.serviceWorker.ready;
|
|
104
|
+
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Wait for controller change
|
|
108
|
+
await page.evaluate(() => {
|
|
109
|
+
return new Promise<void>((resolve) => {
|
|
110
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
111
|
+
resolve();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Testing SW Installation
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
test("verify SW install event", async ({ context, page }) => {
|
|
123
|
+
// Listen for service worker before navigating
|
|
124
|
+
const swPromise = context.waitForEvent("serviceworker");
|
|
125
|
+
|
|
126
|
+
await page.goto("/pwa-app");
|
|
127
|
+
|
|
128
|
+
const sw = await swPromise;
|
|
129
|
+
|
|
130
|
+
// Evaluate in SW context
|
|
131
|
+
const swVersion = await sw.evaluate(() => {
|
|
132
|
+
// Access SW globals
|
|
133
|
+
return (self as any).SW_VERSION || "unknown";
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(swVersion).toBe("1.0.0");
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Unregistering Service Workers
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
test.beforeEach(async ({ page }) => {
|
|
144
|
+
await page.goto("/");
|
|
145
|
+
|
|
146
|
+
// Unregister all service workers for clean state
|
|
147
|
+
await page.evaluate(async () => {
|
|
148
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
149
|
+
await Promise.all(registrations.map((r) => r.unregister()));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Clear caches
|
|
153
|
+
await page.evaluate(async () => {
|
|
154
|
+
const cacheNames = await caches.keys();
|
|
155
|
+
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Cache Testing
|
|
161
|
+
|
|
162
|
+
### Verifying Cached Resources
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
test("assets are cached", async ({ page }) => {
|
|
166
|
+
await page.goto("/pwa-app");
|
|
167
|
+
|
|
168
|
+
// Wait for SW to cache assets
|
|
169
|
+
await page.evaluate(async () => {
|
|
170
|
+
await navigator.serviceWorker.ready;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Check cache contents
|
|
174
|
+
const cachedUrls = await page.evaluate(async () => {
|
|
175
|
+
const cache = await caches.open("app-cache-v1");
|
|
176
|
+
const requests = await cache.keys();
|
|
177
|
+
return requests.map((r) => r.url);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(cachedUrls).toContain(expect.stringContaining("/styles.css"));
|
|
181
|
+
expect(cachedUrls).toContain(expect.stringContaining("/app.js"));
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Testing Cache Strategies
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
test("cache-first strategy", async ({ page }) => {
|
|
189
|
+
await page.goto("/pwa-app");
|
|
190
|
+
|
|
191
|
+
// Wait for initial cache
|
|
192
|
+
await page.waitForFunction(async () => {
|
|
193
|
+
const cache = await caches.open("app-cache-v1");
|
|
194
|
+
const keys = await cache.keys();
|
|
195
|
+
return keys.length > 0;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Block network for cached resources
|
|
199
|
+
await page.route("**/styles.css", (route) => route.abort());
|
|
200
|
+
|
|
201
|
+
// Reload - should work from cache
|
|
202
|
+
await page.reload();
|
|
203
|
+
|
|
204
|
+
// Verify page still styled (CSS loaded from cache)
|
|
205
|
+
const hasStyles = await page.evaluate(() => {
|
|
206
|
+
const body = document.body;
|
|
207
|
+
const styles = window.getComputedStyle(body);
|
|
208
|
+
return styles.fontFamily !== ""; // Has custom font from CSS
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(hasStyles).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Testing Cache Updates
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
test("cache updates on new version", async ({ page }) => {
|
|
219
|
+
await page.goto("/pwa-app");
|
|
220
|
+
|
|
221
|
+
// Get initial cache
|
|
222
|
+
const initialCacheKeys = await page.evaluate(async () => {
|
|
223
|
+
const cache = await caches.open("app-cache-v1");
|
|
224
|
+
const keys = await cache.keys();
|
|
225
|
+
return keys.map((r) => r.url);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Simulate app update by mocking SW response
|
|
229
|
+
await page.route("**/sw.js", (route) => {
|
|
230
|
+
route.fulfill({
|
|
231
|
+
contentType: "application/javascript",
|
|
232
|
+
body: `
|
|
233
|
+
const VERSION = 'v2';
|
|
234
|
+
self.addEventListener('install', (e) => {
|
|
235
|
+
e.waitUntil(caches.open('app-cache-v2'));
|
|
236
|
+
self.skipWaiting();
|
|
237
|
+
});
|
|
238
|
+
`,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Trigger update
|
|
243
|
+
await page.evaluate(async () => {
|
|
244
|
+
const reg = await navigator.serviceWorker.ready;
|
|
245
|
+
await reg.update();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Verify new cache exists
|
|
249
|
+
await page.waitForFunction(async () => {
|
|
250
|
+
return await caches.has("app-cache-v2");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Offline Testing
|
|
256
|
+
|
|
257
|
+
This section covers **offline-first apps (PWAs)** that are designed to work offline using service workers, caching, and background sync. For testing **unexpected network failures** (error recovery, graceful degradation), see [error-testing.md](error-testing.md#offline-testing).
|
|
258
|
+
|
|
259
|
+
### Simulating Offline Mode
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
test("app works offline", async ({ page, context }) => {
|
|
263
|
+
await page.goto("/pwa-app");
|
|
264
|
+
|
|
265
|
+
// Ensure SW is active and content cached
|
|
266
|
+
await page.evaluate(async () => {
|
|
267
|
+
await navigator.serviceWorker.ready;
|
|
268
|
+
});
|
|
269
|
+
await page.waitForTimeout(1000); // Allow caching to complete
|
|
270
|
+
|
|
271
|
+
// Go offline
|
|
272
|
+
await context.setOffline(true);
|
|
273
|
+
|
|
274
|
+
// Navigate to cached page
|
|
275
|
+
await page.reload();
|
|
276
|
+
|
|
277
|
+
// Verify content loads
|
|
278
|
+
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
|
279
|
+
|
|
280
|
+
// Verify offline indicator
|
|
281
|
+
await expect(page.locator(".offline-badge")).toBeVisible();
|
|
282
|
+
|
|
283
|
+
// Go back online
|
|
284
|
+
await context.setOffline(false);
|
|
285
|
+
await expect(page.locator(".offline-badge")).not.toBeVisible();
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Testing Offline Fallback
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
test("shows offline page for uncached routes", async ({ page, context }) => {
|
|
293
|
+
await page.goto("/pwa-app");
|
|
294
|
+
await page.evaluate(() => navigator.serviceWorker.ready);
|
|
295
|
+
|
|
296
|
+
// Go offline
|
|
297
|
+
await context.setOffline(true);
|
|
298
|
+
|
|
299
|
+
// Navigate to uncached page
|
|
300
|
+
await page.goto("/uncached-page");
|
|
301
|
+
|
|
302
|
+
// Should show offline fallback
|
|
303
|
+
await expect(page.getByText("You are offline")).toBeVisible();
|
|
304
|
+
await expect(page.getByRole("button", { name: "Retry" })).toBeVisible();
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Testing Offline Form Submission
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
test("queues form submission offline", async ({ page, context }) => {
|
|
312
|
+
await page.goto("/pwa-app/form");
|
|
313
|
+
|
|
314
|
+
// Go offline
|
|
315
|
+
await context.setOffline(true);
|
|
316
|
+
|
|
317
|
+
// Submit form
|
|
318
|
+
await page.getByLabel("Message").fill("Offline message");
|
|
319
|
+
await page.getByRole("button", { name: "Send" }).click();
|
|
320
|
+
|
|
321
|
+
// Should show queued status
|
|
322
|
+
await expect(page.getByText("Queued for sync")).toBeVisible();
|
|
323
|
+
|
|
324
|
+
// Go online
|
|
325
|
+
await context.setOffline(false);
|
|
326
|
+
|
|
327
|
+
// Trigger sync (or wait for automatic)
|
|
328
|
+
await page.evaluate(async () => {
|
|
329
|
+
const reg = await navigator.serviceWorker.ready;
|
|
330
|
+
// Manually trigger sync for testing
|
|
331
|
+
await (reg as any).sync?.register("form-sync");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Verify submission completed
|
|
335
|
+
await expect(page.getByText("Message sent")).toBeVisible({ timeout: 10000 });
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Push Notifications
|
|
340
|
+
|
|
341
|
+
### Mocking Push Subscription
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
test("handles push subscription", async ({ page, context }) => {
|
|
345
|
+
// Grant notification permission
|
|
346
|
+
await context.grantPermissions(["notifications"]);
|
|
347
|
+
|
|
348
|
+
await page.goto("/pwa-app");
|
|
349
|
+
|
|
350
|
+
// Subscribe to push
|
|
351
|
+
const subscription = await page.evaluate(async () => {
|
|
352
|
+
const reg = await navigator.serviceWorker.ready;
|
|
353
|
+
const sub = await reg.pushManager.subscribe({
|
|
354
|
+
userVisibleOnly: true,
|
|
355
|
+
applicationServerKey: "test-key",
|
|
356
|
+
});
|
|
357
|
+
return sub.toJSON();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(subscription.endpoint).toBeDefined();
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Testing Push Message Handling
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
test("handles push notification", async ({ context, page }) => {
|
|
368
|
+
await context.grantPermissions(["notifications"]);
|
|
369
|
+
await page.goto("/pwa-app");
|
|
370
|
+
|
|
371
|
+
// Wait for SW
|
|
372
|
+
const swPromise = context.waitForEvent("serviceworker");
|
|
373
|
+
const sw = await swPromise;
|
|
374
|
+
|
|
375
|
+
// Simulate push message to service worker
|
|
376
|
+
await sw.evaluate(async () => {
|
|
377
|
+
// Dispatch push event
|
|
378
|
+
const pushEvent = new PushEvent("push", {
|
|
379
|
+
data: new PushMessageData(
|
|
380
|
+
JSON.stringify({ title: "Test", body: "Push message" }),
|
|
381
|
+
),
|
|
382
|
+
});
|
|
383
|
+
self.dispatchEvent(pushEvent);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Note: Actual notification display testing is limited in Playwright
|
|
387
|
+
// Focus on verifying the SW handles the push correctly
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Testing Notification Click
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
test("notification click opens page", async ({ context, page }) => {
|
|
395
|
+
await context.grantPermissions(["notifications"]);
|
|
396
|
+
await page.goto("/pwa-app");
|
|
397
|
+
|
|
398
|
+
// Store notification URL target
|
|
399
|
+
let notificationUrl = "";
|
|
400
|
+
|
|
401
|
+
// Listen for new pages (notification click opens new page)
|
|
402
|
+
context.on("page", (newPage) => {
|
|
403
|
+
notificationUrl = newPage.url();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Trigger notification via SW
|
|
407
|
+
await page.evaluate(async () => {
|
|
408
|
+
const reg = await navigator.serviceWorker.ready;
|
|
409
|
+
await reg.showNotification("Test", {
|
|
410
|
+
body: "Click me",
|
|
411
|
+
data: { url: "/notification-target" },
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Simulate clicking notification (via SW)
|
|
416
|
+
const sw = context.serviceWorkers()[0];
|
|
417
|
+
await sw.evaluate(() => {
|
|
418
|
+
self.dispatchEvent(
|
|
419
|
+
new NotificationEvent("notificationclick", {
|
|
420
|
+
notification: { data: { url: "/notification-target" } } as any,
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Verify navigation occurred
|
|
426
|
+
await page.waitForTimeout(1000);
|
|
427
|
+
// Check if new page opened or current page navigated
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Background Sync
|
|
432
|
+
|
|
433
|
+
### Testing Background Sync Registration
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
test("registers background sync", async ({ page }) => {
|
|
437
|
+
await page.goto("/pwa-app");
|
|
438
|
+
|
|
439
|
+
// Register sync
|
|
440
|
+
const syncRegistered = await page.evaluate(async () => {
|
|
441
|
+
const reg = await navigator.serviceWorker.ready;
|
|
442
|
+
if (!("sync" in reg)) return false;
|
|
443
|
+
|
|
444
|
+
await (reg as any).sync.register("my-sync");
|
|
445
|
+
return true;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(syncRegistered).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Testing Sync Event
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
test("sync event fires when online", async ({ context, page }) => {
|
|
456
|
+
await page.goto("/pwa-app");
|
|
457
|
+
|
|
458
|
+
// Queue data while offline
|
|
459
|
+
await context.setOffline(true);
|
|
460
|
+
|
|
461
|
+
await page.evaluate(async () => {
|
|
462
|
+
// Store data in IndexedDB for sync
|
|
463
|
+
const db = await openDB();
|
|
464
|
+
await db.put("sync-queue", { id: 1, data: "test" });
|
|
465
|
+
|
|
466
|
+
// Register sync
|
|
467
|
+
const reg = await navigator.serviceWorker.ready;
|
|
468
|
+
await (reg as any).sync.register("data-sync");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Track sync completion
|
|
472
|
+
await page.evaluate(() => {
|
|
473
|
+
window.syncCompleted = false;
|
|
474
|
+
navigator.serviceWorker.addEventListener("message", (e) => {
|
|
475
|
+
if (e.data.type === "SYNC_COMPLETE") {
|
|
476
|
+
window.syncCompleted = true;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Go online
|
|
482
|
+
await context.setOffline(false);
|
|
483
|
+
|
|
484
|
+
// Wait for sync to complete
|
|
485
|
+
await page.waitForFunction(() => window.syncCompleted, { timeout: 10000 });
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Anti-Patterns to Avoid
|
|
490
|
+
|
|
491
|
+
| Anti-Pattern | Problem | Solution |
|
|
492
|
+
| ------------------------------ | ----------------------- | -------------------------------------------- |
|
|
493
|
+
| Not clearing SW between tests | Tests affect each other | Unregister SW in beforeEach |
|
|
494
|
+
| Not waiting for SW ready | Race conditions | Always await `navigator.serviceWorker.ready` |
|
|
495
|
+
| Testing in isolation only | Misses real SW behavior | Test with actual caching |
|
|
496
|
+
| Hardcoded timeouts for caching | Flaky tests | Wait for cache to populate |
|
|
497
|
+
| Ignoring SW update cycle | Missing update bugs | Test install, activate, update flows |
|
|
498
|
+
|
|
499
|
+
## Related References
|
|
500
|
+
|
|
501
|
+
- **Network Failures**: See [error-testing.md](error-testing.md#offline-testing) for unexpected network failure patterns
|
|
502
|
+
- **Browser APIs**: See [browser-apis.md](browser-apis.md) for permissions
|
|
503
|
+
- **Network Mocking**: See [network-advanced.md](../advanced/network-advanced.md) for network interception
|
|
504
|
+
- **Browser Extensions**: See [browser-extensions.md](../testing-patterns/browser-extensions.md) for extension service worker patterns
|