Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'test: add E2E collaborative editing sync tests (#564)' (#344) from test/collab-e2e into main

scott 0907fa9d 3107b8b3

+166
+166
e2e/collab-sync.spec.ts
··· 1 + /** 2 + * E2E tests for collaborative editing sync between two browser contexts. 3 + * 4 + * Tests verify that two users editing the same document via Yjs 5 + * see each other's changes in real-time (#564). 6 + */ 7 + import { test, expect, type Page, type Browser, type BrowserContext } from '@playwright/test'; 8 + import { createNewDoc, createNewSheet } from './helpers'; 9 + 10 + /** 11 + * Open a second browser context with the same document URL. 12 + * Sets a different username so we can distinguish the two "users". 13 + */ 14 + async function openSecondContext( 15 + browser: Browser, 16 + url: string, 17 + username: string, 18 + ): Promise<{ context: BrowserContext; page: Page }> { 19 + const context = await browser.newContext(); 20 + const page = await context.newPage(); 21 + await page.addInitScript((name: string) => { 22 + localStorage.setItem('tools-username', name); 23 + }, username); 24 + await page.goto(url); 25 + return { context, page }; 26 + } 27 + 28 + test.describe('Collaborative Editing Sync', () => { 29 + test('docs: two users see each other\'s edits in real-time', async ({ page, browser }) => { 30 + // User A creates a document 31 + const url = await createNewDoc(page); 32 + 33 + // User B opens the same document 34 + const { context: ctxB, page: pageB } = await openSecondContext(browser, url, 'UserB'); 35 + await pageB.waitForSelector('.tiptap', { timeout: 15000 }); 36 + 37 + // User A types content 38 + const editorA = page.locator('.tiptap'); 39 + await editorA.click(); 40 + await page.keyboard.type('Hello from User A'); 41 + 42 + // User B should see User A's text appear 43 + const editorB = pageB.locator('.tiptap'); 44 + await expect(editorB).toContainText('Hello from User A', { timeout: 10000 }); 45 + 46 + // User B types additional content 47 + await editorB.click(); 48 + // Move to end of content 49 + await pageB.keyboard.press('End'); 50 + await pageB.keyboard.type(' and hello from User B'); 51 + 52 + // User A should see the combined text 53 + await expect(editorA).toContainText('User B', { timeout: 10000 }); 54 + 55 + await ctxB.close(); 56 + }); 57 + 58 + test('docs: concurrent edits merge without data loss', async ({ page, browser }) => { 59 + const url = await createNewDoc(page); 60 + 61 + const { context: ctxB, page: pageB } = await openSecondContext(browser, url, 'UserB'); 62 + await pageB.waitForSelector('.tiptap', { timeout: 15000 }); 63 + 64 + // User A types a line 65 + const editorA = page.locator('.tiptap'); 66 + await editorA.click(); 67 + await page.keyboard.type('Line from A'); 68 + await page.keyboard.press('Enter'); 69 + 70 + // Wait for sync 71 + await expect(pageB.locator('.tiptap')).toContainText('Line from A', { timeout: 10000 }); 72 + 73 + // Both users type simultaneously 74 + await page.keyboard.type('Second line A'); 75 + 76 + const editorB = pageB.locator('.tiptap'); 77 + await editorB.click(); 78 + await pageB.keyboard.press('End'); 79 + await pageB.keyboard.press('Enter'); 80 + await pageB.keyboard.type('Line from B'); 81 + 82 + // Both pages should eventually contain both lines 83 + await expect(editorA).toContainText('Line from A', { timeout: 10000 }); 84 + await expect(editorA).toContainText('Line from B', { timeout: 10000 }); 85 + await expect(editorB).toContainText('Second line A', { timeout: 10000 }); 86 + await expect(editorB).toContainText('Line from B', { timeout: 10000 }); 87 + 88 + await ctxB.close(); 89 + }); 90 + 91 + test('sheets: two users see each other\'s cell edits', async ({ page, browser }) => { 92 + const url = await createNewSheet(page); 93 + 94 + const { context: ctxB, page: pageB } = await openSecondContext(browser, url, 'UserB'); 95 + await pageB.waitForSelector('#sheet-grid tbody tr td[data-id]', { timeout: 15000 }); 96 + 97 + // User A edits cell A1 98 + await page.locator('td[data-id="A1"]').click(); 99 + await page.keyboard.type('From A'); 100 + await page.keyboard.press('Enter'); 101 + 102 + // User B should see A1 content 103 + const cellA1B = pageB.locator('td[data-id="A1"] .cell-display'); 104 + await expect(cellA1B).toContainText('From A', { timeout: 10000 }); 105 + 106 + // User B edits cell B1 107 + await pageB.locator('td[data-id="B1"]').click(); 108 + await pageB.keyboard.type('From B'); 109 + await pageB.keyboard.press('Enter'); 110 + 111 + // User A should see B1 content 112 + const cellB1A = page.locator('td[data-id="B1"] .cell-display'); 113 + await expect(cellB1A).toContainText('From B', { timeout: 10000 }); 114 + 115 + await ctxB.close(); 116 + }); 117 + 118 + test('sheets: concurrent cell edits to different cells both persist', async ({ page, browser }) => { 119 + const url = await createNewSheet(page); 120 + 121 + const { context: ctxB, page: pageB } = await openSecondContext(browser, url, 'UserB'); 122 + await pageB.waitForSelector('#sheet-grid tbody tr td[data-id]', { timeout: 15000 }); 123 + 124 + // Both users edit different cells simultaneously 125 + await page.locator('td[data-id="A1"]').click(); 126 + await page.keyboard.type('100'); 127 + await page.keyboard.press('Tab'); 128 + await page.keyboard.type('200'); 129 + await page.keyboard.press('Enter'); 130 + 131 + await pageB.locator('td[data-id="A2"]').click(); 132 + await pageB.keyboard.type('300'); 133 + await pageB.keyboard.press('Tab'); 134 + await pageB.keyboard.type('400'); 135 + await pageB.keyboard.press('Enter'); 136 + 137 + // Both pages should see all four values 138 + await expect(page.locator('td[data-id="A1"] .cell-display')).toContainText('100', { timeout: 10000 }); 139 + await expect(page.locator('td[data-id="B1"] .cell-display')).toContainText('200', { timeout: 10000 }); 140 + await expect(page.locator('td[data-id="A2"] .cell-display')).toContainText('300', { timeout: 10000 }); 141 + await expect(page.locator('td[data-id="B2"] .cell-display')).toContainText('400', { timeout: 10000 }); 142 + 143 + await expect(pageB.locator('td[data-id="A1"] .cell-display')).toContainText('100', { timeout: 10000 }); 144 + await expect(pageB.locator('td[data-id="B1"] .cell-display')).toContainText('200', { timeout: 10000 }); 145 + 146 + await ctxB.close(); 147 + }); 148 + 149 + test('docs: save status shows Saved on both clients after sync', async ({ page, browser }) => { 150 + const url = await createNewDoc(page); 151 + 152 + const { context: ctxB, page: pageB } = await openSecondContext(browser, url, 'UserB'); 153 + await pageB.waitForSelector('.tiptap', { timeout: 15000 }); 154 + 155 + // User A types and waits for save 156 + const editorA = page.locator('.tiptap'); 157 + await editorA.click(); 158 + await page.keyboard.type('Checking save status'); 159 + 160 + // Both should show Saved eventually 161 + await expect(page.locator('#save-text')).toHaveText('Saved', { timeout: 15000 }); 162 + await expect(pageB.locator('#save-text')).toHaveText('Saved', { timeout: 15000 }); 163 + 164 + await ctxB.close(); 165 + }); 166 + });