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.

10M+

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

0msTypical cursor broadcast latency
0Collaboration scenarios covered
0+Browser contexts per test
0%Fewer lines with Assrt

Collaborative Cursor Sync Flow

User A BrowserWebSocket ServerCRDT ProviderUser B BrowserPresence StoreConnect to roomConnect to roomInitialize presenceCursor move {x:120, y:340}Broadcast presence updateDeliver cursor positionCursor move {x:400, y:200}Deliver cursor position

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

.env.test

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.

test/fixtures/collaboration.ts

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.

test/helpers/wait-for-presence.ts
Installing Dependencies
3

Cursor Position Broadcasting Between Two Users

Moderate

3. 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.

test/scenarios/cursor-broadcast.spec.ts

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);
});
38% fewer lines
4

Presence Awareness and User Avatars

Straightforward

4. 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.

test/scenarios/presence-awareness.spec.ts

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 });
});
30% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
5

Multi-Tab Sync Within a Single Session

Moderate

5. 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.

test/scenarios/multi-tab-sync.spec.ts
6

Concurrent Edits and CRDT Conflict Resolution

Complex

6. 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().

test/scenarios/concurrent-edits.spec.ts
Concurrent Edit Test Output

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');
});
41% fewer lines
7

Disconnect, Reconnect, and Cursor State Recovery

Complex

7. 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.

test/scenarios/disconnect-reconnect.spec.ts
8

High-Latency and Lossy Network Conditions

Complex

8. 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.

test/scenarios/latency-simulation.spec.ts
Network Simulation Test Run

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.

tests/collaborative-cursors.assrt

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

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk