import { describe, expect, expectTypeOf, it } from "vitest"; import { getType, isContainer, Loro, LoroList, LoroMap, VersionVector, } from "../src"; import { Container } from "../dist/loro"; it("basic example", () => { const doc = new Loro(); const list: LoroList = doc.getList("list"); list.insert(0, "A"); list.insert(1, "B"); list.insert(2, "C"); const map: LoroMap = doc.getMap("map"); // map can only has string key map.set("key", "value"); expect(doc.toJson()).toStrictEqual({ list: ["A", "B", "C"], map: { key: "value" }, }); // delete 2 elements at index 0 list.delete(0, 2); expect(doc.toJson()).toStrictEqual({ list: ["C"], map: { key: "value" }, }); // Insert a text container to the list const text = list.insertContainer(0, "Text"); text.insert(0, "Hello"); text.insert(0, "Hi! "); // delete 1 element at index 0 expect(doc.toJson()).toStrictEqual({ list: ["Hi! Hello", "C"], map: { key: "value" }, }); // Insert a list container to the map const list2 = map.setContainer("test", "List"); list2.insert(0, 1); expect(doc.toJson()).toStrictEqual({ list: ["Hi! Hello", "C"], map: { key: "value", test: [1] }, }); }); it("basic sync example", () => { const docA = new Loro(); const docB = new Loro(); const listA: LoroList = docA.getList("list"); listA.insert(0, "A"); listA.insert(1, "B"); listA.insert(2, "C"); // B import the ops from A docB.import(docA.exportFrom()); expect(docB.toJson()).toStrictEqual({ list: ["A", "B", "C"], }); const listB: LoroList = docB.getList("list"); // delete 1 element at index 1 listB.delete(1, 1); // A import the ops from B docA.import(docB.exportFrom(docA.version())); // list at A is now ["A", "C"], with the same state as B expect(docA.toJson()).toStrictEqual({ list: ["A", "C"], }); expect(docA.toJson()).toStrictEqual(docB.toJson()); }); it("basic events", () => { const doc = new Loro(); doc.subscribe((event) => {}); const list = doc.getList("list"); }); describe("list", () => { it("insert containers", () => { const doc = new Loro(); const list = doc.getList("list"); const map = list.insertContainer(0, "Map"); map.set("key", "value"); const v = list.get(0) as LoroMap; console.log(v); expect(v instanceof LoroMap).toBeTruthy(); expect(v.toJson()).toStrictEqual({ key: "value" }); }); it("toArray", () => { const doc = new Loro(); const list = doc.getList("list"); list.insert(0, 1); list.insert(1, 2); expect(list.toArray()).toStrictEqual([1, 2]); list.insertContainer(2, "Text"); const t = list.toArray()[2]; expect(isContainer(t)).toBeTruthy(); expect(getType(t)).toBe("Text"); expect(getType(123)).toBe("Json"); }); }); describe("map", () => { it("get child container", () => { const doc = new Loro(); const map = doc.getMap("map"); const list = map.setContainer("key", "List"); list.insert(0, 1); expect(map.get("key") instanceof LoroList).toBeTruthy(); expect((map.get("key") as LoroList).toJson()).toStrictEqual([1]); }); it("set large int", () => { const doc = new Loro(); const map = doc.getMap("map"); map.set("key", 2147483699); expect(map.get("key")).toBe(2147483699); }); }); describe("import", () => { it("pending", () => { const a = new Loro(); a.getText("text").insert(0, "a"); const b = new Loro(); b.import(a.exportFrom()); b.getText("text").insert(1, "b"); const c = new Loro(); c.import(b.exportFrom()); c.getText("text").insert(2, "c"); // c export from b's version, which cannot be imported directly to a. // This operation is pending. a.import(c.exportFrom(b.version())); expect(a.getText("text").toString()).toBe("a"); // a import the missing ops from b. It makes the pending operation from c valid. a.import(b.exportFrom(a.version())); expect(a.getText("text").toString()).toBe("abc"); }); it("import by frontiers", () => { const a = new Loro(); a.getText("text").insert(0, "a"); const b = new Loro(); b.import(a.exportFrom()); b.getText("text").insert(1, "b"); b.getList("list").insert(0, [1, 2]); const updates = b.exportFrom(b.frontiersToVV(a.frontiers())); a.import(updates); expect(a.toJson()).toStrictEqual(b.toJson()); }); it("from snapshot", () => { const a = new Loro(); a.getText("text").insert(0, "hello"); const bytes = a.exportSnapshot(); const b = Loro.fromSnapshot(bytes); b.getText("text").insert(0, "123"); expect(b.toJson()).toStrictEqual({ text: "123hello" }); }); it("importBatch Error #181", () => { const docA = new Loro(); const updateA = docA.exportSnapshot(); const docB = new Loro(); docB.importUpdateBatch([updateA]); docB.getText("text").insert(0, "hello"); docB.commit(); console.log(docB.exportFrom()); }); }); describe("map", () => { it("keys", () => { const doc = new Loro(); const map = doc.getMap("map"); map.set("foo", "bar"); map.set("baz", "bar"); const entries = map.keys(); expect(entries).toStrictEqual(["foo", "baz"]); }); it("values", () => { const doc = new Loro(); const map = doc.getMap("map"); map.set("foo", "bar"); map.set("baz", "bar"); const entries = map.values(); expect(entries).toStrictEqual(["bar", "bar"]); }); it("entries", () => { const doc = new Loro(); const map = doc.getMap("map"); map.set("foo", "bar"); map.set("baz", "bar"); map.set("new", 11); map.delete("new"); const entries = map.entries(); expect(entries).toStrictEqual([ ["foo", "bar"], ["baz", "bar"], ]); }); it("entries should return container handlers", () => { const doc = new Loro(); const map = doc.getMap("map"); map.setContainer("text", "Text"); map.set("foo", "bar"); const entries = map.entries(); expect((entries[0][1]! as Container).kind() === "Text").toBeTruthy(); }); }); it("handlers should still be usable after doc is dropped", () => { const doc = new Loro(); const text = doc.getText("text"); const list = doc.getList("list"); const map = doc.getMap("map"); doc.free(); text.insert(0, "123"); expect(text.toString()).toBe("123"); list.insert(0, 1); expect(list.toJson()).toStrictEqual([1]); map.set("k", 8); expect(map.toJson()).toStrictEqual({ k: 8 }); }); it("get change with given lamport", () => { const doc1 = new Loro(); doc1.setPeerId(1); const doc2 = new Loro(); doc2.setPeerId(2); doc1.getText("text").insert(0, "01234"); doc2.import(doc1.exportFrom()); doc2.getText("text").insert(0, "56789"); doc1.import(doc2.exportFrom()); doc1.getText("text").insert(0, "01234"); doc1.commit(); { const change = doc1.getChangeAtLamport("1", 1)!; expect(change.lamport).toBe(0); expect(change.peer).toBe("1"); expect(change.length).toBe(5); } { const change = doc1.getChangeAtLamport("1", 7)!; expect(change.lamport).toBe(0); expect(change.peer).toBe("1"); expect(change.length).toBe(5); } { const change = doc1.getChangeAtLamport("1", 10)!; expect(change.lamport).toBe(10); expect(change.peer).toBe("1"); expect(change.length).toBe(5); } { const change = doc1.getChangeAtLamport("1", 13)!; expect(change.lamport).toBe(10); expect(change.peer).toBe("1"); expect(change.length).toBe(5); } { const change = doc1.getChangeAtLamport("1", 20)!; expect(change.lamport).toBe(10); expect(change.peer).toBe("1"); expect(change.length).toBe(5); } { const change = doc1.getChangeAtLamport("111", 13); expect(change).toBeUndefined(); } }); it("isContainer", () => { expect(isContainer("123")).toBeFalsy(); expect(isContainer(123)).toBeFalsy(); expect(isContainer(123n)).toBeFalsy(); expect(isContainer(new Map())).toBeFalsy(); expect(isContainer(new Set())).toBeFalsy(); expect(isContainer({})).toBeFalsy(); expect(isContainer(undefined)).toBeFalsy(); expect(isContainer(null)).toBeFalsy(); const doc = new Loro(); const t = doc.getText("t"); expect(isContainer(t)).toBeTruthy(); expect(getType(t)).toBe("Text"); expect(getType(123)).toBe("Json"); }); it("getValueType", () => { // Type tests const doc = new Loro(); const t = doc.getText("t"); expectTypeOf(getType(t)).toEqualTypeOf<"Text">(); expect(getType(t)).toBe("Text"); expectTypeOf(getType(123)).toEqualTypeOf<"Json">(); expect(getType(123)).toBe("Json"); expectTypeOf(getType(undefined)).toEqualTypeOf<"Json">(); expect(getType(undefined)).toBe("Json"); expectTypeOf(getType(null)).toEqualTypeOf<"Json">(); expect(getType(null)).toBe("Json"); expectTypeOf(getType({})).toEqualTypeOf<"Json">(); expect(getType({})).toBe("Json"); const map = doc.getMap("map"); const list = doc.getList("list"); const tree = doc.getTree("tree"); expectTypeOf(getType(map)).toEqualTypeOf<"Map">(); expect(getType(map)).toBe("Map"); expectTypeOf(getType(list)).toEqualTypeOf<"List">(); expect(getType(list)).toBe("List"); expectTypeOf(getType(tree)).toEqualTypeOf<"Tree">(); expect(getType(tree)).toBe("Tree"); }); it("enable timestamp", () => { const doc = new Loro(); doc.setPeerId(1); doc.getText("123").insert(0, "123"); doc.commit(); { const c = doc.getChangeAt({ peer: "1", counter: 0 }); expect(c.timestamp).toBe(0); } doc.setRecordTimestamp(true); doc.getText("123").insert(0, "123"); doc.commit(); { const c = doc.getChangeAt({ peer: "1", counter: 4 }); expect(c.timestamp).toBeCloseTo(Date.now(), -1); } }); it("commit with specified timestamp", () => { const doc = new Loro(); doc.setPeerId(1); doc.getText("123").insert(0, "123"); doc.commit(undefined, 111); const c = doc.getChangeAt({ peer: "1", counter: 0 }); expect(c.timestamp).toBe(111); }); it("can control the mergeable interval", () => { { const doc = new Loro(); doc.setPeerId(1); doc.getText("123").insert(0, "1"); doc.commit(undefined, 110); doc.getText("123").insert(0, "1"); doc.commit(undefined, 120); expect(doc.getAllChanges().get("1")?.length).toBe(1); } { const doc = new Loro(); doc.setPeerId(1); doc.setChangeMergeInterval(10); doc.getText("123").insert(0, "1"); doc.commit(undefined, 110); doc.getText("123").insert(0, "1"); doc.commit(undefined, 120); expect(doc.getAllChanges().get("1")?.length).toBe(2); } });