import { describe, expect, it } from "vitest"; import { Delta, getType, ListDiff, Loro, LoroEventBatch, LoroList, LoroMap, LoroText, MapDiff, TextDiff, } from "../src"; describe("event", () => { it("target", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { expect(event.by).toBe("local"); lastEvent = event; }); const text = loro.getText("text"); const id = text.id; text.insert(0, "123"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].target).toEqual(id); }); it("path", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { lastEvent = event; }); const map = loro.getMap("map"); const subMap = map.setContainer("sub", new LoroMap()); subMap.set("0", "1"); loro.commit(); await oneMs(); expect(lastEvent?.events[1].path).toStrictEqual(["map", "sub"]); const list = subMap.setContainer("list", new LoroList()); list.insert(0, "2"); const text = list.insertContainer(1, new LoroText()); loro.commit(); await oneMs(); text.insert(0, "3"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].path).toStrictEqual(["map", "sub", "list", 1]); }); it("text diff", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { lastEvent = event; }); const text = loro.getText("t"); text.insert(0, "3"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "text", diff: [{ insert: "3" }], } as TextDiff); text.insert(1, "12"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "text", diff: [{ retain: 1 }, { insert: "12" }], } as TextDiff); }); it("list diff", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { lastEvent = event; }); const text = loro.getList("l"); text.insert(0, "3"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "list", diff: [{ insert: ["3"] }], } as ListDiff); text.insert(1, "12"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "list", diff: [{ retain: 1 }, { insert: ["12"] }], } as ListDiff); }); it("map diff", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { lastEvent = event; }); const map = loro.getMap("m"); map.set("0", "3"); map.set("1", "2"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "map", updated: { "0": "3", "1": "2", }, } as MapDiff); map.set("0", "0"); map.set("1", "1"); loro.commit(); await oneMs(); expect(lastEvent?.events[0].diff).toStrictEqual({ type: "map", updated: { "0": "0", "1": "1", }, } as MapDiff); }); it("tree", async () => { const loro = new Loro(); let lastEvent: undefined | LoroEventBatch; loro.subscribe((event) => { lastEvent = event; }); const tree = loro.getTree("tree"); const id = tree.id; tree.createNode(); loro.commit(); await oneMs(); expect(lastEvent?.events[0].target).toEqual(id); }); describe("subscribe container events", () => { it("text", async () => { const loro = new Loro(); const text = loro.getText("text"); let ran = 0; const sub = text.subscribe((event) => { if (!ran) { expect((event.events[0].diff as any).diff).toStrictEqual([ { insert: "123" }, ] as Delta[]); } ran += 1; for (const containerDiff of event.events) { expect(containerDiff.target).toBe(text.id); } }); text.insert(0, "123"); loro.commit(); await oneMs(); text.insert(1, "456"); loro.commit(); await oneMs(); expect(ran).toBeTruthy(); // subscribeOnce test expect(text.toString()).toEqual("145623"); // unsubscribe const oldRan = ran; text.unsubscribe(sub); text.insert(0, "789"); loro.commit(); await oneMs(); expect(ran).toBe(oldRan); }); it("map subscribe deep", async () => { const loro = new Loro(); const map = loro.getMap("map"); let times = 0; const sub = map.subscribe((event) => { times += 1; }); const subMap = map.setContainer("sub", new LoroMap()); loro.commit(); await oneMs(); expect(times).toBe(1); const text = subMap.setContainer("k", new LoroText()); loro.commit(); await oneMs(); expect(times).toBe(2); text.insert(0, "123"); loro.commit(); await oneMs(); expect(times).toBe(3); // unsubscribe loro.unsubscribe(sub); text.insert(0, "123"); loro.commit(); await oneMs(); expect(times).toBe(3); }); it("list subscribe deep", async () => { const loro = new Loro(); const list = loro.getList("list"); let times = 0; const sub = list.subscribe((event) => { times += 1; }); const text = list.insertContainer(0, new LoroText()); loro.commit(); await oneMs(); expect(times).toBe(1); text.insert(0, "123"); await oneMs(); loro.commit(); await oneMs(); expect(times).toBe(2); // unsubscribe loro.unsubscribe(sub); text.insert(0, "123"); loro.commit(); await oneMs(); expect(times).toBe(2); }); }); describe("text event length should be utf16", () => { it("test", async () => { const loro = new Loro(); const text = loro.getText("text"); let string = ""; text.subscribe((event) => { for (const containerDiff of event.events) { const diff = containerDiff.diff; expect(diff.type).toBe("text"); if (diff.type === "text") { let newString = ""; let pos = 0; for (const delta of diff.diff) { if (delta.retain != null) { newString += string.slice(pos, pos + delta.retain); pos += delta.retain; } else if (delta.insert != null) { newString += delta.insert; } else { pos += delta.delete; } } string = newString + string.slice(pos); } } }); text.insert(0, "δ½ ε₯½"); loro.commit(); await oneMs(); expect(text.toString()).toBe(string); text.insert(1, "δΈ–η•Œ"); loro.commit(); await oneMs(); expect(text.toString()).toBe(string); text.insert(2, "πŸ‘"); loro.commit(); await oneMs(); expect(text.toString()).toBe(string); text.insert(2, "β™ͺ(^βˆ‡^*)"); loro.commit(); await oneMs(); expect(text.toString()).toBe(string); }); }); describe("handler in event", () => { it("test", async () => { const loro = new Loro(); const list = loro.getList("list"); let first = true; loro.subscribe((e) => { if (first) { const diff = (e.events[0].diff as ListDiff).diff; const text = diff[0].insert![0] as LoroText; text.insert(0, "abc"); first = false; } }); list.insertContainer(0, new LoroText()); loro.commit(); await oneMs(); expect(loro.toJSON().list[0]).toBe("abc"); }); }); it("diff can contain containers", async () => { const doc = new Loro(); const list = doc.getList("list"); let ran = false; doc.subscribe((event) => { if (event.events[0].diff.type === "list") { for (const item of event.events[0].diff.diff) { const t = item.insert![0] as LoroText; expect(t.toString()).toBe("Hello"); expect(item.insert?.length).toBe(2); expect(getType(item.insert![0])).toBe("Text"); expect(getType(item.insert![1])).toBe("Map"); } ran = true; } }); list.insertContainer(0, new LoroMap()); const t = list.insertContainer(0, new LoroText()); t.insert(0, "He"); t.insert(2, "llo"); doc.commit(); await new Promise((resolve) => setTimeout(resolve, 1)); expect(ran).toBeTruthy(); }); it("remote event", async () => { const doc = new Loro(); const list = doc.getList("list"); list.insert(0, 123); { const doc2 = new Loro(); let triggered = false; doc2.subscribe((event) => { expect(event.by).toBe("import"); triggered = true; }); doc2.import(doc.exportFrom()); await oneMs(); expect(triggered).toBeTruthy(); } { const doc2 = new Loro(); let triggered = false; doc2.subscribe((event) => { expect(event.by).toBe("import"); triggered = true; }); doc2.import(doc.exportSnapshot()); await oneMs(); expect(triggered).toBeTruthy(); } }); it("checkout event", async () => { const doc = new Loro(); const list = doc.getList("list"); list.insert(0, 123); doc.commit(); let triggered = false; doc.subscribe((e) => { expect(e.by).toBe("checkout"); triggered = true; }); doc.checkout([]); await oneMs(); expect(triggered).toBeTruthy(); }); }); function oneMs(): Promise { return new Promise((r) => setTimeout(r)); }