@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,393 @@
|
|
|
1
|
+
# Multi-User & Collaboration Testing
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Multiple Browser Contexts](#multiple-browser-contexts)
|
|
6
|
+
2. [Real-Time Collaboration](#real-time-collaboration)
|
|
7
|
+
3. [Role-Based Testing](#role-based-testing)
|
|
8
|
+
4. [Concurrent Actions](#concurrent-actions)
|
|
9
|
+
5. [Chat & Messaging](#chat--messaging)
|
|
10
|
+
|
|
11
|
+
## Multiple Browser Contexts
|
|
12
|
+
|
|
13
|
+
### Two Users in Same Test
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
test("two users see each other's changes", async ({ browser }) => {
|
|
17
|
+
// Create two isolated contexts (like two browsers)
|
|
18
|
+
const userAContext = await browser.newContext();
|
|
19
|
+
const userBContext = await browser.newContext();
|
|
20
|
+
|
|
21
|
+
const userAPage = await userAContext.newPage();
|
|
22
|
+
const userBPage = await userBContext.newPage();
|
|
23
|
+
|
|
24
|
+
// Both users go to the same document
|
|
25
|
+
await userAPage.goto("/doc/shared-123");
|
|
26
|
+
await userBPage.goto("/doc/shared-123");
|
|
27
|
+
|
|
28
|
+
// User A types
|
|
29
|
+
await userAPage.getByLabel("Content").fill("Hello from User A");
|
|
30
|
+
|
|
31
|
+
// User B should see the change
|
|
32
|
+
await expect(userBPage.getByText("Hello from User A")).toBeVisible();
|
|
33
|
+
|
|
34
|
+
// Cleanup
|
|
35
|
+
await userAContext.close();
|
|
36
|
+
await userBContext.close();
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Multiple Users with Auth States
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
test("admin and user interaction", async ({ browser }) => {
|
|
44
|
+
// Load different auth states
|
|
45
|
+
const adminContext = await browser.newContext({
|
|
46
|
+
storageState: ".auth/admin.json",
|
|
47
|
+
});
|
|
48
|
+
const userContext = await browser.newContext({
|
|
49
|
+
storageState: ".auth/user.json",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const adminPage = await adminContext.newPage();
|
|
53
|
+
const userPage = await userContext.newPage();
|
|
54
|
+
|
|
55
|
+
// User submits request
|
|
56
|
+
await userPage.goto("/support");
|
|
57
|
+
await userPage.getByLabel("Message").fill("Need help!");
|
|
58
|
+
await userPage.getByRole("button", { name: "Submit" }).click();
|
|
59
|
+
|
|
60
|
+
// Admin sees and responds
|
|
61
|
+
await adminPage.goto("/admin/tickets");
|
|
62
|
+
await expect(adminPage.getByText("Need help!")).toBeVisible();
|
|
63
|
+
await adminPage.getByRole("button", { name: "Reply" }).click();
|
|
64
|
+
await adminPage.getByLabel("Response").fill("How can I help?");
|
|
65
|
+
await adminPage.getByRole("button", { name: "Send" }).click();
|
|
66
|
+
|
|
67
|
+
// User sees response
|
|
68
|
+
await expect(userPage.getByText("How can I help?")).toBeVisible();
|
|
69
|
+
|
|
70
|
+
await adminContext.close();
|
|
71
|
+
await userContext.close();
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Multi-User Fixture
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// fixtures/multi-user.fixture.ts
|
|
79
|
+
import { test as base, Browser, BrowserContext, Page } from "@playwright/test";
|
|
80
|
+
|
|
81
|
+
type UserSession = {
|
|
82
|
+
context: BrowserContext;
|
|
83
|
+
page: Page;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type MultiUserFixtures = {
|
|
87
|
+
createUser: (authState?: string) => Promise<UserSession>;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const test = base.extend<MultiUserFixtures>({
|
|
91
|
+
createUser: async ({ browser }, use) => {
|
|
92
|
+
const sessions: UserSession[] = [];
|
|
93
|
+
|
|
94
|
+
await use(async (authState) => {
|
|
95
|
+
const context = await browser.newContext({
|
|
96
|
+
storageState: authState,
|
|
97
|
+
});
|
|
98
|
+
const page = await context.newPage();
|
|
99
|
+
sessions.push({ context, page });
|
|
100
|
+
return { context, page };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Cleanup all sessions
|
|
104
|
+
for (const session of sessions) {
|
|
105
|
+
await session.context.close();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Usage
|
|
111
|
+
test("3 users collaborate", async ({ createUser }) => {
|
|
112
|
+
const alice = await createUser(".auth/alice.json");
|
|
113
|
+
const bob = await createUser(".auth/bob.json");
|
|
114
|
+
const charlie = await createUser(".auth/charlie.json");
|
|
115
|
+
|
|
116
|
+
// All navigate to same room
|
|
117
|
+
await alice.page.goto("/room/123");
|
|
118
|
+
await bob.page.goto("/room/123");
|
|
119
|
+
await charlie.page.goto("/room/123");
|
|
120
|
+
|
|
121
|
+
// Test interactions...
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Real-Time Collaboration
|
|
126
|
+
|
|
127
|
+
### Collaborative Document
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
test("real-time collaborative editing", async ({ browser }) => {
|
|
131
|
+
const user1 = await browser.newContext();
|
|
132
|
+
const user2 = await browser.newContext();
|
|
133
|
+
|
|
134
|
+
const page1 = await user1.newPage();
|
|
135
|
+
const page2 = await user2.newPage();
|
|
136
|
+
|
|
137
|
+
await page1.goto("/docs/shared");
|
|
138
|
+
await page2.goto("/docs/shared");
|
|
139
|
+
|
|
140
|
+
// User 1 types at the beginning
|
|
141
|
+
const editor1 = page1.getByRole("textbox");
|
|
142
|
+
await editor1.click();
|
|
143
|
+
await editor1.press("Home");
|
|
144
|
+
await editor1.type("User 1: ");
|
|
145
|
+
|
|
146
|
+
// User 2 types at the end
|
|
147
|
+
const editor2 = page2.getByRole("textbox");
|
|
148
|
+
await editor2.click();
|
|
149
|
+
await editor2.press("End");
|
|
150
|
+
await editor2.type(" - User 2");
|
|
151
|
+
|
|
152
|
+
// Both should see combined result
|
|
153
|
+
await expect(page1.getByRole("textbox")).toContainText("User 1:");
|
|
154
|
+
await expect(page1.getByRole("textbox")).toContainText("- User 2");
|
|
155
|
+
await expect(page2.getByRole("textbox")).toContainText("User 1:");
|
|
156
|
+
await expect(page2.getByRole("textbox")).toContainText("- User 2");
|
|
157
|
+
|
|
158
|
+
await user1.close();
|
|
159
|
+
await user2.close();
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Cursor Presence
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
test("shows other user cursors", async ({ browser }) => {
|
|
167
|
+
const ctx1 = await browser.newContext();
|
|
168
|
+
const ctx2 = await browser.newContext();
|
|
169
|
+
|
|
170
|
+
const page1 = await ctx1.newPage();
|
|
171
|
+
const page2 = await ctx2.newPage();
|
|
172
|
+
|
|
173
|
+
// Mock to identify users
|
|
174
|
+
await page1.route("**/api/me", (route) =>
|
|
175
|
+
route.fulfill({ json: { id: "user-1", name: "Alice" } }),
|
|
176
|
+
);
|
|
177
|
+
await page2.route("**/api/me", (route) =>
|
|
178
|
+
route.fulfill({ json: { id: "user-2", name: "Bob" } }),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await page1.goto("/whiteboard/123");
|
|
182
|
+
await page2.goto("/whiteboard/123");
|
|
183
|
+
|
|
184
|
+
// Move cursor on page1
|
|
185
|
+
await page1.mouse.move(200, 200);
|
|
186
|
+
|
|
187
|
+
// Page2 should see Alice's cursor
|
|
188
|
+
await expect(page2.getByTestId("cursor-user-1")).toBeVisible();
|
|
189
|
+
await expect(page2.getByText("Alice")).toBeVisible();
|
|
190
|
+
|
|
191
|
+
await ctx1.close();
|
|
192
|
+
await ctx2.close();
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Role-Based Testing
|
|
197
|
+
|
|
198
|
+
### Test RBAC
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const roles = [
|
|
202
|
+
{ role: "admin", canDelete: true, canEdit: true, canView: true },
|
|
203
|
+
{ role: "editor", canDelete: false, canEdit: true, canView: true },
|
|
204
|
+
{ role: "viewer", canDelete: false, canEdit: false, canView: true },
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (const { role, canDelete, canEdit, canView } of roles) {
|
|
208
|
+
test(`${role} permissions`, async ({ browser }) => {
|
|
209
|
+
const context = await browser.newContext({
|
|
210
|
+
storageState: `.auth/${role}.json`,
|
|
211
|
+
});
|
|
212
|
+
const page = await context.newPage();
|
|
213
|
+
|
|
214
|
+
await page.goto("/document/123");
|
|
215
|
+
|
|
216
|
+
// Check view permission
|
|
217
|
+
if (canView) {
|
|
218
|
+
await expect(page.getByTestId("content")).toBeVisible();
|
|
219
|
+
} else {
|
|
220
|
+
await expect(page.getByText("Access denied")).toBeVisible();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check edit permission
|
|
224
|
+
const editButton = page.getByRole("button", { name: "Edit" });
|
|
225
|
+
if (canEdit) {
|
|
226
|
+
await expect(editButton).toBeEnabled();
|
|
227
|
+
} else {
|
|
228
|
+
await expect(editButton).toBeDisabled();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check delete permission
|
|
232
|
+
const deleteButton = page.getByRole("button", { name: "Delete" });
|
|
233
|
+
if (canDelete) {
|
|
234
|
+
await expect(deleteButton).toBeVisible();
|
|
235
|
+
} else {
|
|
236
|
+
await expect(deleteButton).toBeHidden();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await context.close();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Permission Escalation Test
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
test("cannot access admin routes as user", async ({ browser }) => {
|
|
248
|
+
const userContext = await browser.newContext({
|
|
249
|
+
storageState: ".auth/user.json",
|
|
250
|
+
});
|
|
251
|
+
const page = await userContext.newPage();
|
|
252
|
+
|
|
253
|
+
// Try to access admin page directly
|
|
254
|
+
await page.goto("/admin/users");
|
|
255
|
+
|
|
256
|
+
// Should redirect or show error
|
|
257
|
+
await expect(page).not.toHaveURL("/admin/users");
|
|
258
|
+
await expect(page.getByText("Access denied")).toBeVisible();
|
|
259
|
+
|
|
260
|
+
await userContext.close();
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Concurrent Actions
|
|
265
|
+
|
|
266
|
+
### Race Condition Testing
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
test("handles concurrent edits", async ({ browser }) => {
|
|
270
|
+
const ctx1 = await browser.newContext();
|
|
271
|
+
const ctx2 = await browser.newContext();
|
|
272
|
+
|
|
273
|
+
const page1 = await ctx1.newPage();
|
|
274
|
+
const page2 = await ctx2.newPage();
|
|
275
|
+
|
|
276
|
+
await page1.goto("/item/123");
|
|
277
|
+
await page2.goto("/item/123");
|
|
278
|
+
|
|
279
|
+
// Both click edit at the same time
|
|
280
|
+
await Promise.all([
|
|
281
|
+
page1.getByRole("button", { name: "Edit" }).click(),
|
|
282
|
+
page2.getByRole("button", { name: "Edit" }).click(),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
// Both try to save different values
|
|
286
|
+
await page1.getByLabel("Name").fill("Value from User 1");
|
|
287
|
+
await page2.getByLabel("Name").fill("Value from User 2");
|
|
288
|
+
|
|
289
|
+
await Promise.all([
|
|
290
|
+
page1.getByRole("button", { name: "Save" }).click(),
|
|
291
|
+
page2.getByRole("button", { name: "Save" }).click(),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
// One should succeed, one should get conflict error
|
|
295
|
+
const page1HasConflict = await page1.getByText("Conflict").isVisible();
|
|
296
|
+
const page2HasConflict = await page2.getByText("Conflict").isVisible();
|
|
297
|
+
|
|
298
|
+
// Exactly one should have conflict
|
|
299
|
+
expect(page1HasConflict || page2HasConflict).toBe(true);
|
|
300
|
+
expect(page1HasConflict && page2HasConflict).toBe(false);
|
|
301
|
+
|
|
302
|
+
await ctx1.close();
|
|
303
|
+
await ctx2.close();
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Optimistic Locking Test
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
test("optimistic locking prevents overwrites", async ({ browser }) => {
|
|
311
|
+
const ctx1 = await browser.newContext();
|
|
312
|
+
const ctx2 = await browser.newContext();
|
|
313
|
+
|
|
314
|
+
const page1 = await ctx1.newPage();
|
|
315
|
+
const page2 = await ctx2.newPage();
|
|
316
|
+
|
|
317
|
+
// Both load the same version
|
|
318
|
+
await page1.goto("/record/123");
|
|
319
|
+
await page2.goto("/record/123");
|
|
320
|
+
|
|
321
|
+
// User 1 edits and saves first
|
|
322
|
+
await page1.getByRole("button", { name: "Edit" }).click();
|
|
323
|
+
await page1.getByLabel("Value").fill("Updated by User 1");
|
|
324
|
+
await page1.getByRole("button", { name: "Save" }).click();
|
|
325
|
+
await expect(page1.getByText("Saved")).toBeVisible();
|
|
326
|
+
|
|
327
|
+
// User 2 tries to save with stale version
|
|
328
|
+
await page2.getByRole("button", { name: "Edit" }).click();
|
|
329
|
+
await page2.getByLabel("Value").fill("Updated by User 2");
|
|
330
|
+
await page2.getByRole("button", { name: "Save" }).click();
|
|
331
|
+
|
|
332
|
+
// Should fail with version conflict
|
|
333
|
+
await expect(page2.getByText("Someone else modified this")).toBeVisible();
|
|
334
|
+
await expect(page2.getByRole("button", { name: "Reload" })).toBeVisible();
|
|
335
|
+
|
|
336
|
+
await ctx1.close();
|
|
337
|
+
await ctx2.close();
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Chat & Messaging
|
|
342
|
+
|
|
343
|
+
### Real-Time Chat
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
test("chat messages sync between users", async ({ browser }) => {
|
|
347
|
+
const aliceCtx = await browser.newContext();
|
|
348
|
+
const bobCtx = await browser.newContext();
|
|
349
|
+
|
|
350
|
+
const alicePage = await aliceCtx.newPage();
|
|
351
|
+
const bobPage = await bobCtx.newPage();
|
|
352
|
+
|
|
353
|
+
// Setup user identities
|
|
354
|
+
await alicePage.route("**/api/me", (r) =>
|
|
355
|
+
r.fulfill({ json: { name: "Alice" } }),
|
|
356
|
+
);
|
|
357
|
+
await bobPage.route("**/api/me", (r) => r.fulfill({ json: { name: "Bob" } }));
|
|
358
|
+
|
|
359
|
+
await alicePage.goto("/chat/room-1");
|
|
360
|
+
await bobPage.goto("/chat/room-1");
|
|
361
|
+
|
|
362
|
+
// Alice sends message
|
|
363
|
+
await alicePage.getByLabel("Message").fill("Hi Bob!");
|
|
364
|
+
await alicePage.getByRole("button", { name: "Send" }).click();
|
|
365
|
+
|
|
366
|
+
// Bob sees it
|
|
367
|
+
await expect(bobPage.getByText("Alice: Hi Bob!")).toBeVisible();
|
|
368
|
+
|
|
369
|
+
// Bob replies
|
|
370
|
+
await bobPage.getByLabel("Message").fill("Hey Alice!");
|
|
371
|
+
await bobPage.getByRole("button", { name: "Send" }).click();
|
|
372
|
+
|
|
373
|
+
// Alice sees it
|
|
374
|
+
await expect(alicePage.getByText("Bob: Hey Alice!")).toBeVisible();
|
|
375
|
+
|
|
376
|
+
await aliceCtx.close();
|
|
377
|
+
await bobCtx.close();
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Anti-Patterns to Avoid
|
|
382
|
+
|
|
383
|
+
| Anti-Pattern | Problem | Solution |
|
|
384
|
+
| ----------------------------- | ----------------------------- | ---------------------------- |
|
|
385
|
+
| Sharing context between users | State leaks, not isolated | Create separate contexts |
|
|
386
|
+
| Not closing contexts | Memory leak, browser overload | Always close in cleanup |
|
|
387
|
+
| Hardcoded timing for sync | Flaky tests | Use `expect().toBeVisible()` |
|
|
388
|
+
| Testing only single user | Misses collaboration bugs | Test multi-user scenarios |
|
|
389
|
+
|
|
390
|
+
## Related References
|
|
391
|
+
|
|
392
|
+
- **Authentication**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for auth setup
|
|
393
|
+
- **WebSockets**: See [websockets.md](../browser-apis/websockets.md) for real-time mocking
|