import { expect } from "playwright/test"; import { afterAll, describe, it } from "vitest"; import { BASE_URL, E2E_PROJECT_ID, E2E_UID_2, E2E_WORKSPACE_PATH, mintToken } from "../helpers"; import { createDocWith } from "../setup/db"; import { authedPage, closeBrowser, getBrowser } from "./browser"; afterAll(closeBrowser); describe("real-time collaboration (two users, one doc)", () => { it("shows live edits across two sessions", async () => { const title = `collab-${Date.now()}`; const docId = await createDocWith(mintToken(), title); // A member sees only the projects they were let into, so the partner is let into the seeded one first. const open = await fetch(`${BASE_URL}/api/permissions`, { method: "PUT", headers: { Authorization: `Bearer ${mintToken()}`, "Content-Type": "application/json", }, body: JSON.stringify({ resource_type: "project", resource_ids: [E2E_PROJECT_ID], user_ids: [E2E_UID_2], actions: ["projects:open"], grant: true, }), }); expect(open.status).toBe(204); // Docs are permission-scoped: the creator must share the doc before a second member can open it (ADR 0042). const share = await fetch(`${BASE_URL}/api/permissions`, { method: "PUT", headers: { Authorization: `Bearer ${mintToken()}`, "Content-Type": "application/json", }, body: JSON.stringify({ doc_ids: [docId], user_ids: [E2E_UID_2], actions: ["docs:read", "docs:write"], grant: true, }), }); expect(share.status).toBe(204); // Docs are inline-editable with no separate "Edit" mode toggle; DocPage/DocDetail render the ProseMirror body directly for anyone with write access. const userA = await authedPage(`${E2E_WORKSPACE_PATH}/docs/${docId}`); await userA.waitForLoadState("networkidle"); await userA.locator(".ProseMirror").waitFor({ state: "visible" }); // second session as a different user (same browser, fresh context) const browser = await getBrowser(); const ctxB = await browser.newContext({ baseURL: BASE_URL }); await ctxB.addInitScript( ({ value }) => window.localStorage.setItem("session", value), { value: JSON.stringify({ state: { token: mintToken(E2E_UID_2), isLoggedIn: true }, version: 0 }) }, ); const userB = await ctxB.newPage(); await userB.goto(`${E2E_WORKSPACE_PATH}/docs/${docId}`); await userB.waitForLoadState("networkidle"); await userB.locator(".ProseMirror").waitFor({ state: "visible" }); const sentence = `written by user B ${Date.now()}`; const editorB = userB.locator(".ProseMirror"); await editorB.click(); await editorB.pressSequentially(sentence); // user A sees the text without any reload (CRDT sync) await expect(userA.locator(".ProseMirror").getByText(sentence)).toBeVisible({ timeout: 15_000 }); // soft presence check: a remote caret (collapsed) or selection (range) renders for the peer on the other side const remoteCursors = await userA .locator(".ProseMirror-yjs-cursor, .ProseMirror-yjs-selection") .count() .catch(() => 0); expect(remoteCursors).toBeGreaterThan(0); await ctxB.close(); }); });