From ee28466865db4ef4c2bd0c2150ce7f1bbf879181 Mon Sep 17 00:00:00 2001 From: Thiago Bellini Ribeiro Date: Sun, 31 Mar 2024 13:21:12 -0300 Subject: [PATCH] test: add basic loro => pm sync tests --- src/lib.ts | 42 +++++++---- src/sync-plugin.ts | 19 ++--- tests/basic.test.ts | 169 +++++++++++++++++++++++++++++++++++++++++++- tests/schema.ts | 2 +- tests/utils.ts | 26 +++++++ 5 files changed, 230 insertions(+), 28 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index 1ef5e45..1fa1659 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -2,6 +2,7 @@ import { simpleDiff } from "lib0/diff"; import { Delta, Loro, + LoroEventBatch, LoroList, LoroMap, LoroText, @@ -61,8 +62,8 @@ export function createNodeFromLoroObj( } if (obj instanceof LoroMap) { - const attributes = getLoroMapAttributes(obj, mapping); - const children = getLoroMapChildren(obj, mapping); + const attributes = getLoroMapAttributes(obj); + const children = getLoroMapChildren(obj); const nodeName = obj.get("nodeName"); if (nodeName == null || typeof nodeName !== "string") { @@ -215,11 +216,11 @@ function eqLoroObjNode( return false; } - const loroChildren = getLoroMapChildren(obj, mapping); + const loroChildren = getLoroMapChildren(obj); const normalizedContent = normalizeNodeContent(node); return ( loroChildren.length === normalizedContent.length && - eqAttrs(getLoroMapAttributes(obj, mapping).toJson(), node.attrs) && + eqAttrs(getLoroMapAttributes(obj).toJson(), node.attrs) && normalizedContent.every((childNode, i) => eqLoroObjNode(loroChildren.get(i)!, childNode, mapping), ) @@ -296,7 +297,7 @@ function computeChildEqualityFactor( factor: number; foundMappedChild: boolean; } { - const loroChildren = getLoroMapChildren(obj, mapping); + const loroChildren = getLoroMapChildren(obj); const loroChildLength = loroChildren.length; const nodeChildren = normalizeNodeContent(node); @@ -361,14 +362,14 @@ export function createLoroMap( obj.set("nodeName", node.type.name); - const attrs = getLoroMapAttributes(obj, mapping); + const attrs = getLoroMapAttributes(obj); for (const [key, value] of Object.entries(node.attrs)) { if (value !== null) { attrs.set(key, value); } } - const children = getLoroMapChildren(obj, mapping); + const children = getLoroMapChildren(obj); normalizeNodeContent(node).forEach((child, i) => createLoroChild(children, null, child, mapping), ); @@ -394,7 +395,6 @@ export function updateLoroMap( export function getLoroMapAttributes( obj: LoroMap, - mapping: LoroNodeMapping, ): LoroMap<{ [key: string]: string }> { return obj.getOrCreateContainer(ATTRIBUTES_KEY, new LoroMap()); } @@ -404,7 +404,7 @@ export function updateLoroMapAttributes( node: Node, mapping: LoroNodeMapping, ): void { - const attrs = getLoroMapAttributes(obj, mapping); + const attrs = getLoroMapAttributes(obj); const keys = new Set(attrs.keys()); const pAttrs = node.attrs; @@ -429,10 +429,7 @@ export function updateLoroMapAttributes( } } -export function getLoroMapChildren( - obj: LoroMap, - mapping: LoroNodeMapping, -): LoroList { +export function getLoroMapChildren(obj: LoroMap): LoroList { return obj.getOrCreateContainer(CHILDREN_KEY, new LoroList()); } @@ -441,7 +438,7 @@ export function updateLoroMapChildren( node: Node, mapping: LoroNodeMapping, ): void { - const loroChildren = getLoroMapChildren(obj, mapping); + const loroChildren = getLoroMapChildren(obj); const loroChildLength = loroChildren.length; const nodeChildren = normalizeNodeContent(node); @@ -584,3 +581,20 @@ export function updateLoroMapChildren( ); } } + +export function clearChangedNodes( + doc: Loro, + event: LoroEventBatch, + mapping: LoroNodeMapping, +) { + for (const e of event.events) { + const obj = doc.getContainerById(e.target); + mapping.delete(obj); + + let parentObj = obj.parent(); + while (parentObj != null) { + mapping.delete(parentObj); + parentObj = parentObj.parent(); + } + } +} diff --git a/src/sync-plugin.ts b/src/sync-plugin.ts index c9e69fb..2191992 100644 --- a/src/sync-plugin.ts +++ b/src/sync-plugin.ts @@ -8,7 +8,12 @@ import { } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Slice, Fragment } from "prosemirror-model"; -import { LoroNodeMapping, createNodeFromLoroObj, updateDoc } from "./lib"; +import { + LoroNodeMapping, + clearChangedNodes, + createNodeFromLoroObj, + updateDoc, +} from "./lib"; export const loroSyncPluginKey = new PluginKey("loro-sync"); @@ -117,17 +122,7 @@ function update(view: EditorView, event: LoroEventBatch) { const state = loroSyncPluginKey.getState(view.state) as LoroSyncPluginState; const mapping = state.mapping; - for (const e of event.events) { - const obj = state.doc.getContainerById(e.target); - mapping.delete(obj); - - let parentObj = obj.parent(); - while (parentObj != null) { - mapping.delete(parentObj); - parentObj = parentObj.parent(); - } - } - + clearChangedNodes(state.doc, event, mapping); const node = createNodeFromLoroObj( view.state.schema, state.doc.getMap("doc"), diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 38e81c6..4ba15f1 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -7,12 +7,21 @@ import { Loro, LoroText } from "loro-crdt"; import { LoroNodeMapping, ROOT_DOC_KEY, + clearChangedNodes, createNodeFromLoroObj, + getLoroMapAttributes, + getLoroMapChildren, updateDoc, } from "../src"; import { schema } from "./schema"; -import { createEditorState } from "./utils"; +import { + createEditorState, + insertLoroMap, + insertLoroText, + setupLoroMap, + oneMs, +} from "./utils"; const examplePmContent = { doc: { @@ -395,4 +404,162 @@ describe("createNodeFromLoroObj", () => { const editorState = createEditorState(schema, examplePmContent.doc); expect(editorState.toJSON()).toEqual(examplePmContent); }); + + test("node syncs changes correctly", async () => { + const loroDoc = new Loro(); + const mapping: LoroNodeMapping = new Map(); + + loroDoc.subscribe((event) => clearChangedNodes(loroDoc, event, mapping)); + + // First we create an empty content + const loroInnerDoc = loroDoc.getMap(ROOT_DOC_KEY); + setupLoroMap(loroInnerDoc, ROOT_DOC_KEY); + + let node = createNodeFromLoroObj(schema, loroInnerDoc, mapping); + expect(node.toJSON()).toEqual({ + type: "doc", + }); + + // Now lets add a paragraph + const loroParagraph = new LoroText(); + const p1 = insertLoroMap(getLoroMapChildren(loroInnerDoc), "paragraph"); + const p1Text = insertLoroText(getLoroMapChildren(p1)); + p1Text.insert(0, "Hello world!"); + + loroDoc.commit(); + await oneMs(); + // FIXME: Why the subscription is not triggering? + mapping.clear(); + + node = createNodeFromLoroObj(schema, loroInnerDoc, mapping); + expect(node.toJSON()).toEqual({ + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Hello world!" }], + }, + ], + }); + + // Lets make "world" bold + p1Text.mark({ start: 6, end: 11 }, "bold", true); + + loroDoc.commit(); + await oneMs(); + // FIXME: Why the subscription is not triggering? + mapping.clear(); + + node = createNodeFromLoroObj(schema, loroInnerDoc, mapping); + expect(node.toJSON()).toEqual({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Hello " }, + { type: "text", marks: [{ type: "bold" }], text: "world" }, + { type: "text", text: "!" }, + ], + }, + ], + }); + + // Add a second paragraph + const p2 = insertLoroMap(getLoroMapChildren(loroInnerDoc), "paragraph"); + const p2Text = insertLoroText(getLoroMapChildren(p2)); + p2Text.insert(0, "Second paragraph"); + + loroDoc.commit(); + await oneMs(); + // FIXME: Why the subscription is not triggering? + mapping.clear(); + + node = createNodeFromLoroObj(schema, loroInnerDoc, mapping); + expect(node.toJSON()).toEqual({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Hello " }, + { type: "text", marks: [{ type: "bold" }], text: "world" }, + { type: "text", text: "!" }, + ], + }, + { + type: "paragraph", + content: [{ type: "text", text: "Second paragraph" }], + }, + ], + }); + + // Now lets add a bullet list + const bulletList = insertLoroMap( + getLoroMapChildren(loroInnerDoc), + "bulletList", + ); + const bullet1 = insertLoroMap(getLoroMapChildren(bulletList), "listItem"); + const bullet1Paragraph = insertLoroMap( + getLoroMapChildren(bullet1), + "paragraph", + ); + const bullet1Text = insertLoroText(getLoroMapChildren(bullet1Paragraph)); + bullet1Text.insert(0, "Bullet 1"); + + const bullet2 = insertLoroMap(getLoroMapChildren(bulletList), "listItem"); + const bullet2Paragraph = insertLoroMap( + getLoroMapChildren(bullet2), + "paragraph", + ); + const bullet2Text = insertLoroText(getLoroMapChildren(bullet2Paragraph)); + bullet2Text.insert(0, "Bullet 2"); + + loroDoc.commit(); + await oneMs(); + // FIXME: Why the subscription is not triggering? + mapping.clear(); + + node = createNodeFromLoroObj(schema, loroInnerDoc, mapping); + expect(node.toJSON()).toEqual({ + type: "doc", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Hello " }, + { type: "text", marks: [{ type: "bold" }], text: "world" }, + { type: "text", text: "!" }, + ], + }, + { + type: "paragraph", + content: [{ type: "text", text: "Second paragraph" }], + }, + { + type: "bulletList", + content: [ + { + type: "listItem", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Bullet 1" }], + }, + ], + }, + { + type: "listItem", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Bullet 2" }], + }, + ], + }, + ], + }, + ], + }); + }); }); diff --git a/tests/schema.ts b/tests/schema.ts index 2ebfc45..5afe025 100644 --- a/tests/schema.ts +++ b/tests/schema.ts @@ -2,7 +2,7 @@ import { Schema, NodeSpec, MarkSpec } from "prosemirror-model"; const nodes: { [key: string]: NodeSpec } = { doc: { - content: "block+", + content: "block*", }, noteTitle: { attrs: { emoji: { default: "" } }, diff --git a/tests/utils.ts b/tests/utils.ts index 56199ff..5db3ef8 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,5 +1,11 @@ import { Schema } from "prosemirror-model"; import { EditorState } from "prosemirror-state"; +import { + LoroNodeMapping, + getLoroMapAttributes, + getLoroMapChildren, +} from "../src"; +import { Loro, LoroList, LoroMap, LoroText } from "loro-crdt"; export function createEditorState(schema: Schema, content: any): EditorState { const doc = schema.nodeFromJSON(content); @@ -8,3 +14,23 @@ export function createEditorState(schema: Schema, content: any): EditorState { schema, }); } + +export function insertLoroText(parent: LoroList): LoroText { + return parent.insertContainer(parent.length, new LoroText()); +} + +export function insertLoroMap(parent: LoroList, nodeName: string): LoroMap { + const obj = parent.insertContainer(parent.length, new LoroMap()); + setupLoroMap(obj, nodeName); + return obj; +} + +export function setupLoroMap(obj: LoroMap, nodeName: string): void { + obj.set("nodeName", nodeName); + getLoroMapChildren(obj); + getLoroMapAttributes(obj); +} + +export function oneMs(): Promise { + return new Promise((r) => setTimeout(r)); +}