@assertrx/dino 1.0.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/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Multi-User and Collaboration Testing
|
|
2
|
+
|
|
3
|
+
> **When to use**: When your application involves real-time collaboration, multi-user workflows, or any scenario where two or more users interact with the same resource simultaneously -- chat apps, shared documents, multiplayer features, admin-and-user flows.
|
|
4
|
+
> **Prerequisites**: [core/fixtures-and-hooks.md](fixtures-and-hooks.md), [core/configuration.md](configuration.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Two independent browser contexts in one test = two users
|
|
10
|
+
const alice = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
11
|
+
const bob = await browser.newContext({ storageState: 'auth/bob.json' });
|
|
12
|
+
const alicePage = await alice.newPage();
|
|
13
|
+
const bobPage = await bob.newPage();
|
|
14
|
+
|
|
15
|
+
// Each operates independently — different cookies, sessions, localStorage
|
|
16
|
+
await alicePage.goto('/chat/room-1');
|
|
17
|
+
await bobPage.goto('/chat/room-1');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Patterns
|
|
21
|
+
|
|
22
|
+
### Two Users in One Test via Browser Contexts
|
|
23
|
+
|
|
24
|
+
**Use when**: You need to verify that actions by one user are visible to another in real time.
|
|
25
|
+
**Avoid when**: You only need to test a single user's flow. One context per test is the default.
|
|
26
|
+
|
|
27
|
+
Each `browser.newContext()` creates a fully isolated session -- separate cookies, localStorage, and network state. This is how you simulate two logged-in users in a single test without launching a second browser.
|
|
28
|
+
|
|
29
|
+
**TypeScript**
|
|
30
|
+
```typescript
|
|
31
|
+
import { test, expect } from '@playwright/test';
|
|
32
|
+
|
|
33
|
+
test('alice sends a message and bob sees it', async ({ browser }) => {
|
|
34
|
+
// Create two isolated contexts
|
|
35
|
+
const aliceContext = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
36
|
+
const bobContext = await browser.newContext({ storageState: 'auth/bob.json' });
|
|
37
|
+
|
|
38
|
+
const alicePage = await aliceContext.newPage();
|
|
39
|
+
const bobPage = await bobContext.newPage();
|
|
40
|
+
|
|
41
|
+
// Both navigate to the same chat room
|
|
42
|
+
await alicePage.goto('/chat/general');
|
|
43
|
+
await bobPage.goto('/chat/general');
|
|
44
|
+
|
|
45
|
+
// Alice sends a message
|
|
46
|
+
await alicePage.getByRole('textbox', { name: 'Message' }).fill('Hello Bob!');
|
|
47
|
+
await alicePage.getByRole('button', { name: 'Send' }).click();
|
|
48
|
+
|
|
49
|
+
// Bob sees it in real time
|
|
50
|
+
await expect(bobPage.getByText('Hello Bob!')).toBeVisible();
|
|
51
|
+
|
|
52
|
+
// Alice also sees her own message
|
|
53
|
+
await expect(alicePage.getByText('Hello Bob!')).toBeVisible();
|
|
54
|
+
|
|
55
|
+
// Cleanup
|
|
56
|
+
await aliceContext.close();
|
|
57
|
+
await bobContext.close();
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**JavaScript**
|
|
62
|
+
```javascript
|
|
63
|
+
const { test, expect } = require('@playwright/test');
|
|
64
|
+
|
|
65
|
+
test('alice sends a message and bob sees it', async ({ browser }) => {
|
|
66
|
+
const aliceContext = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
67
|
+
const bobContext = await browser.newContext({ storageState: 'auth/bob.json' });
|
|
68
|
+
|
|
69
|
+
const alicePage = await aliceContext.newPage();
|
|
70
|
+
const bobPage = await bobContext.newPage();
|
|
71
|
+
|
|
72
|
+
await alicePage.goto('/chat/general');
|
|
73
|
+
await bobPage.goto('/chat/general');
|
|
74
|
+
|
|
75
|
+
await alicePage.getByRole('textbox', { name: 'Message' }).fill('Hello Bob!');
|
|
76
|
+
await alicePage.getByRole('button', { name: 'Send' }).click();
|
|
77
|
+
|
|
78
|
+
await expect(bobPage.getByText('Hello Bob!')).toBeVisible();
|
|
79
|
+
await expect(alicePage.getByText('Hello Bob!')).toBeVisible();
|
|
80
|
+
|
|
81
|
+
await aliceContext.close();
|
|
82
|
+
await bobContext.close();
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multi-User Fixture for Reusability
|
|
87
|
+
|
|
88
|
+
**Use when**: Multiple tests need two-user setups. Wrap context creation in a fixture to avoid boilerplate.
|
|
89
|
+
**Avoid when**: Only one test needs multi-user logic.
|
|
90
|
+
|
|
91
|
+
**TypeScript**
|
|
92
|
+
```typescript
|
|
93
|
+
// fixtures.ts
|
|
94
|
+
import { test as base, expect, BrowserContext, Page } from '@playwright/test';
|
|
95
|
+
|
|
96
|
+
type MultiUserFixtures = {
|
|
97
|
+
aliceContext: BrowserContext;
|
|
98
|
+
alicePage: Page;
|
|
99
|
+
bobContext: BrowserContext;
|
|
100
|
+
bobPage: Page;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const test = base.extend<MultiUserFixtures>({
|
|
104
|
+
aliceContext: async ({ browser }, use) => {
|
|
105
|
+
const context = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
106
|
+
await use(context);
|
|
107
|
+
await context.close();
|
|
108
|
+
},
|
|
109
|
+
alicePage: async ({ aliceContext }, use) => {
|
|
110
|
+
const page = await aliceContext.newPage();
|
|
111
|
+
await use(page);
|
|
112
|
+
},
|
|
113
|
+
bobContext: async ({ browser }, use) => {
|
|
114
|
+
const context = await browser.newContext({ storageState: 'auth/bob.json' });
|
|
115
|
+
await use(context);
|
|
116
|
+
await context.close();
|
|
117
|
+
},
|
|
118
|
+
bobPage: async ({ bobContext }, use) => {
|
|
119
|
+
const page = await bobContext.newPage();
|
|
120
|
+
await use(page);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export { expect };
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// collaboration.spec.ts
|
|
129
|
+
import { test, expect } from './fixtures';
|
|
130
|
+
|
|
131
|
+
test('both users see shared document title', async ({ alicePage, bobPage }) => {
|
|
132
|
+
await alicePage.goto('/docs/shared-doc');
|
|
133
|
+
await bobPage.goto('/docs/shared-doc');
|
|
134
|
+
|
|
135
|
+
await alicePage.getByRole('textbox', { name: 'Title' }).fill('Project Plan');
|
|
136
|
+
|
|
137
|
+
await expect(bobPage.getByRole('textbox', { name: 'Title' })).toHaveValue('Project Plan');
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**JavaScript**
|
|
142
|
+
```javascript
|
|
143
|
+
// fixtures.js
|
|
144
|
+
const { test: base, expect } = require('@playwright/test');
|
|
145
|
+
|
|
146
|
+
const test = base.extend({
|
|
147
|
+
aliceContext: async ({ browser }, use) => {
|
|
148
|
+
const context = await browser.newContext({ storageState: 'auth/alice.json' });
|
|
149
|
+
await use(context);
|
|
150
|
+
await context.close();
|
|
151
|
+
},
|
|
152
|
+
alicePage: async ({ aliceContext }, use) => {
|
|
153
|
+
const page = await aliceContext.newPage();
|
|
154
|
+
await use(page);
|
|
155
|
+
},
|
|
156
|
+
bobContext: async ({ browser }, use) => {
|
|
157
|
+
const context = await browser.newContext({ storageState: 'auth/bob.json' });
|
|
158
|
+
await use(context);
|
|
159
|
+
await context.close();
|
|
160
|
+
},
|
|
161
|
+
bobPage: async ({ bobContext }, use) => {
|
|
162
|
+
const page = await bobContext.newPage();
|
|
163
|
+
await use(page);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
module.exports = { test, expect };
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Collaborative Editing with Conflict Detection
|
|
171
|
+
|
|
172
|
+
**Use when**: Testing real-time collaborative editors (Google Docs-style) where both users edit the same content.
|
|
173
|
+
**Avoid when**: Your app does not support concurrent editing.
|
|
174
|
+
|
|
175
|
+
**TypeScript**
|
|
176
|
+
```typescript
|
|
177
|
+
import { test, expect } from './fixtures';
|
|
178
|
+
|
|
179
|
+
test('concurrent edits merge without data loss', async ({ alicePage, bobPage }) => {
|
|
180
|
+
await alicePage.goto('/docs/shared-doc');
|
|
181
|
+
await bobPage.goto('/docs/shared-doc');
|
|
182
|
+
|
|
183
|
+
// Wait for both to be connected
|
|
184
|
+
await expect(alicePage.getByTestId('connection-status')).toHaveText('Connected');
|
|
185
|
+
await expect(bobPage.getByTestId('connection-status')).toHaveText('Connected');
|
|
186
|
+
|
|
187
|
+
// Alice types at the beginning
|
|
188
|
+
const aliceEditor = alicePage.getByRole('textbox', { name: 'Editor' });
|
|
189
|
+
await aliceEditor.pressSequentially('Alice was here. ');
|
|
190
|
+
|
|
191
|
+
// Bob types at the end (simultaneously)
|
|
192
|
+
const bobEditor = bobPage.getByRole('textbox', { name: 'Editor' });
|
|
193
|
+
await bobEditor.press('End');
|
|
194
|
+
await bobEditor.pressSequentially('Bob was here.');
|
|
195
|
+
|
|
196
|
+
// Both edits should be visible to both users
|
|
197
|
+
await expect(aliceEditor).toContainText('Alice was here.');
|
|
198
|
+
await expect(aliceEditor).toContainText('Bob was here.');
|
|
199
|
+
await expect(bobEditor).toContainText('Alice was here.');
|
|
200
|
+
await expect(bobEditor).toContainText('Bob was here.');
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**JavaScript**
|
|
205
|
+
```javascript
|
|
206
|
+
const { test, expect } = require('./fixtures');
|
|
207
|
+
|
|
208
|
+
test('concurrent edits merge without data loss', async ({ alicePage, bobPage }) => {
|
|
209
|
+
await alicePage.goto('/docs/shared-doc');
|
|
210
|
+
await bobPage.goto('/docs/shared-doc');
|
|
211
|
+
|
|
212
|
+
await expect(alicePage.getByTestId('connection-status')).toHaveText('Connected');
|
|
213
|
+
await expect(bobPage.getByTestId('connection-status')).toHaveText('Connected');
|
|
214
|
+
|
|
215
|
+
const aliceEditor = alicePage.getByRole('textbox', { name: 'Editor' });
|
|
216
|
+
await aliceEditor.pressSequentially('Alice was here. ');
|
|
217
|
+
|
|
218
|
+
const bobEditor = bobPage.getByRole('textbox', { name: 'Editor' });
|
|
219
|
+
await bobEditor.press('End');
|
|
220
|
+
await bobEditor.pressSequentially('Bob was here.');
|
|
221
|
+
|
|
222
|
+
await expect(aliceEditor).toContainText('Alice was here.');
|
|
223
|
+
await expect(aliceEditor).toContainText('Bob was here.');
|
|
224
|
+
await expect(bobEditor).toContainText('Alice was here.');
|
|
225
|
+
await expect(bobEditor).toContainText('Bob was here.');
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Shared State Verification (Presence, Cursors, Indicators)
|
|
230
|
+
|
|
231
|
+
**Use when**: Verifying that one user's presence or activity is reflected in the other user's UI -- online indicators, typing indicators, cursor positions.
|
|
232
|
+
**Avoid when**: Presence is not a feature of your app.
|
|
233
|
+
|
|
234
|
+
**TypeScript**
|
|
235
|
+
```typescript
|
|
236
|
+
import { test, expect } from './fixtures';
|
|
237
|
+
|
|
238
|
+
test('bob sees alice typing indicator', async ({ alicePage, bobPage }) => {
|
|
239
|
+
await alicePage.goto('/chat/general');
|
|
240
|
+
await bobPage.goto('/chat/general');
|
|
241
|
+
|
|
242
|
+
// Alice starts typing
|
|
243
|
+
await alicePage.getByRole('textbox', { name: 'Message' }).pressSequentially('Hel', { delay: 100 });
|
|
244
|
+
|
|
245
|
+
// Bob sees the typing indicator
|
|
246
|
+
await expect(bobPage.getByText('Alice is typing...')).toBeVisible();
|
|
247
|
+
|
|
248
|
+
// Alice stops typing — indicator disappears after debounce
|
|
249
|
+
await expect(bobPage.getByText('Alice is typing...')).toBeHidden({ timeout: 5000 });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('online users list updates when user joins', async ({ alicePage, bobPage }) => {
|
|
253
|
+
await alicePage.goto('/chat/general');
|
|
254
|
+
|
|
255
|
+
// Alice sees only herself
|
|
256
|
+
await expect(alicePage.getByTestId('online-users').getByText('Alice')).toBeVisible();
|
|
257
|
+
await expect(alicePage.getByTestId('online-users').getByText('Bob')).toBeHidden();
|
|
258
|
+
|
|
259
|
+
// Bob joins
|
|
260
|
+
await bobPage.goto('/chat/general');
|
|
261
|
+
|
|
262
|
+
// Alice now sees Bob in the online list
|
|
263
|
+
await expect(alicePage.getByTestId('online-users').getByText('Bob')).toBeVisible();
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**JavaScript**
|
|
268
|
+
```javascript
|
|
269
|
+
const { test, expect } = require('./fixtures');
|
|
270
|
+
|
|
271
|
+
test('bob sees alice typing indicator', async ({ alicePage, bobPage }) => {
|
|
272
|
+
await alicePage.goto('/chat/general');
|
|
273
|
+
await bobPage.goto('/chat/general');
|
|
274
|
+
|
|
275
|
+
await alicePage.getByRole('textbox', { name: 'Message' }).pressSequentially('Hel', { delay: 100 });
|
|
276
|
+
await expect(bobPage.getByText('Alice is typing...')).toBeVisible();
|
|
277
|
+
await expect(bobPage.getByText('Alice is typing...')).toBeHidden({ timeout: 5000 });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('online users list updates when user joins', async ({ alicePage, bobPage }) => {
|
|
281
|
+
await alicePage.goto('/chat/general');
|
|
282
|
+
await expect(alicePage.getByTestId('online-users').getByText('Alice')).toBeVisible();
|
|
283
|
+
|
|
284
|
+
await bobPage.goto('/chat/general');
|
|
285
|
+
await expect(alicePage.getByTestId('online-users').getByText('Bob')).toBeVisible();
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Race Condition Testing Between Users
|
|
290
|
+
|
|
291
|
+
**Use when**: You need to verify that simultaneous actions (two users clicking "buy" on the last item, or two users editing the same field) are handled correctly.
|
|
292
|
+
**Avoid when**: Your app has no shared mutable resources.
|
|
293
|
+
|
|
294
|
+
**TypeScript**
|
|
295
|
+
```typescript
|
|
296
|
+
import { test, expect } from './fixtures';
|
|
297
|
+
|
|
298
|
+
test('only one user can claim the last item', async ({ alicePage, bobPage }) => {
|
|
299
|
+
await alicePage.goto('/store/limited-item');
|
|
300
|
+
await bobPage.goto('/store/limited-item');
|
|
301
|
+
|
|
302
|
+
// Both see the item is available
|
|
303
|
+
await expect(alicePage.getByText('1 remaining')).toBeVisible();
|
|
304
|
+
await expect(bobPage.getByText('1 remaining')).toBeVisible();
|
|
305
|
+
|
|
306
|
+
// Both click buy at approximately the same time
|
|
307
|
+
const aliceBuy = alicePage.getByRole('button', { name: 'Buy now' }).click();
|
|
308
|
+
const bobBuy = bobPage.getByRole('button', { name: 'Buy now' }).click();
|
|
309
|
+
await Promise.all([aliceBuy, bobBuy]);
|
|
310
|
+
|
|
311
|
+
// One succeeds, one fails — verify the app handles the race
|
|
312
|
+
const aliceResult = await alicePage.getByTestId('purchase-result').textContent();
|
|
313
|
+
const bobResult = await bobPage.getByTestId('purchase-result').textContent();
|
|
314
|
+
|
|
315
|
+
const results = [aliceResult, bobResult];
|
|
316
|
+
expect(results).toContain('Purchase successful');
|
|
317
|
+
expect(results).toContain('Item no longer available');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('simultaneous form submissions do not create duplicates', async ({ alicePage, bobPage }) => {
|
|
321
|
+
await alicePage.goto('/admin/settings');
|
|
322
|
+
await bobPage.goto('/admin/settings');
|
|
323
|
+
|
|
324
|
+
// Both change the same setting
|
|
325
|
+
await alicePage.getByLabel('Company name').fill('Alice Corp');
|
|
326
|
+
await bobPage.getByLabel('Company name').fill('Bob Inc');
|
|
327
|
+
|
|
328
|
+
// Submit simultaneously
|
|
329
|
+
await Promise.all([
|
|
330
|
+
alicePage.getByRole('button', { name: 'Save' }).click(),
|
|
331
|
+
bobPage.getByRole('button', { name: 'Save' }).click(),
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
// Reload both pages — one value should win, no corruption
|
|
335
|
+
await alicePage.reload();
|
|
336
|
+
const finalValue = await alicePage.getByLabel('Company name').inputValue();
|
|
337
|
+
expect(['Alice Corp', 'Bob Inc']).toContain(finalValue);
|
|
338
|
+
|
|
339
|
+
// The "loser" should see a conflict notification or the updated value
|
|
340
|
+
await bobPage.reload();
|
|
341
|
+
await expect(bobPage.getByLabel('Company name')).toHaveValue(finalValue);
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**JavaScript**
|
|
346
|
+
```javascript
|
|
347
|
+
const { test, expect } = require('./fixtures');
|
|
348
|
+
|
|
349
|
+
test('only one user can claim the last item', async ({ alicePage, bobPage }) => {
|
|
350
|
+
await alicePage.goto('/store/limited-item');
|
|
351
|
+
await bobPage.goto('/store/limited-item');
|
|
352
|
+
|
|
353
|
+
await expect(alicePage.getByText('1 remaining')).toBeVisible();
|
|
354
|
+
await expect(bobPage.getByText('1 remaining')).toBeVisible();
|
|
355
|
+
|
|
356
|
+
const aliceBuy = alicePage.getByRole('button', { name: 'Buy now' }).click();
|
|
357
|
+
const bobBuy = bobPage.getByRole('button', { name: 'Buy now' }).click();
|
|
358
|
+
await Promise.all([aliceBuy, bobBuy]);
|
|
359
|
+
|
|
360
|
+
const aliceResult = await alicePage.getByTestId('purchase-result').textContent();
|
|
361
|
+
const bobResult = await bobPage.getByTestId('purchase-result').textContent();
|
|
362
|
+
|
|
363
|
+
const results = [aliceResult, bobResult];
|
|
364
|
+
expect(results).toContain('Purchase successful');
|
|
365
|
+
expect(results).toContain('Item no longer available');
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### N Users with Dynamic Context Creation
|
|
370
|
+
|
|
371
|
+
**Use when**: You need more than two users, or the number of users is variable (load-like collaboration tests).
|
|
372
|
+
**Avoid when**: Two users suffice. Keep it simple.
|
|
373
|
+
|
|
374
|
+
**TypeScript**
|
|
375
|
+
```typescript
|
|
376
|
+
import { test, expect, Browser, Page } from '@playwright/test';
|
|
377
|
+
|
|
378
|
+
async function createUser(browser: Browser, name: string, room: string): Promise<Page> {
|
|
379
|
+
const context = await browser.newContext({ storageState: `auth/${name}.json` });
|
|
380
|
+
const page = await context.newPage();
|
|
381
|
+
await page.goto(`/chat/${room}`);
|
|
382
|
+
return page;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
test('five users in a chat room all see each other', async ({ browser }) => {
|
|
386
|
+
const names = ['alice', 'bob', 'charlie', 'diana', 'eve'];
|
|
387
|
+
const pages = await Promise.all(
|
|
388
|
+
names.map((name) => createUser(browser, name, 'team-room'))
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// First user sends a message
|
|
392
|
+
await pages[0].getByRole('textbox', { name: 'Message' }).fill('Hello everyone!');
|
|
393
|
+
await pages[0].getByRole('button', { name: 'Send' }).click();
|
|
394
|
+
|
|
395
|
+
// All users see the message
|
|
396
|
+
for (const page of pages) {
|
|
397
|
+
await expect(page.getByText('Hello everyone!')).toBeVisible();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Cleanup
|
|
401
|
+
for (const page of pages) {
|
|
402
|
+
await page.context().close();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**JavaScript**
|
|
408
|
+
```javascript
|
|
409
|
+
const { test, expect } = require('@playwright/test');
|
|
410
|
+
|
|
411
|
+
async function createUser(browser, name, room) {
|
|
412
|
+
const context = await browser.newContext({ storageState: `auth/${name}.json` });
|
|
413
|
+
const page = await context.newPage();
|
|
414
|
+
await page.goto(`/chat/${room}`);
|
|
415
|
+
return page;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
test('five users in a chat room all see each other', async ({ browser }) => {
|
|
419
|
+
const names = ['alice', 'bob', 'charlie', 'diana', 'eve'];
|
|
420
|
+
const pages = await Promise.all(
|
|
421
|
+
names.map((name) => createUser(browser, name, 'team-room'))
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
await pages[0].getByRole('textbox', { name: 'Message' }).fill('Hello everyone!');
|
|
425
|
+
await pages[0].getByRole('button', { name: 'Send' }).click();
|
|
426
|
+
|
|
427
|
+
for (const page of pages) {
|
|
428
|
+
await expect(page.getByText('Hello everyone!')).toBeVisible();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (const page of pages) {
|
|
432
|
+
await page.context().close();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Decision Guide
|
|
438
|
+
|
|
439
|
+
| Scenario | Approach | Why |
|
|
440
|
+
|---|---|---|
|
|
441
|
+
| Two users interact in real time | Two `browser.newContext()` in one test | Fully isolated sessions, same test timeline |
|
|
442
|
+
| Same multi-user setup across many tests | Custom fixture per user context | Eliminates boilerplate, guarantees cleanup |
|
|
443
|
+
| Testing presence/typing indicators | Sequential actions, assert on other page | Order matters -- act on one, assert on the other |
|
|
444
|
+
| Race condition (simultaneous clicks) | `Promise.all([action1, action2])` | Fires both as close to simultaneously as possible |
|
|
445
|
+
| Admin performs action, user sees result | Two contexts with different `storageState` | Different auth roles, same browser instance |
|
|
446
|
+
| 3+ users in one test | Loop with `browser.newContext()` per user | Each context is cheap; browser is shared |
|
|
447
|
+
| Cross-browser multi-user (Chrome + Firefox) | Separate browser launches in `beforeAll` | Rarely needed; contexts within one browser suffice |
|
|
448
|
+
|
|
449
|
+
## Anti-Patterns
|
|
450
|
+
|
|
451
|
+
| Don't Do This | Problem | Do This Instead |
|
|
452
|
+
|---|---|---|
|
|
453
|
+
| Use one context with two pages for two users | Pages in the same context share cookies and localStorage | Use `browser.newContext()` per user |
|
|
454
|
+
| Open a second browser for the second user | Wastes memory and startup time | Use a second `browser.newContext()` -- contexts are lightweight |
|
|
455
|
+
| `await page.waitForTimeout(2000)` for real-time sync | Arbitrary delay; flaky in CI, slow in dev | `await expect(bobPage.getByText('msg')).toBeVisible()` |
|
|
456
|
+
| Share mutable state via `let` variables between contexts | Test order and timing become implicit dependencies | Each context is independent; assert via UI |
|
|
457
|
+
| Put multi-user setup in `beforeAll` | `beforeAll` cannot access `page` or `context` | Use test-scoped fixtures or inline `browser.newContext()` |
|
|
458
|
+
| Forget to close extra contexts | Leaks memory and may cause port exhaustion | Always close contexts in fixture teardown or `finally` |
|
|
459
|
+
| Assert on both pages without waiting | The second page may not have received the update yet | Use `expect` with auto-retry on the receiving page |
|
|
460
|
+
|
|
461
|
+
## Troubleshooting
|
|
462
|
+
|
|
463
|
+
| Symptom | Cause | Fix |
|
|
464
|
+
|---|---|---|
|
|
465
|
+
| Second user sees stale data | Context created before server state was ready | Navigate the second page after the first user's action completes |
|
|
466
|
+
| `storageState` file not found | Auth setup project has not run, or path is wrong | Run auth setup first via `dependencies` in `playwright.config` |
|
|
467
|
+
| Both users have the same session | Reusing the same `storageState` file for both | Create distinct auth state files per user role |
|
|
468
|
+
| Real-time updates never arrive on second page | WebSocket/SSE connection not established before assertion | Wait for a connection indicator before asserting: `await expect(page.getByTestId('connected')).toBeVisible()` |
|
|
469
|
+
| Test is slow with many contexts | Too many contexts in a single test (10+) | Limit to 3-5 contexts per test; use API setup to reduce page interactions |
|
|
470
|
+
| `Promise.all` race test always has same winner | Network/event loop makes one always faster | This is expected in testing -- assert that the app handles both outcomes, not which user wins |
|
|
471
|
+
|
|
472
|
+
## Related
|
|
473
|
+
|
|
474
|
+
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrap multi-user contexts in fixtures
|
|
475
|
+
- [core/websockets-and-realtime.md](websockets-and-realtime.md) -- test the transport layer beneath collaboration features
|
|
476
|
+
- [core/test-data-management.md](test-data-management.md) -- set up shared resources (rooms, documents) before multi-user tests
|
|
477
|
+
- [core/configuration.md](configuration.md) -- configure `storageState` per project for different user roles
|