Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
advanced/multi-user.md
1# Multi-User & Collaboration Testing23## Table of Contents451. [Multiple Browser Contexts](#multiple-browser-contexts)62. [Real-Time Collaboration](#real-time-collaboration)73. [Role-Based Testing](#role-based-testing)84. [Concurrent Actions](#concurrent-actions)95. [Chat & Messaging](#chat--messaging)1011## Multiple Browser Contexts1213### Two Users in Same Test1415```typescript16test("two users see each other's changes", async ({ browser }) => {17// Create two isolated contexts (like two browsers)18const userAContext = await browser.newContext();19const userBContext = await browser.newContext();2021const userAPage = await userAContext.newPage();22const userBPage = await userBContext.newPage();2324// Both users go to the same document25await userAPage.goto("/doc/shared-123");26await userBPage.goto("/doc/shared-123");2728// User A types29await userAPage.getByLabel("Content").fill("Hello from User A");3031// User B should see the change32await expect(userBPage.getByText("Hello from User A")).toBeVisible();3334// Cleanup35await userAContext.close();36await userBContext.close();37});38```3940### Multiple Users with Auth States4142```typescript43test("admin and user interaction", async ({ browser }) => {44// Load different auth states45const adminContext = await browser.newContext({46storageState: ".auth/admin.json",47});48const userContext = await browser.newContext({49storageState: ".auth/user.json",50});5152const adminPage = await adminContext.newPage();53const userPage = await userContext.newPage();5455// User submits request56await userPage.goto("/support");57await userPage.getByLabel("Message").fill("Need help!");58await userPage.getByRole("button", { name: "Submit" }).click();5960// Admin sees and responds61await adminPage.goto("/admin/tickets");62await expect(adminPage.getByText("Need help!")).toBeVisible();63await adminPage.getByRole("button", { name: "Reply" }).click();64await adminPage.getByLabel("Response").fill("How can I help?");65await adminPage.getByRole("button", { name: "Send" }).click();6667// User sees response68await expect(userPage.getByText("How can I help?")).toBeVisible();6970await adminContext.close();71await userContext.close();72});73```7475### Multi-User Fixture7677```typescript78// fixtures/multi-user.fixture.ts79import { test as base, Browser, BrowserContext, Page } from "@playwright/test";8081type UserSession = {82context: BrowserContext;83page: Page;84};8586type MultiUserFixtures = {87createUser: (authState?: string) => Promise<UserSession>;88};8990export const test = base.extend<MultiUserFixtures>({91createUser: async ({ browser }, use) => {92const sessions: UserSession[] = [];9394await use(async (authState) => {95const context = await browser.newContext({96storageState: authState,97});98const page = await context.newPage();99sessions.push({ context, page });100return { context, page };101});102103// Cleanup all sessions104for (const session of sessions) {105await session.context.close();106}107},108});109110// Usage111test("3 users collaborate", async ({ createUser }) => {112const alice = await createUser(".auth/alice.json");113const bob = await createUser(".auth/bob.json");114const charlie = await createUser(".auth/charlie.json");115116// All navigate to same room117await alice.page.goto("/room/123");118await bob.page.goto("/room/123");119await charlie.page.goto("/room/123");120121// Test interactions...122});123```124125## Real-Time Collaboration126127### Collaborative Document128129```typescript130test("real-time collaborative editing", async ({ browser }) => {131const user1 = await browser.newContext();132const user2 = await browser.newContext();133134const page1 = await user1.newPage();135const page2 = await user2.newPage();136137await page1.goto("/docs/shared");138await page2.goto("/docs/shared");139140// User 1 types at the beginning141const editor1 = page1.getByRole("textbox");142await editor1.click();143await editor1.press("Home");144await editor1.type("User 1: ");145146// User 2 types at the end147const editor2 = page2.getByRole("textbox");148await editor2.click();149await editor2.press("End");150await editor2.type(" - User 2");151152// Both should see combined result153await expect(page1.getByRole("textbox")).toContainText("User 1:");154await expect(page1.getByRole("textbox")).toContainText("- User 2");155await expect(page2.getByRole("textbox")).toContainText("User 1:");156await expect(page2.getByRole("textbox")).toContainText("- User 2");157158await user1.close();159await user2.close();160});161```162163### Cursor Presence164165```typescript166test("shows other user cursors", async ({ browser }) => {167const ctx1 = await browser.newContext();168const ctx2 = await browser.newContext();169170const page1 = await ctx1.newPage();171const page2 = await ctx2.newPage();172173// Mock to identify users174await page1.route("**/api/me", (route) =>175route.fulfill({ json: { id: "user-1", name: "Alice" } }),176);177await page2.route("**/api/me", (route) =>178route.fulfill({ json: { id: "user-2", name: "Bob" } }),179);180181await page1.goto("/whiteboard/123");182await page2.goto("/whiteboard/123");183184// Move cursor on page1185await page1.mouse.move(200, 200);186187// Page2 should see Alice's cursor188await expect(page2.getByTestId("cursor-user-1")).toBeVisible();189await expect(page2.getByText("Alice")).toBeVisible();190191await ctx1.close();192await ctx2.close();193});194```195196## Role-Based Testing197198### Test RBAC199200```typescript201const 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];206207for (const { role, canDelete, canEdit, canView } of roles) {208test(`${role} permissions`, async ({ browser }) => {209const context = await browser.newContext({210storageState: `.auth/${role}.json`,211});212const page = await context.newPage();213214await page.goto("/document/123");215216// Check view permission217if (canView) {218await expect(page.getByTestId("content")).toBeVisible();219} else {220await expect(page.getByText("Access denied")).toBeVisible();221}222223// Check edit permission224const editButton = page.getByRole("button", { name: "Edit" });225if (canEdit) {226await expect(editButton).toBeEnabled();227} else {228await expect(editButton).toBeDisabled();229}230231// Check delete permission232const deleteButton = page.getByRole("button", { name: "Delete" });233if (canDelete) {234await expect(deleteButton).toBeVisible();235} else {236await expect(deleteButton).toBeHidden();237}238239await context.close();240});241}242```243244### Permission Escalation Test245246```typescript247test("cannot access admin routes as user", async ({ browser }) => {248const userContext = await browser.newContext({249storageState: ".auth/user.json",250});251const page = await userContext.newPage();252253// Try to access admin page directly254await page.goto("/admin/users");255256// Should redirect or show error257await expect(page).not.toHaveURL("/admin/users");258await expect(page.getByText("Access denied")).toBeVisible();259260await userContext.close();261});262```263264## Concurrent Actions265266### Race Condition Testing267268```typescript269test("handles concurrent edits", async ({ browser }) => {270const ctx1 = await browser.newContext();271const ctx2 = await browser.newContext();272273const page1 = await ctx1.newPage();274const page2 = await ctx2.newPage();275276await page1.goto("/item/123");277await page2.goto("/item/123");278279// Both click edit at the same time280await Promise.all([281page1.getByRole("button", { name: "Edit" }).click(),282page2.getByRole("button", { name: "Edit" }).click(),283]);284285// Both try to save different values286await page1.getByLabel("Name").fill("Value from User 1");287await page2.getByLabel("Name").fill("Value from User 2");288289await Promise.all([290page1.getByRole("button", { name: "Save" }).click(),291page2.getByRole("button", { name: "Save" }).click(),292]);293294// One should succeed, one should get conflict error295const page1HasConflict = await page1.getByText("Conflict").isVisible();296const page2HasConflict = await page2.getByText("Conflict").isVisible();297298// Exactly one should have conflict299expect(page1HasConflict || page2HasConflict).toBe(true);300expect(page1HasConflict && page2HasConflict).toBe(false);301302await ctx1.close();303await ctx2.close();304});305```306307### Optimistic Locking Test308309```typescript310test("optimistic locking prevents overwrites", async ({ browser }) => {311const ctx1 = await browser.newContext();312const ctx2 = await browser.newContext();313314const page1 = await ctx1.newPage();315const page2 = await ctx2.newPage();316317// Both load the same version318await page1.goto("/record/123");319await page2.goto("/record/123");320321// User 1 edits and saves first322await page1.getByRole("button", { name: "Edit" }).click();323await page1.getByLabel("Value").fill("Updated by User 1");324await page1.getByRole("button", { name: "Save" }).click();325await expect(page1.getByText("Saved")).toBeVisible();326327// User 2 tries to save with stale version328await page2.getByRole("button", { name: "Edit" }).click();329await page2.getByLabel("Value").fill("Updated by User 2");330await page2.getByRole("button", { name: "Save" }).click();331332// Should fail with version conflict333await expect(page2.getByText("Someone else modified this")).toBeVisible();334await expect(page2.getByRole("button", { name: "Reload" })).toBeVisible();335336await ctx1.close();337await ctx2.close();338});339```340341## Chat & Messaging342343### Real-Time Chat344345```typescript346test("chat messages sync between users", async ({ browser }) => {347const aliceCtx = await browser.newContext();348const bobCtx = await browser.newContext();349350const alicePage = await aliceCtx.newPage();351const bobPage = await bobCtx.newPage();352353// Setup user identities354await alicePage.route("**/api/me", (r) =>355r.fulfill({ json: { name: "Alice" } }),356);357await bobPage.route("**/api/me", (r) => r.fulfill({ json: { name: "Bob" } }));358359await alicePage.goto("/chat/room-1");360await bobPage.goto("/chat/room-1");361362// Alice sends message363await alicePage.getByLabel("Message").fill("Hi Bob!");364await alicePage.getByRole("button", { name: "Send" }).click();365366// Bob sees it367await expect(bobPage.getByText("Alice: Hi Bob!")).toBeVisible();368369// Bob replies370await bobPage.getByLabel("Message").fill("Hey Alice!");371await bobPage.getByRole("button", { name: "Send" }).click();372373// Alice sees it374await expect(alicePage.getByText("Bob: Hey Alice!")).toBeVisible();375376await aliceCtx.close();377await bobCtx.close();378});379```380381## Anti-Patterns to Avoid382383| 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 |389390## Related References391392- **Authentication**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for auth setup393- **WebSockets**: See [websockets.md](../browser-apis/websockets.md) for real-time mocking394