/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import {$createTextNode, $getRoot, ParagraphNode, TextNode} from 'lexical'; import {Client, createTestConnection, waitForReact} from './utils'; describe('Collaboration', () => { let container: null | HTMLDivElement = null; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container!); container = null; }); async function expectCorrectInitialContent(client1: Client, client2: Client) { // Should be empty, as client has not yet updated expect(client1.getHTML()).toEqual(''); expect(client1.getHTML()).toEqual(client2.getHTML()); // Wait for clients to render the initial content await Promise.resolve().then(); expect(client1.getHTML()).toEqual('
Hello world
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); // Insert some text on client 2 await waitForReact(() => { client2.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello metaverse
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getDocJSON()).toEqual({ root: '[object Object]Hello metaverse', }); expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); client1.stop(); client2.stop(); }); it('Should collaborate basic text insertion conflicts between two clients', async () => { const connector = createTestConnection(); const client1 = connector.createClient('1'); const client2 = connector.createClient('2'); client1.start(container!); client2.start(container!); await expectCorrectInitialContent(client1, client2); client1.disconnect(); // Insert some a text node on client 1 await waitForReact(() => { client1.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello world
', ); expect(client2.getHTML()).toEqual('Hello world
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); await waitForReact(() => { client1.connect(); }); // Text content should be repeated, but there should only be a single node expect(client1.getHTML()).toEqual( 'Hello worldHello world
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getDocJSON()).toEqual({ root: '[object Object]Hello worldHello world', }); expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); client2.disconnect(); await waitForReact(() => { client1.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello world
', ); expect(client2.getHTML()).toEqual( 'Hello worldHello world
', ); await waitForReact(() => { client2.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello world!
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getDocJSON()).toEqual({ root: '[object Object]Hello world!', }); expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); client1.stop(); client2.stop(); }); it('Should collaborate basic text deletion conflicts between two clients', async () => { const connector = createTestConnection(); const client1 = connector.createClient('1'); const client2 = connector.createClient('2'); client1.start(container!); client2.start(container!); await expectCorrectInitialContent(client1, client2); // Insert some a text node on client 1 await waitForReact(() => { client1.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello world
', ); expect(client1.getHTML()).toEqual(client2.getHTML()); expect(client1.getDocJSON()).toEqual({ root: '[object Object]Hello world', }); expect(client1.getDocJSON()).toEqual(client2.getDocJSON()); client1.disconnect(); // Delete the text on client 1 await waitForReact(() => { client1.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello world
', ); // Insert some text on client 2 await waitForReact(() => { client2.update(() => { const root = $getRoot(); const paragraph = root.getFirstChildHello worldHello world
', ); await waitForReact(() => { client1.connect(); }); // TODO we can probably handle these conflicts better. We could keep around // a "fallback" {Map} when we remove text without any adjacent text nodes. This // would require big changes in `CollabElementNode.splice` and also need adjustments // in `CollabElementNode.applyChildrenYjsDelta` to handle the existence of these // fallback maps. For now though, if a user clears all text nodes from an element // and another user inserts some text into the same element at the same time, the // deletion will take precedence on conflicts. expect(client1.getHTML()).toEqual('