mirror of
https://github.com/loro-dev/loro.git
synced 2025-01-23 05:24:51 +00:00
463 lines
13 KiB
TypeScript
463 lines
13 KiB
TypeScript
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
import {
|
|
Container,
|
|
getType,
|
|
isContainer,
|
|
Loro,
|
|
LoroList,
|
|
LoroMap,
|
|
LoroText,
|
|
LoroTree,
|
|
} from "../src";
|
|
|
|
it("basic example", () => {
|
|
const doc = new Loro();
|
|
const list = doc.getList("list");
|
|
list.insert(0, "A");
|
|
list.insert(1, "B");
|
|
list.insert(2, "C");
|
|
|
|
const map = 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, new LoroText());
|
|
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", new LoroList());
|
|
list2.insert(0, 1);
|
|
expect(doc.toJSON()).toStrictEqual({
|
|
list: ["Hi! Hello", "C"],
|
|
map: { key: "value", test: [1] },
|
|
});
|
|
});
|
|
|
|
it("get or create on Map", () => {
|
|
const docA = new Loro();
|
|
const map = docA.getMap("map");
|
|
const container = map.getOrCreateContainer("list", new LoroList());
|
|
container.insert(0, 1);
|
|
container.insert(0, 2);
|
|
const text = map.getOrCreateContainer("text", new LoroText());
|
|
text.insert(0, "Hello");
|
|
expect(docA.toJSON()).toStrictEqual({
|
|
map: { list: [2, 1], text: "Hello" },
|
|
});
|
|
});
|
|
|
|
it("basic sync example", () => {
|
|
const docA = new Loro();
|
|
const docB = new Loro();
|
|
const listA = 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 = 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, new LoroMap());
|
|
map.set("key", "value");
|
|
const v = list.get(0) as LoroMap;
|
|
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, new LoroText());
|
|
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", new LoroList());
|
|
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();
|
|
});
|
|
});
|
|
|
|
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", new LoroText());
|
|
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(isContainer(doc.getMap("m"))).toBeTruthy();
|
|
expect(isContainer(doc.getMovableList("l"))).toBeTruthy();
|
|
expect(isContainer(doc.getText("text"))).toBeTruthy();
|
|
expect(isContainer(doc.getTree("tree"))).toBeTruthy();
|
|
expect(isContainer(doc.getList("list"))).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");
|
|
const text = doc.getText("text");
|
|
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");
|
|
expectTypeOf(getType(text)).toEqualTypeOf<"Text">();
|
|
expect(getType(text)).toBe("Text");
|
|
});
|
|
|
|
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);
|
|
console.log(doc.getAllChanges());
|
|
expect(doc.getAllChanges().get("1")?.length).toBe(2);
|
|
}
|
|
});
|
|
|
|
it("get container parent", () => {
|
|
const doc = new Loro();
|
|
const m = doc.getMap("m");
|
|
expect(m.parent()).toBeUndefined();
|
|
const list = m.setContainer("t", new LoroList());
|
|
expect(list.parent()!.id).toBe(m.id);
|
|
const text = list.insertContainer(0, new LoroText());
|
|
expect(text.parent()!.id).toBe(list.id);
|
|
const tree = list.insertContainer(1, new LoroTree());
|
|
expect(tree.parent()!.id).toBe(list.id);
|
|
const treeNode = tree.createNode();
|
|
const subtext = treeNode.data.setContainer("t", new LoroText());
|
|
expect(subtext.parent()!.id).toBe(treeNode.data.id);
|
|
});
|
|
|
|
it("prelim support", () => {
|
|
// Now we can create a new container directly
|
|
const map = new LoroMap();
|
|
map.set("3", 2);
|
|
const list = new LoroList();
|
|
list.insertContainer(0, map);
|
|
// map should still be valid
|
|
map.set("9", 9);
|
|
// the type of setContainer/insertContainer changed
|
|
const text = map.setContainer("text", new LoroText());
|
|
{
|
|
// Changes will be reflected in the container tree
|
|
text.insert(0, "Heello");
|
|
expect(list.toJSON()).toStrictEqual([{ "3": 2, "9": 9, text: "Heello" }]);
|
|
text.delete(1, 1);
|
|
expect(list.toJSON()).toStrictEqual([{ "3": 2, "9": 9, text: "Hello" }]);
|
|
}
|
|
const doc = new Loro();
|
|
const rootMap = doc.getMap("map");
|
|
rootMap.setContainer("test", map); // new way to create sub-container
|
|
|
|
// Use getAttached() to get the attached version of text
|
|
const attachedText = text.getAttached()!;
|
|
expect(text.isAttached()).toBeFalsy();
|
|
expect(attachedText.isAttached()).toBeTruthy();
|
|
text.insert(0, "Detached ");
|
|
attachedText.insert(0, "Attached ");
|
|
expect(text.toString()).toBe("Detached Hello");
|
|
expect(doc.toJSON()).toStrictEqual({
|
|
map: {
|
|
test: {
|
|
"3": 2,
|
|
"9": 9,
|
|
text: "Attached Hello",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("get elem by path", () => {
|
|
const doc = new Loro();
|
|
const map = doc.getMap("map");
|
|
map.set("key", 1);
|
|
expect(doc.getByPath("map/key")).toBe(1);
|
|
const map1 = doc.getByPath("map") as LoroMap;
|
|
expect(getType(map1)).toBe("Map");
|
|
map1.set("key1", 1);
|
|
expect(doc.getByPath("map/key1")).toBe(1);
|
|
});
|