Skip to content

Commit

Permalink
test: add basic loro => pm sync tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bellini666 committed Mar 31, 2024
1 parent 0829b92 commit ee28466
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 28 deletions.
42 changes: 28 additions & 14 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { simpleDiff } from "lib0/diff";
import {
Delta,
Loro,
LoroEventBatch,
LoroList,
LoroMap,
LoroText,
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
);
Expand All @@ -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());
}
Expand All @@ -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;
Expand All @@ -429,10 +429,7 @@ export function updateLoroMapAttributes(
}
}

export function getLoroMapChildren(
obj: LoroMap,
mapping: LoroNodeMapping,
): LoroList<LoroType[]> {
export function getLoroMapChildren(obj: LoroMap): LoroList<LoroType[]> {
return obj.getOrCreateContainer(CHILDREN_KEY, new LoroList());
}

Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
}
}
19 changes: 7 additions & 12 deletions src/sync-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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"),
Expand Down
169 changes: 168 additions & 1 deletion tests/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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" }],
},
],
},
],
},
],
});
});
});
2 changes: 1 addition & 1 deletion tests/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" } },
Expand Down
26 changes: 26 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<void> {
return new Promise((r) => setTimeout(r));
}

0 comments on commit ee28466

Please sign in to comment.