SurfSense/.cursor/skills/playwright-testing/advanced/multi-user.md
2026-05-10 04:19:55 +05:30

11 KiB

Multi-User & Collaboration Testing

Table of Contents

  1. Multiple Browser Contexts
  2. Real-Time Collaboration
  3. Role-Based Testing
  4. Concurrent Actions
  5. Chat & Messaging

Multiple Browser Contexts

Two Users in Same Test

test("two users see each other's changes", async ({ browser }) => {
  // Create two isolated contexts (like two browsers)
  const userAContext = await browser.newContext();
  const userBContext = await browser.newContext();

  const userAPage = await userAContext.newPage();
  const userBPage = await userBContext.newPage();

  // Both users go to the same document
  await userAPage.goto("/doc/shared-123");
  await userBPage.goto("/doc/shared-123");

  // User A types
  await userAPage.getByLabel("Content").fill("Hello from User A");

  // User B should see the change
  await expect(userBPage.getByText("Hello from User A")).toBeVisible();

  // Cleanup
  await userAContext.close();
  await userBContext.close();
});

Multiple Users with Auth States

test("admin and user interaction", async ({ browser }) => {
  // Load different auth states
  const adminContext = await browser.newContext({
    storageState: ".auth/admin.json",
  });
  const userContext = await browser.newContext({
    storageState: ".auth/user.json",
  });

  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  // User submits request
  await userPage.goto("/support");
  await userPage.getByLabel("Message").fill("Need help!");
  await userPage.getByRole("button", { name: "Submit" }).click();

  // Admin sees and responds
  await adminPage.goto("/admin/tickets");
  await expect(adminPage.getByText("Need help!")).toBeVisible();
  await adminPage.getByRole("button", { name: "Reply" }).click();
  await adminPage.getByLabel("Response").fill("How can I help?");
  await adminPage.getByRole("button", { name: "Send" }).click();

  // User sees response
  await expect(userPage.getByText("How can I help?")).toBeVisible();

  await adminContext.close();
  await userContext.close();
});

Multi-User Fixture

// fixtures/multi-user.fixture.ts
import { test as base, Browser, BrowserContext, Page } from "@playwright/test";

type UserSession = {
  context: BrowserContext;
  page: Page;
};

type MultiUserFixtures = {
  createUser: (authState?: string) => Promise<UserSession>;
};

export const test = base.extend<MultiUserFixtures>({
  createUser: async ({ browser }, use) => {
    const sessions: UserSession[] = [];

    await use(async (authState) => {
      const context = await browser.newContext({
        storageState: authState,
      });
      const page = await context.newPage();
      sessions.push({ context, page });
      return { context, page };
    });

    // Cleanup all sessions
    for (const session of sessions) {
      await session.context.close();
    }
  },
});

// Usage
test("3 users collaborate", async ({ createUser }) => {
  const alice = await createUser(".auth/alice.json");
  const bob = await createUser(".auth/bob.json");
  const charlie = await createUser(".auth/charlie.json");

  // All navigate to same room
  await alice.page.goto("/room/123");
  await bob.page.goto("/room/123");
  await charlie.page.goto("/room/123");

  // Test interactions...
});

Real-Time Collaboration

Collaborative Document

test("real-time collaborative editing", async ({ browser }) => {
  const user1 = await browser.newContext();
  const user2 = await browser.newContext();

  const page1 = await user1.newPage();
  const page2 = await user2.newPage();

  await page1.goto("/docs/shared");
  await page2.goto("/docs/shared");

  // User 1 types at the beginning
  const editor1 = page1.getByRole("textbox");
  await editor1.click();
  await editor1.press("Home");
  await editor1.type("User 1: ");

  // User 2 types at the end
  const editor2 = page2.getByRole("textbox");
  await editor2.click();
  await editor2.press("End");
  await editor2.type(" - User 2");

  // Both should see combined result
  await expect(page1.getByRole("textbox")).toContainText("User 1:");
  await expect(page1.getByRole("textbox")).toContainText("- User 2");
  await expect(page2.getByRole("textbox")).toContainText("User 1:");
  await expect(page2.getByRole("textbox")).toContainText("- User 2");

  await user1.close();
  await user2.close();
});

Cursor Presence

test("shows other user cursors", async ({ browser }) => {
  const ctx1 = await browser.newContext();
  const ctx2 = await browser.newContext();

  const page1 = await ctx1.newPage();
  const page2 = await ctx2.newPage();

  // Mock to identify users
  await page1.route("**/api/me", (route) =>
    route.fulfill({ json: { id: "user-1", name: "Alice" } }),
  );
  await page2.route("**/api/me", (route) =>
    route.fulfill({ json: { id: "user-2", name: "Bob" } }),
  );

  await page1.goto("/whiteboard/123");
  await page2.goto("/whiteboard/123");

  // Move cursor on page1
  await page1.mouse.move(200, 200);

  // Page2 should see Alice's cursor
  await expect(page2.getByTestId("cursor-user-1")).toBeVisible();
  await expect(page2.getByText("Alice")).toBeVisible();

  await ctx1.close();
  await ctx2.close();
});

Role-Based Testing

Test RBAC

const roles = [
  { role: "admin", canDelete: true, canEdit: true, canView: true },
  { role: "editor", canDelete: false, canEdit: true, canView: true },
  { role: "viewer", canDelete: false, canEdit: false, canView: true },
];

for (const { role, canDelete, canEdit, canView } of roles) {
  test(`${role} permissions`, async ({ browser }) => {
    const context = await browser.newContext({
      storageState: `.auth/${role}.json`,
    });
    const page = await context.newPage();

    await page.goto("/document/123");

    // Check view permission
    if (canView) {
      await expect(page.getByTestId("content")).toBeVisible();
    } else {
      await expect(page.getByText("Access denied")).toBeVisible();
    }

    // Check edit permission
    const editButton = page.getByRole("button", { name: "Edit" });
    if (canEdit) {
      await expect(editButton).toBeEnabled();
    } else {
      await expect(editButton).toBeDisabled();
    }

    // Check delete permission
    const deleteButton = page.getByRole("button", { name: "Delete" });
    if (canDelete) {
      await expect(deleteButton).toBeVisible();
    } else {
      await expect(deleteButton).toBeHidden();
    }

    await context.close();
  });
}

Permission Escalation Test

test("cannot access admin routes as user", async ({ browser }) => {
  const userContext = await browser.newContext({
    storageState: ".auth/user.json",
  });
  const page = await userContext.newPage();

  // Try to access admin page directly
  await page.goto("/admin/users");

  // Should redirect or show error
  await expect(page).not.toHaveURL("/admin/users");
  await expect(page.getByText("Access denied")).toBeVisible();

  await userContext.close();
});

Concurrent Actions

Race Condition Testing

test("handles concurrent edits", async ({ browser }) => {
  const ctx1 = await browser.newContext();
  const ctx2 = await browser.newContext();

  const page1 = await ctx1.newPage();
  const page2 = await ctx2.newPage();

  await page1.goto("/item/123");
  await page2.goto("/item/123");

  // Both click edit at the same time
  await Promise.all([
    page1.getByRole("button", { name: "Edit" }).click(),
    page2.getByRole("button", { name: "Edit" }).click(),
  ]);

  // Both try to save different values
  await page1.getByLabel("Name").fill("Value from User 1");
  await page2.getByLabel("Name").fill("Value from User 2");

  await Promise.all([
    page1.getByRole("button", { name: "Save" }).click(),
    page2.getByRole("button", { name: "Save" }).click(),
  ]);

  // One should succeed, one should get conflict error
  const page1HasConflict = await page1.getByText("Conflict").isVisible();
  const page2HasConflict = await page2.getByText("Conflict").isVisible();

  // Exactly one should have conflict
  expect(page1HasConflict || page2HasConflict).toBe(true);
  expect(page1HasConflict && page2HasConflict).toBe(false);

  await ctx1.close();
  await ctx2.close();
});

Optimistic Locking Test

test("optimistic locking prevents overwrites", async ({ browser }) => {
  const ctx1 = await browser.newContext();
  const ctx2 = await browser.newContext();

  const page1 = await ctx1.newPage();
  const page2 = await ctx2.newPage();

  // Both load the same version
  await page1.goto("/record/123");
  await page2.goto("/record/123");

  // User 1 edits and saves first
  await page1.getByRole("button", { name: "Edit" }).click();
  await page1.getByLabel("Value").fill("Updated by User 1");
  await page1.getByRole("button", { name: "Save" }).click();
  await expect(page1.getByText("Saved")).toBeVisible();

  // User 2 tries to save with stale version
  await page2.getByRole("button", { name: "Edit" }).click();
  await page2.getByLabel("Value").fill("Updated by User 2");
  await page2.getByRole("button", { name: "Save" }).click();

  // Should fail with version conflict
  await expect(page2.getByText("Someone else modified this")).toBeVisible();
  await expect(page2.getByRole("button", { name: "Reload" })).toBeVisible();

  await ctx1.close();
  await ctx2.close();
});

Chat & Messaging

Real-Time Chat

test("chat messages sync between users", async ({ browser }) => {
  const aliceCtx = await browser.newContext();
  const bobCtx = await browser.newContext();

  const alicePage = await aliceCtx.newPage();
  const bobPage = await bobCtx.newPage();

  // Setup user identities
  await alicePage.route("**/api/me", (r) =>
    r.fulfill({ json: { name: "Alice" } }),
  );
  await bobPage.route("**/api/me", (r) => r.fulfill({ json: { name: "Bob" } }));

  await alicePage.goto("/chat/room-1");
  await bobPage.goto("/chat/room-1");

  // Alice sends message
  await alicePage.getByLabel("Message").fill("Hi Bob!");
  await alicePage.getByRole("button", { name: "Send" }).click();

  // Bob sees it
  await expect(bobPage.getByText("Alice: Hi Bob!")).toBeVisible();

  // Bob replies
  await bobPage.getByLabel("Message").fill("Hey Alice!");
  await bobPage.getByRole("button", { name: "Send" }).click();

  // Alice sees it
  await expect(alicePage.getByText("Bob: Hey Alice!")).toBeVisible();

  await aliceCtx.close();
  await bobCtx.close();
});

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Sharing context between users State leaks, not isolated Create separate contexts
Not closing contexts Memory leak, browser overload Always close in cleanup
Hardcoded timing for sync Flaky tests Use expect().toBeVisible()
Testing only single user Misses collaboration bugs Test multi-user scenarios