@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,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
|