@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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. 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