@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,391 @@
|
|
|
1
|
+
# Browser APIs: Geolocation, Permissions & More
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Geolocation](#geolocation)
|
|
6
|
+
2. [Permissions](#permissions)
|
|
7
|
+
3. [Clipboard](#clipboard)
|
|
8
|
+
4. [Notifications](#notifications)
|
|
9
|
+
5. [Camera & Microphone](#camera--microphone)
|
|
10
|
+
|
|
11
|
+
## Geolocation
|
|
12
|
+
|
|
13
|
+
### Mock Location
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
test("shows nearby stores", async ({ context }) => {
|
|
17
|
+
// Grant permission and set location
|
|
18
|
+
await context.grantPermissions(["geolocation"]);
|
|
19
|
+
await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); // San Francisco
|
|
20
|
+
|
|
21
|
+
const page = await context.newPage();
|
|
22
|
+
await page.goto("/store-finder");
|
|
23
|
+
await page.getByRole("button", { name: "Find Nearby" }).click();
|
|
24
|
+
|
|
25
|
+
await expect(page.getByText("San Francisco")).toBeVisible();
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Geolocation Fixture
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// fixtures/geolocation.fixture.ts
|
|
33
|
+
import { test as base } from "@playwright/test";
|
|
34
|
+
|
|
35
|
+
type Coordinates = { latitude: number; longitude: number; accuracy?: number };
|
|
36
|
+
|
|
37
|
+
type GeoFixtures = {
|
|
38
|
+
setLocation: (coords: Coordinates) => Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const test = base.extend<GeoFixtures>({
|
|
42
|
+
setLocation: async ({ context }, use) => {
|
|
43
|
+
await context.grantPermissions(["geolocation"]);
|
|
44
|
+
|
|
45
|
+
await use(async (coords) => {
|
|
46
|
+
await context.setGeolocation({
|
|
47
|
+
latitude: coords.latitude,
|
|
48
|
+
longitude: coords.longitude,
|
|
49
|
+
accuracy: coords.accuracy ?? 100,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Usage
|
|
56
|
+
test("delivery zone check", async ({ page, setLocation }) => {
|
|
57
|
+
await setLocation({ latitude: 40.7128, longitude: -74.006 }); // NYC
|
|
58
|
+
|
|
59
|
+
await page.goto("/delivery");
|
|
60
|
+
|
|
61
|
+
await expect(page.getByText("Delivery available")).toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Test Location Changes
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
test("tracks location updates", async ({ context }) => {
|
|
69
|
+
await context.grantPermissions(["geolocation"]);
|
|
70
|
+
|
|
71
|
+
const page = await context.newPage();
|
|
72
|
+
await page.goto("/tracking");
|
|
73
|
+
|
|
74
|
+
// Initial location
|
|
75
|
+
await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
|
|
76
|
+
await page.getByRole("button", { name: "Start Tracking" }).click();
|
|
77
|
+
|
|
78
|
+
await expect(page.getByTestId("location")).toContainText("37.7749");
|
|
79
|
+
|
|
80
|
+
// Move to new location
|
|
81
|
+
await context.setGeolocation({ latitude: 37.8044, longitude: -122.2712 });
|
|
82
|
+
|
|
83
|
+
// Trigger location update
|
|
84
|
+
await page.evaluate(() => {
|
|
85
|
+
navigator.geolocation.getCurrentPosition(() => {});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await expect(page.getByTestId("location")).toContainText("37.8044");
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Test Geolocation Denial
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
test("handles location denied", async ({ browser }) => {
|
|
96
|
+
// Create context without geolocation permission
|
|
97
|
+
const context = await browser.newContext({
|
|
98
|
+
permissions: [], // No permissions
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const page = await context.newPage();
|
|
102
|
+
await page.goto("/store-finder");
|
|
103
|
+
await page.getByRole("button", { name: "Find Nearby" }).click();
|
|
104
|
+
|
|
105
|
+
await expect(page.getByText("Location access denied")).toBeVisible();
|
|
106
|
+
await expect(page.getByLabel("Enter ZIP code")).toBeVisible();
|
|
107
|
+
|
|
108
|
+
await context.close();
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Permissions
|
|
113
|
+
|
|
114
|
+
### Grant Permissions
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
test("notifications with permission", async ({ context }) => {
|
|
118
|
+
await context.grantPermissions(["notifications"]);
|
|
119
|
+
|
|
120
|
+
const page = await context.newPage();
|
|
121
|
+
await page.goto("/alerts");
|
|
122
|
+
|
|
123
|
+
// Notification API should work
|
|
124
|
+
const permission = await page.evaluate(() => Notification.permission);
|
|
125
|
+
expect(permission).toBe("granted");
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Test Permission Denied
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
test("handles notification permission denied", async ({ browser }) => {
|
|
133
|
+
const context = await browser.newContext({
|
|
134
|
+
permissions: [], // Deny all
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const page = await context.newPage();
|
|
138
|
+
await page.goto("/notifications");
|
|
139
|
+
|
|
140
|
+
await page.getByRole("button", { name: "Enable Notifications" }).click();
|
|
141
|
+
|
|
142
|
+
await expect(page.getByText("Please enable notifications")).toBeVisible();
|
|
143
|
+
|
|
144
|
+
await context.close();
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Multiple Permissions
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
test("video call with permissions", async ({ context }) => {
|
|
152
|
+
await context.grantPermissions(["camera", "microphone", "notifications"]);
|
|
153
|
+
|
|
154
|
+
const page = await context.newPage();
|
|
155
|
+
await page.goto("/video-call");
|
|
156
|
+
|
|
157
|
+
// All permissions should be granted
|
|
158
|
+
const permissions = await page.evaluate(async () => ({
|
|
159
|
+
camera: await navigator.permissions.query({
|
|
160
|
+
name: "camera" as PermissionName,
|
|
161
|
+
}),
|
|
162
|
+
microphone: await navigator.permissions.query({
|
|
163
|
+
name: "microphone" as PermissionName,
|
|
164
|
+
}),
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
expect(permissions.camera.state).toBe("granted");
|
|
168
|
+
expect(permissions.microphone.state).toBe("granted");
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Clipboard
|
|
173
|
+
|
|
174
|
+
### Test Copy to Clipboard
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
test("copy button works", async ({ page, context }) => {
|
|
178
|
+
// Grant clipboard permissions
|
|
179
|
+
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
180
|
+
|
|
181
|
+
await page.goto("/share");
|
|
182
|
+
|
|
183
|
+
await page.getByRole("button", { name: "Copy Link" }).click();
|
|
184
|
+
|
|
185
|
+
// Read clipboard content
|
|
186
|
+
const clipboardContent = await page.evaluate(() =>
|
|
187
|
+
navigator.clipboard.readText(),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(clipboardContent).toContain("https://example.com/share/");
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Test Paste from Clipboard
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
test("paste from clipboard", async ({ page, context }) => {
|
|
198
|
+
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
199
|
+
|
|
200
|
+
await page.goto("/editor");
|
|
201
|
+
|
|
202
|
+
// Write to clipboard
|
|
203
|
+
await page.evaluate(() => navigator.clipboard.writeText("Pasted content"));
|
|
204
|
+
|
|
205
|
+
// Trigger paste
|
|
206
|
+
await page.getByLabel("Content").focus();
|
|
207
|
+
await page.keyboard.press("Control+V");
|
|
208
|
+
|
|
209
|
+
await expect(page.getByLabel("Content")).toHaveValue("Pasted content");
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Clipboard Fixture
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// fixtures/clipboard.fixture.ts
|
|
217
|
+
import { test as base } from "@playwright/test";
|
|
218
|
+
|
|
219
|
+
type ClipboardFixtures = {
|
|
220
|
+
clipboard: {
|
|
221
|
+
write: (text: string) => Promise<void>;
|
|
222
|
+
read: () => Promise<string>;
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const test = base.extend<ClipboardFixtures>({
|
|
227
|
+
clipboard: async ({ page, context }, use) => {
|
|
228
|
+
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
229
|
+
|
|
230
|
+
await use({
|
|
231
|
+
write: async (text) => {
|
|
232
|
+
await page.evaluate((t) => navigator.clipboard.writeText(t), text);
|
|
233
|
+
},
|
|
234
|
+
read: async () => {
|
|
235
|
+
return page.evaluate(() => navigator.clipboard.readText());
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Notifications
|
|
243
|
+
|
|
244
|
+
### Mock Notification API
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
test("shows browser notification", async ({ page }) => {
|
|
248
|
+
const notifications: any[] = [];
|
|
249
|
+
|
|
250
|
+
// Mock Notification constructor
|
|
251
|
+
await page.addInitScript(() => {
|
|
252
|
+
(window as any).__notifications = [];
|
|
253
|
+
(window as any).Notification = class {
|
|
254
|
+
constructor(title: string, options?: NotificationOptions) {
|
|
255
|
+
(window as any).__notifications.push({ title, ...options });
|
|
256
|
+
}
|
|
257
|
+
static permission = "granted";
|
|
258
|
+
static requestPermission = async () => "granted";
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await page.goto("/alerts");
|
|
263
|
+
await page.getByRole("button", { name: "Notify Me" }).click();
|
|
264
|
+
|
|
265
|
+
// Check notification was created
|
|
266
|
+
const created = await page.evaluate(() => (window as any).__notifications);
|
|
267
|
+
expect(created).toHaveLength(1);
|
|
268
|
+
expect(created[0].title).toBe("New Alert");
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Test Notification Click
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
test("notification click handler", async ({ page }) => {
|
|
276
|
+
await page.addInitScript(() => {
|
|
277
|
+
(window as any).Notification = class {
|
|
278
|
+
onclick: (() => void) | null = null;
|
|
279
|
+
constructor(title: string) {
|
|
280
|
+
// Simulate click after creation
|
|
281
|
+
setTimeout(() => this.onclick?.(), 100);
|
|
282
|
+
}
|
|
283
|
+
static permission = "granted";
|
|
284
|
+
static requestPermission = async () => "granted";
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await page.goto("/messages");
|
|
289
|
+
await page.evaluate(() => {
|
|
290
|
+
new Notification("New Message");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Should navigate to messages when notification clicked
|
|
294
|
+
await expect(page).toHaveURL(/\/messages/);
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Camera & Microphone
|
|
299
|
+
|
|
300
|
+
### Mock Media Devices
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
test("video preview works", async ({ page, context }) => {
|
|
304
|
+
await context.grantPermissions(["camera"]);
|
|
305
|
+
|
|
306
|
+
// Mock getUserMedia
|
|
307
|
+
await page.addInitScript(() => {
|
|
308
|
+
navigator.mediaDevices.getUserMedia = async () => {
|
|
309
|
+
const canvas = document.createElement("canvas");
|
|
310
|
+
canvas.width = 640;
|
|
311
|
+
canvas.height = 480;
|
|
312
|
+
return canvas.captureStream();
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await page.goto("/video-settings");
|
|
317
|
+
await page.getByRole("button", { name: "Start Camera" }).click();
|
|
318
|
+
|
|
319
|
+
await expect(page.getByTestId("video-preview")).toBeVisible();
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Test Media Device Selection
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
test("switch camera", async ({ page }) => {
|
|
327
|
+
await page.addInitScript(() => {
|
|
328
|
+
navigator.mediaDevices.enumerateDevices = async () =>
|
|
329
|
+
[
|
|
330
|
+
{
|
|
331
|
+
deviceId: "cam1",
|
|
332
|
+
kind: "videoinput",
|
|
333
|
+
label: "Front Camera",
|
|
334
|
+
groupId: "1",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
deviceId: "cam2",
|
|
338
|
+
kind: "videoinput",
|
|
339
|
+
label: "Back Camera",
|
|
340
|
+
groupId: "2",
|
|
341
|
+
},
|
|
342
|
+
] as MediaDeviceInfo[];
|
|
343
|
+
|
|
344
|
+
navigator.mediaDevices.getUserMedia = async () => {
|
|
345
|
+
const canvas = document.createElement("canvas");
|
|
346
|
+
return canvas.captureStream();
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await page.goto("/camera");
|
|
351
|
+
|
|
352
|
+
// Should show camera options
|
|
353
|
+
await expect(page.getByRole("combobox", { name: "Camera" })).toBeVisible();
|
|
354
|
+
await expect(page.getByText("Front Camera")).toBeVisible();
|
|
355
|
+
await expect(page.getByText("Back Camera")).toBeVisible();
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Test Media Errors
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
test("handles camera access error", async ({ page }) => {
|
|
363
|
+
await page.addInitScript(() => {
|
|
364
|
+
navigator.mediaDevices.getUserMedia = async () => {
|
|
365
|
+
throw new DOMException("Permission denied", "NotAllowedError");
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await page.goto("/video-call");
|
|
370
|
+
await page.getByRole("button", { name: "Join Call" }).click();
|
|
371
|
+
|
|
372
|
+
await expect(page.getByText("Camera access denied")).toBeVisible();
|
|
373
|
+
await expect(
|
|
374
|
+
page.getByRole("button", { name: "Join Audio Only" }),
|
|
375
|
+
).toBeVisible();
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Anti-Patterns to Avoid
|
|
380
|
+
|
|
381
|
+
| Anti-Pattern | Problem | Solution |
|
|
382
|
+
| ----------------------------- | --------------------------------- | ----------------------------------- |
|
|
383
|
+
| Not granting permissions | Tests fail with permission errors | Use `context.grantPermissions()` |
|
|
384
|
+
| Testing real geolocation | Flaky, environment-dependent | Mock with `setGeolocation()` |
|
|
385
|
+
| Not testing permission denial | Misses error handling | Test both granted and denied states |
|
|
386
|
+
| Using real camera/mic | CI has no devices | Mock `getUserMedia` |
|
|
387
|
+
|
|
388
|
+
## Related References
|
|
389
|
+
|
|
390
|
+
- **Fixtures**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for context fixtures
|
|
391
|
+
- **Mobile**: See [mobile-testing.md](../advanced/mobile-testing.md) for device emulation
|