@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.
Files changed (97) hide show
  1. package/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
  2. package/.agents/skills/frontend-design/LICENSE.txt +177 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
  5. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  6. package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
  7. package/.agents/skills/playwright-best-practices/README.md +147 -0
  8. package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
  9. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
  10. package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
  11. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
  12. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
  13. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
  14. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
  15. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
  16. package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
  17. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
  18. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
  19. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
  20. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
  21. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
  22. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
  23. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
  24. package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
  25. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
  26. package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
  27. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
  28. package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
  29. package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
  30. package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
  31. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
  32. package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
  33. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
  34. package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
  35. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
  36. package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
  37. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
  38. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
  39. package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
  40. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
  41. package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
  42. package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
  43. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
  44. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
  45. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
  46. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
  47. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
  48. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
  49. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
  50. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
  51. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
  52. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
  53. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
  54. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
  55. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
  56. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
  57. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
  58. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
  59. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
  60. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
  61. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
  62. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
  63. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
  64. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
  65. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
  66. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
  67. package/.env.example +21 -0
  68. package/README.md +30 -0
  69. package/bin/arcality.mjs +86 -0
  70. package/package.json +66 -0
  71. package/playwright.config.ts +12 -0
  72. package/scripts/cleanup-qmsdev.mjs +63 -0
  73. package/scripts/discover-view.mjs +52 -0
  74. package/scripts/extract-view.mjs +64 -0
  75. package/scripts/gen-and-run.mjs +838 -0
  76. package/scripts/init.mjs +290 -0
  77. package/scripts/migrate-to-central-out.mjs +157 -0
  78. package/scripts/postinstall.mjs +63 -0
  79. package/scripts/rebrand-report.mjs +241 -0
  80. package/scripts/setup.mjs +166 -0
  81. package/src/KnowledgeService.ts +239 -0
  82. package/src/arcalityClient.mjs +266 -0
  83. package/src/configLoader.mjs +179 -0
  84. package/src/configManager.mjs +172 -0
  85. package/src/consoleBanner.ts +32 -0
  86. package/src/envSetup.ts +205 -0
  87. package/src/index.ts +25 -0
  88. package/src/projectInspector.ts +42 -0
  89. package/src/services/collectiveMemoryService.ts +178 -0
  90. package/src/testRunner.ts +201 -0
  91. package/tests/_helpers/ArcalityReporter.ts +490 -0
  92. package/tests/_helpers/agentic-runner.spec.ts +741 -0
  93. package/tests/_helpers/ai-agent-helper.ts +1573 -0
  94. package/tests/_helpers/discover-view.spec.ts +238 -0
  95. package/tests/_helpers/extract-view.spec.ts +118 -0
  96. package/tests/_helpers/qa-tools.ts +333 -0
  97. package/tests/_helpers/smart-action.spec.ts +1458 -0
@@ -0,0 +1,506 @@
1
+ # Browser Extension Testing
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [Setup & Configuration](#setup--configuration)
6
+ 2. [Loading Extensions](#loading-extensions)
7
+ 3. [Popup Testing](#popup-testing)
8
+ 4. [Background Script Testing](#background-script-testing)
9
+ 5. [Content Script Testing](#content-script-testing)
10
+ 6. [Extension APIs](#extension-apis)
11
+ 7. [Cross-Browser Testing](#cross-browser-testing)
12
+
13
+ ## Setup & Configuration
14
+
15
+ ### Prerequisites
16
+
17
+ ```bash
18
+ npm install -D @playwright/test
19
+ npx playwright install chromium # Extensions only work in Chromium
20
+ ```
21
+
22
+ ### Basic Configuration
23
+
24
+ ```typescript
25
+ // playwright.config.ts
26
+ import { defineConfig } from "@playwright/test";
27
+ import path from "path";
28
+
29
+ export default defineConfig({
30
+ testDir: "./tests",
31
+ use: {
32
+ // Extensions require non-headless Chromium
33
+ headless: false,
34
+ },
35
+ projects: [
36
+ {
37
+ name: "chromium-extension",
38
+ use: {
39
+ browserName: "chromium",
40
+ },
41
+ },
42
+ ],
43
+ });
44
+ ```
45
+
46
+ ### Extension Fixture
47
+
48
+ ```typescript
49
+ // fixtures/extension.ts
50
+ import { test as base, chromium, BrowserContext, Page } from "@playwright/test";
51
+ import path from "path";
52
+
53
+ type ExtensionFixtures = {
54
+ context: BrowserContext;
55
+ extensionId: string;
56
+ backgroundPage: Page;
57
+ };
58
+
59
+ export const test = base.extend<ExtensionFixtures>({
60
+ context: async ({}, use) => {
61
+ const pathToExtension = path.join(__dirname, "../extension");
62
+
63
+ const context = await chromium.launchPersistentContext("", {
64
+ headless: false,
65
+ args: [
66
+ `--disable-extensions-except=${pathToExtension}`,
67
+ `--load-extension=${pathToExtension}`,
68
+ ],
69
+ });
70
+
71
+ await use(context);
72
+ await context.close();
73
+ },
74
+
75
+ extensionId: async ({ context }, use) => {
76
+ // Get extension ID from service worker URL
77
+ let extensionId = "";
78
+
79
+ // Wait for service worker to be registered
80
+ const serviceWorker =
81
+ context.serviceWorkers()[0] ||
82
+ (await context.waitForEvent("serviceworker"));
83
+
84
+ extensionId = serviceWorker.url().split("/")[2];
85
+
86
+ await use(extensionId);
87
+ },
88
+
89
+ backgroundPage: async ({ context }, use) => {
90
+ // For Manifest V2 extensions
91
+ const backgroundPage =
92
+ context.backgroundPages()[0] ||
93
+ (await context.waitForEvent("backgroundpage"));
94
+
95
+ await use(backgroundPage);
96
+ },
97
+ });
98
+
99
+ export { expect } from "@playwright/test";
100
+ ```
101
+
102
+ ## Loading Extensions
103
+
104
+ ### Manifest V3 (Service Worker)
105
+
106
+ ```typescript
107
+ test("load MV3 extension", async () => {
108
+ const pathToExtension = path.join(__dirname, "../my-extension");
109
+
110
+ const context = await chromium.launchPersistentContext("", {
111
+ headless: false,
112
+ args: [
113
+ `--disable-extensions-except=${pathToExtension}`,
114
+ `--load-extension=${pathToExtension}`,
115
+ ],
116
+ });
117
+
118
+ // Wait for service worker
119
+ const serviceWorker = await context.waitForEvent("serviceworker");
120
+ expect(serviceWorker.url()).toContain("chrome-extension://");
121
+
122
+ await context.close();
123
+ });
124
+ ```
125
+
126
+ ### Manifest V2 (Background Page)
127
+
128
+ ```typescript
129
+ test("load MV2 extension", async () => {
130
+ const pathToExtension = path.join(__dirname, "../my-extension-v2");
131
+
132
+ const context = await chromium.launchPersistentContext("", {
133
+ headless: false,
134
+ args: [
135
+ `--disable-extensions-except=${pathToExtension}`,
136
+ `--load-extension=${pathToExtension}`,
137
+ ],
138
+ });
139
+
140
+ // Wait for background page
141
+ const backgroundPage = await context.waitForEvent("backgroundpage");
142
+ expect(backgroundPage.url()).toContain("chrome-extension://");
143
+
144
+ await context.close();
145
+ });
146
+ ```
147
+
148
+ ### Multiple Extensions
149
+
150
+ ```typescript
151
+ test("load multiple extensions", async () => {
152
+ const extension1 = path.join(__dirname, "../extension1");
153
+ const extension2 = path.join(__dirname, "../extension2");
154
+
155
+ const context = await chromium.launchPersistentContext("", {
156
+ headless: false,
157
+ args: [
158
+ `--disable-extensions-except=${extension1},${extension2}`,
159
+ `--load-extension=${extension1},${extension2}`,
160
+ ],
161
+ });
162
+
163
+ // Both service workers should be available
164
+ await context.waitForEvent("serviceworker");
165
+ await context.waitForEvent("serviceworker");
166
+
167
+ expect(context.serviceWorkers().length).toBe(2);
168
+
169
+ await context.close();
170
+ });
171
+ ```
172
+
173
+ ## Popup Testing
174
+
175
+ ### Opening Extension Popup
176
+
177
+ ```typescript
178
+ test("test popup UI", async ({ context, extensionId }) => {
179
+ // Open popup directly by URL
180
+ const popupPage = await context.newPage();
181
+ await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
182
+
183
+ // Test popup interactions
184
+ await expect(popupPage.getByRole("heading")).toHaveText("My Extension");
185
+ await popupPage.getByRole("button", { name: "Enable" }).click();
186
+ await expect(popupPage.getByText("Enabled")).toBeVisible();
187
+ });
188
+ ```
189
+
190
+ ### Popup State Persistence
191
+
192
+ ```typescript
193
+ test("popup remembers state", async ({ context, extensionId }) => {
194
+ // First interaction
195
+ const popup1 = await context.newPage();
196
+ await popup1.goto(`chrome-extension://${extensionId}/popup.html`);
197
+ await popup1.getByRole("checkbox", { name: "Dark Mode" }).check();
198
+ await popup1.close();
199
+
200
+ // Reopen popup
201
+ const popup2 = await context.newPage();
202
+ await popup2.goto(`chrome-extension://${extensionId}/popup.html`);
203
+
204
+ // State should persist
205
+ await expect(
206
+ popup2.getByRole("checkbox", { name: "Dark Mode" }),
207
+ ).toBeChecked();
208
+ });
209
+ ```
210
+
211
+ ### Popup Communication with Background
212
+
213
+ ```typescript
214
+ test("popup sends message to background", async ({ context, extensionId }) => {
215
+ const popup = await context.newPage();
216
+ await popup.goto(`chrome-extension://${extensionId}/popup.html`);
217
+
218
+ // Set up listener for response
219
+ const responsePromise = popup.evaluate(() => {
220
+ return new Promise((resolve) => {
221
+ chrome.runtime.onMessage.addListener((message) => {
222
+ if (message.type === "RESPONSE") resolve(message.data);
223
+ });
224
+ });
225
+ });
226
+
227
+ // Click button that sends message
228
+ await popup.getByRole("button", { name: "Fetch Data" }).click();
229
+
230
+ // Verify response
231
+ const response = await responsePromise;
232
+ expect(response).toBeDefined();
233
+ });
234
+ ```
235
+
236
+ ## Background Script Testing
237
+
238
+ ### Manifest V3 Service Worker
239
+
240
+ ```typescript
241
+ test("service worker handles messages", async ({ context, extensionId }) => {
242
+ const page = await context.newPage();
243
+ await page.goto("https://example.com");
244
+
245
+ // Send message to service worker from page
246
+ const response = await page.evaluate(async (extId) => {
247
+ return new Promise((resolve) => {
248
+ chrome.runtime.sendMessage(extId, { type: "GET_STATUS" }, resolve);
249
+ });
250
+ }, extensionId);
251
+
252
+ expect(response).toEqual({ status: "active" });
253
+ });
254
+ ```
255
+
256
+ ### Testing Background Logic
257
+
258
+ ```typescript
259
+ test("background script logic", async ({ context }) => {
260
+ const serviceWorker =
261
+ context.serviceWorkers()[0] ||
262
+ (await context.waitForEvent("serviceworker"));
263
+
264
+ // Evaluate in service worker context
265
+ const result = await serviceWorker.evaluate(async () => {
266
+ // Access extension APIs
267
+ const storage = await chrome.storage.local.get("settings");
268
+ return storage;
269
+ });
270
+
271
+ expect(result.settings).toBeDefined();
272
+ });
273
+ ```
274
+
275
+ ### Alarms and Timers
276
+
277
+ ```typescript
278
+ test("alarm triggers correctly", async ({ context }) => {
279
+ const serviceWorker = await context.waitForEvent("serviceworker");
280
+
281
+ // Create alarm
282
+ await serviceWorker.evaluate(async () => {
283
+ await chrome.alarms.create("test-alarm", { delayInMinutes: 0.01 });
284
+ });
285
+
286
+ // Wait for alarm handler
287
+ await serviceWorker.evaluate(() => {
288
+ return new Promise<void>((resolve) => {
289
+ chrome.alarms.onAlarm.addListener((alarm) => {
290
+ if (alarm.name === "test-alarm") resolve();
291
+ });
292
+ });
293
+ });
294
+
295
+ // Verify alarm was handled (check side effects)
296
+ const wasHandled = await serviceWorker.evaluate(async () => {
297
+ const { alarmTriggered } = await chrome.storage.local.get("alarmTriggered");
298
+ return alarmTriggered;
299
+ });
300
+
301
+ expect(wasHandled).toBe(true);
302
+ });
303
+ ```
304
+
305
+ ## Content Script Testing
306
+
307
+ ### Injected Content Script
308
+
309
+ ```typescript
310
+ test("content script injects UI", async ({ context }) => {
311
+ const page = await context.newPage();
312
+ await page.goto("https://example.com");
313
+
314
+ // Wait for content script to inject elements
315
+ await expect(page.locator("#my-extension-widget")).toBeVisible();
316
+
317
+ // Interact with injected UI
318
+ await page.locator("#my-extension-widget button").click();
319
+ await expect(page.locator("#my-extension-widget .result")).toHaveText(
320
+ "Success",
321
+ );
322
+ });
323
+ ```
324
+
325
+ ### Content Script Communication
326
+
327
+ ```typescript
328
+ test("content script communicates with background", async ({
329
+ context,
330
+ extensionId,
331
+ }) => {
332
+ const page = await context.newPage();
333
+ await page.goto("https://example.com");
334
+
335
+ // Trigger content script action
336
+ await page.locator("#my-extension-button").click();
337
+
338
+ // Wait for background response reflected in UI
339
+ await expect(page.locator("#my-extension-status")).toHaveText("Connected");
340
+ });
341
+ ```
342
+
343
+ ### Page Modification Testing
344
+
345
+ ```typescript
346
+ test("content script modifies page", async ({ context }) => {
347
+ const page = await context.newPage();
348
+ await page.goto("https://example.com");
349
+
350
+ // Verify content script modifications
351
+ const hasModification = await page.evaluate(() => {
352
+ // Check for injected styles
353
+ const styles = document.querySelectorAll('style[data-extension="my-ext"]');
354
+ return styles.length > 0;
355
+ });
356
+
357
+ expect(hasModification).toBe(true);
358
+
359
+ // Check DOM modifications
360
+ const modifiedElements = await page
361
+ .locator("[data-modified-by-extension]")
362
+ .count();
363
+ expect(modifiedElements).toBeGreaterThan(0);
364
+ });
365
+ ```
366
+
367
+ ## Extension APIs
368
+
369
+ ### Storage API
370
+
371
+ ```typescript
372
+ test("chrome.storage operations", async ({ context }) => {
373
+ const serviceWorker = await context.waitForEvent("serviceworker");
374
+
375
+ // Set storage
376
+ await serviceWorker.evaluate(async () => {
377
+ await chrome.storage.local.set({ key: "value", count: 42 });
378
+ });
379
+
380
+ // Get storage
381
+ const data = await serviceWorker.evaluate(async () => {
382
+ return await chrome.storage.local.get(["key", "count"]);
383
+ });
384
+
385
+ expect(data).toEqual({ key: "value", count: 42 });
386
+
387
+ // Test storage.sync
388
+ await serviceWorker.evaluate(async () => {
389
+ await chrome.storage.sync.set({ synced: true });
390
+ });
391
+
392
+ const syncData = await serviceWorker.evaluate(async () => {
393
+ return await chrome.storage.sync.get("synced");
394
+ });
395
+
396
+ expect(syncData.synced).toBe(true);
397
+ });
398
+ ```
399
+
400
+ ### Tabs API
401
+
402
+ ```typescript
403
+ test("chrome.tabs operations", async ({ context }) => {
404
+ const serviceWorker = await context.waitForEvent("serviceworker");
405
+
406
+ // Create a tab
407
+ const page = await context.newPage();
408
+ await page.goto("https://example.com");
409
+
410
+ // Query tabs from service worker
411
+ const tabs = await serviceWorker.evaluate(async () => {
412
+ return await chrome.tabs.query({ url: "*://example.com/*" });
413
+ });
414
+
415
+ expect(tabs.length).toBeGreaterThan(0);
416
+ expect(tabs[0].url).toContain("example.com");
417
+
418
+ // Send message to tab
419
+ await serviceWorker.evaluate(async (tabId) => {
420
+ await chrome.tabs.sendMessage(tabId, { type: "PING" });
421
+ }, tabs[0].id);
422
+ });
423
+ ```
424
+
425
+ ### Context Menus
426
+
427
+ ```typescript
428
+ test("context menu actions", async ({ context, extensionId }) => {
429
+ const serviceWorker = await context.waitForEvent("serviceworker");
430
+
431
+ // Create context menu
432
+ await serviceWorker.evaluate(async () => {
433
+ await chrome.contextMenus.create({
434
+ id: "test-menu",
435
+ title: "Test Action",
436
+ contexts: ["selection"],
437
+ });
438
+ });
439
+
440
+ // Simulate context menu click
441
+ const page = await context.newPage();
442
+ await page.goto("https://example.com");
443
+
444
+ // Select text
445
+ await page.evaluate(() => {
446
+ const range = document.createRange();
447
+ range.selectNodeContents(document.body.firstChild!);
448
+ window.getSelection()?.addRange(range);
449
+ });
450
+
451
+ // Trigger context menu action programmatically
452
+ await serviceWorker.evaluate(async () => {
453
+ // Simulate the click handler
454
+ chrome.contextMenus.onClicked.dispatch(
455
+ { menuItemId: "test-menu", selectionText: "selected text" },
456
+ { id: 1, url: "https://example.com" },
457
+ );
458
+ });
459
+ });
460
+ ```
461
+
462
+ ### Permissions API
463
+
464
+ ```typescript
465
+ test("request permissions", async ({ context, extensionId }) => {
466
+ const popup = await context.newPage();
467
+ await popup.goto(`chrome-extension://${extensionId}/popup.html`);
468
+
469
+ // Check current permissions
470
+ const hasPermission = await popup.evaluate(async () => {
471
+ return await chrome.permissions.contains({
472
+ origins: ["https://*.github.com/*"],
473
+ });
474
+ });
475
+
476
+ // Request new permission (will show prompt in real scenario)
477
+ // For testing, we check the request is made correctly
478
+ const permissionRequest = popup.evaluate(async () => {
479
+ try {
480
+ return await chrome.permissions.request({
481
+ origins: ["https://*.github.com/*"],
482
+ });
483
+ } catch (e) {
484
+ return false;
485
+ }
486
+ });
487
+
488
+ // In automated tests, permission prompts are typically auto-granted or mocked
489
+ });
490
+ ```
491
+
492
+ ## Anti-Patterns to Avoid
493
+
494
+ | Anti-Pattern | Problem | Solution |
495
+ | ------------------------------ | --------------------- | ---------------------------------------- |
496
+ | Testing in headless mode | Extensions don't load | Use `headless: false` |
497
+ | Not waiting for service worker | Race conditions | Wait for `serviceworker` event |
498
+ | Hardcoding extension ID | ID changes on reload | Extract ID from service worker URL |
499
+ | Testing packed extensions only | Slow iteration | Test unpacked during development |
500
+ | Ignoring MV3 differences | Breaking changes | Test both MV2 and MV3 if supporting both |
501
+
502
+ ## Related References
503
+
504
+ - **Service Workers**: See [service-workers.md](../browser-apis/service-workers.md) for SW testing patterns
505
+ - **Multi-Context**: See [multi-context.md](../advanced/multi-context.md) for popup handling
506
+ - **Browser APIs**: See [browser-apis.md](../browser-apis/browser-apis.md) for permissions testing