Real-Time Collaboration Testing Guide
How to Test Collaborative Cursors with Playwright: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing collaborative cursors with Playwright. Multi-tab browser contexts, Liveblocks and Yjs provider sync, cursor position broadcasting, presence awareness, conflict resolution, and the pitfalls that break real collaboration test suites.
“Liveblocks powers real-time collaboration for over ten million monthly active users across thousands of production applications, from Figma-style design tools to collaborative document editors.”
Liveblocks 2025 usage data
Collaborative Cursor Sync Flow
1. Why Testing Collaborative Cursors Is Harder Than It Looks
Collaborative cursors are the colored pointers that show where other users are working in a shared document or canvas. They look simple, but the underlying system involves WebSocket connections, presence channels, CRDT (Conflict-free Replicated Data Type) synchronization, and real-time state broadcasting across multiple clients. Testing this requires something fundamentally different from a standard single-user Playwright test: you need multiple independent browser contexts connected to the same room simultaneously, each acting as a distinct user.
The first structural challenge is multi-context orchestration. Playwright can create multiple BrowserContext instances in a single test, but coordinating actions across them requires careful sequencing. You cannot simply run two pages in parallel and hope they see each other; you must wait for both connections to establish, verify presence awareness, then perform actions and assert the remote side received the update.
The second challenge is timing. Collaborative cursor updates are inherently asynchronous. When User A moves their cursor, the position travels through a WebSocket to the server (or directly via WebRTC), gets broadcast to other participants, and then renders on User B's screen. This round-trip can take anywhere from 10ms on localhost to 200ms in production. Your tests must account for this latency without resorting to arbitrary waitForTimeout calls that make the suite slow and brittle.
Third, providers differ significantly. Liveblocks uses a managed WebSocket infrastructure with rooms and presence APIs. Yjs relies on a provider model (y-websocket, y-webrtc, or Hocuspocus) where the CRDT document is the source of truth and awareness (presence) is a separate protocol. PartyKit, Supabase Realtime, and Firebase each have their own connection lifecycle. Your test harness must understand the specific provider's readiness signals.
Fourth, conflict resolution is invisible until it breaks. Two users editing the same word at the same time should produce a deterministic merge. CRDTs guarantee eventual consistency, but the intermediate states can be surprising. Testing conflict resolution means performing truly simultaneous edits and verifying both clients converge to the same document state.
Fifth, network reliability matters. Collaborative editors must handle disconnects gracefully: buffering local changes, showing stale presence indicators, and replaying missed updates on reconnect. Testing this means programmatically killing WebSocket connections mid-test and verifying recovery behavior.
Collaborative Cursor Data Flow
User A
Moves cursor
WebSocket
Send position
Room Server
Broadcast to peers
CRDT Merge
Update shared state
User B
Render remote cursor
Presence Store
Update user list
Yjs Awareness Protocol
Local Change
Cursor position update
Awareness Encode
awareness.setLocalState()
Provider Sync
y-websocket transport
Remote Decode
awareness.on('change')
Render Cursor
CSS transform applied
2. Setting Up a Multi-Context Test Environment
Testing collaborative features requires a fundamentally different Playwright setup than single-user tests. You need multiple browser contexts that can connect to the same collaboration room simultaneously. Each context represents a distinct user with its own cookies, storage, and WebSocket connections.
Collaboration Test Environment Checklist
- Install Playwright with WebSocket debugging support enabled
- Configure your collaboration server (Liveblocks dev key or local Hocuspocus/y-websocket)
- Set up test room names with unique identifiers per test run to avoid cross-contamination
- Create a helper function that spawns N browser contexts and connects each to the same room
- Add a waitForPresence utility that polls until all expected users appear in the room
- Configure WebSocket route interception for disconnect/reconnect scenarios
- Set up cleanup hooks that close all contexts and delete test rooms after each test
- Disable cursor animation CSS transitions in test mode for deterministic position assertions
Environment Variables
Multi-Context Test Fixture
The key pattern is a reusable fixture that creates multiple browser contexts and pages, each configured with a distinct user identity. Playwright's browser.newContext() creates isolated sessions sharing the same browser process, which is exactly what you need: separate WebSocket connections, separate cookies, but all running in the same test.
Waiting for Presence Sync
The most critical helper function is one that waits until both users are visible in the room. Without this, your test will race against the WebSocket connection and fail intermittently.
Cursor Position Broadcasting Between Two Users
Moderate3. Scenario: Cursor Position Broadcasting Between Two Users
The fundamental collaborative cursor test: User A moves their mouse over the document canvas, and User B sees a colored cursor indicator tracking User A's position in real time. This validates the entire presence pipeline, from local cursor capture through WebSocket broadcast to remote rendering.
The key challenge is asserting position accuracy. Cursor coordinates are relative to the document viewport, and both users may have different scroll positions or window sizes. Your test should normalize coordinates or use a fixed viewport size to ensure deterministic results.
Cursor Broadcast Test: Playwright vs Assrt
test('cursor position broadcasts', async ({ userA, userB }) => {
await waitForPresence(userA.page, 2);
await waitForPresence(userB.page, 2);
const canvas = userA.page.locator('[data-testid="editor-canvas"]');
await canvas.hover({ position: { x: 200, y: 300 } });
await waitForRemoteCursor(userB.page, 'Alice');
const remoteCursor = userB.page.locator(
'[data-testid="remote-cursor-Alice"]'
);
const box = await remoteCursor.boundingBox();
expect(box).toBeTruthy();
expect(box!.x).toBeGreaterThan(190);
expect(box!.x).toBeLessThan(210);
});Presence Awareness and User Avatars
Straightforward4. Scenario: Presence Awareness and User Avatars
Presence awareness is the user list that shows who is currently in the document. Most collaborative editors display small avatar circles in the toolbar, often with a count indicator when many users are present. Testing this requires verifying that joining and leaving a room correctly updates the presence list on all connected clients.
With Liveblocks, presence is managed through the useOthers() hook and the useSelf() hook. With Yjs, presence uses the awareness protocol, a separate channel from the document sync that carries ephemeral state like cursor position, user name, and selection ranges. Both approaches share the same testing strategy: open multiple contexts, verify all appear in the presence list, close one, and verify it disappears.
Presence Test: Playwright vs Assrt
test('user leaving removes presence', async ({ browser, userA, roomId }) => {
const tempCtx = await browser.newContext({ storageState: { ... } });
const tempPage = await tempCtx.newPage();
await tempPage.goto(`${BASE}/doc/${roomId}`);
await waitForPresence(userA.page, 2);
await expect(userA.page.locator('text=TempUser')).toBeVisible();
await tempCtx.close();
await expect(
userA.page.locator('text=TempUser')
).toBeHidden({ timeout: 15_000 });
});Multi-Tab Sync Within a Single Session
Moderate5. Scenario: Multi-Tab Sync Within a Single Session
A subtler but important scenario: the same user opens the same document in two browser tabs. Both tabs should show the same document state, and edits in one tab should appear in the other. However, the presence system should ideally show this as one user, not two separate cursors. Some implementations handle this with a BroadcastChannel for same-origin tab sync, while others treat each tab as a separate connection and deduplicate server-side.
In Playwright, multi-tab testing within the same context uses the same cookies and storage. Creating a second page in the same context simulates a real multi-tab scenario accurately.
Concurrent Edits and CRDT Conflict Resolution
Complex6. Scenario: Concurrent Edits and CRDT Conflict Resolution
This is the hardest scenario to test correctly. When User A and User B type into the same paragraph at the same time, the CRDT layer (Yjs, Automerge, or Liveblocks Storage) must merge both edits deterministically. The result should contain both users' contributions without data loss, though the exact character order at the insertion point depends on the CRDT algorithm's conflict resolution rules.
The testing strategy is to perform edits as close to simultaneously as possible, then wait for both documents to converge, and finally assert that both clients show identical content. With Yjs, you can also verify the underlying CRDT state directly through the Y.Doc API exposed via page.evaluate().
Concurrent Edit Test: Playwright vs Assrt
test('concurrent edits merge', async ({ userA, userB }) => {
await waitForPresence(userA.page, 2);
const editorA = userA.page.locator('[data-testid="editor-content"]');
const editorB = userB.page.locator('[data-testid="editor-content"]');
await editorA.click();
await editorB.click();
await Promise.all([
userA.page.keyboard.type('ALICE_TEXT'),
userB.page.keyboard.type('BOB_TEXT'),
]);
await userA.page.waitForTimeout(2_000);
const contentA = await editorA.textContent();
const contentB = await editorB.textContent();
expect(contentA).toBe(contentB);
expect(contentA).toContain('ALICE_TEXT');
expect(contentA).toContain('BOB_TEXT');
});Disconnect, Reconnect, and Cursor State Recovery
Complex7. Scenario: Disconnect, Reconnect, and Cursor State Recovery
Real users lose connectivity all the time. A collaborative editor should handle disconnects gracefully: showing a connection status indicator, preserving local edits during the offline period, and syncing those edits once the connection is restored. Cursor presence should also recover, with remote cursors reappearing after the disconnected user reconnects.
Playwright provides two ways to simulate disconnects. The first is page.route() to block WebSocket upgrade requests. The second is the context.setOffline(true) method, which simulates full network disconnection. The route approach is more surgical, letting you block only the collaboration WebSocket while keeping HTTP requests working.
High-Latency and Lossy Network Conditions
Complex8. Scenario: High-Latency and Lossy Network Conditions
Collaborative cursors must remain usable even on slow or unreliable connections. In production, users on mobile networks or in regions with high ping times will experience delayed cursor updates. Your test suite should verify that the application degrades gracefully: cursors should still eventually appear, edits should still converge, and the UI should not break or show incorrect state.
Playwright does not natively support WebSocket throttling, but you can use Chrome DevTools Protocol (CDP) to emulate network conditions. Alternatively, route interception with artificial delays provides a simpler (though less precise) simulation.
9. Common Pitfalls That Break Collaborative Cursor Tests
Collaborative cursor tests have a unique failure profile. Most issues stem from timing assumptions, connection lifecycle misunderstandings, or environment contamination between test runs. The following pitfalls come from real GitHub issues and community discussions across the Liveblocks, Yjs, and Hocuspocus projects.
Anti-Patterns to Avoid
- Using waitForTimeout instead of waiting for presence signals. Timeouts make tests slow on fast machines and flaky on slow ones. Always wait for a DOM element or network event that confirms readiness.
- Sharing room IDs across tests. If test A and test B both use 'test-room', parallel execution causes phantom users and cursor ghosts. Generate unique room IDs per test with timestamps or random suffixes.
- Forgetting to close browser contexts in cleanup. Unclosed contexts keep WebSocket connections alive, polluting subsequent tests with stale presence entries from previous runs.
- Asserting exact pixel coordinates for cursor positions. Browser rendering, font metrics, and sub-pixel rounding differ across platforms. Use bounding box tolerance (plus or minus 5px) or relative position checks.
- Testing cursor movement speed by counting events. Mouse move events are coalesced by the browser; a smooth drag might fire 10 events or 100 depending on system load. Assert final position, not event count.
- Ignoring CSS transitions on cursor elements. If your remote cursor has a CSS transition (common for smooth movement), the element's computed position mid-animation will not match the target. Disable transitions in test mode or wait for transitionend.
- Running more than 3 browser contexts in a single test on CI. Each context consumes memory and CPU. CI runners with 2 cores will timeout when 5 contexts compete for resources. Keep multi-user tests to 2 or 3 contexts.
- Not accounting for Yjs awareness timeout. Yjs removes awareness state after 30 seconds of no updates by default. Long-running tests that pause between actions will see cursors vanish unexpectedly. Set the awareness timeout higher in test config.
Flaky Test Debugging Checklist
When a Collaboration Test Fails Intermittently
- Check if room IDs are unique per test run (stale room data causes ghost presence)
- Verify WebSocket connections are fully established before performing actions
- Confirm the collaboration server is running and accessible from the test environment
- Look for CSS transition or animation timing affecting element position assertions
- Check CI resource limits: memory pressure causes WebSocket connections to drop
- Review Yjs awareness timeout settings if cursors disappear mid-test
- Ensure test cleanup closes all contexts and connections before the next test starts
10. Writing These Scenarios in Plain English with Assrt
The Playwright code above is powerful but dense. Each scenario involves multi-context setup, fixture wiring, presence waiting, coordinate assertions, and cleanup. Assrt lets you express these same scenarios as structured plain-English files that compile into the same Playwright TypeScript. The collaboration-specific parts (multiple users, presence sync, concurrent actions) map naturally to Assrt's multi-user scenario syntax.
Assrt compiles each scenario block into the same Playwright TypeScript shown in the preceding sections. The multi-user fixture, the presence waiting, the coordinate tolerance, and the cleanup hooks are all generated automatically. When your collaboration provider updates its API or changes DOM selectors, Assrt detects the failure, analyzes the new structure, and opens a pull request with the updated code. Your scenario files remain untouched.
Start with the cursor broadcast scenario. Once it passes in your CI, add the presence awareness test, then concurrent edits, then disconnect recovery, then latency simulation. In a single afternoon you can build comprehensive collaborative cursor coverage that most teams never achieve because the multi-context test setup is too tedious to maintain by hand.
Related Guides
How to Test Ably Realtime
A practical guide to testing Ably Realtime messaging with Playwright. Covers token auth...
How to Test AI Chat Streaming UI
A practical guide to testing AI chat streaming interfaces with Playwright. Covers...
How to Test Intercom Messenger
A practical guide to testing Intercom Messenger with Playwright. Covers iframe traversal,...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.