import * as assert from "assert" import { unstable as Automerge } from "../src" import * as WASM from "@automerge/automerge-wasm" describe("Automerge", () => { describe("basics", () => { it("should init clone and free", () => { let doc1 = Automerge.init() let doc2 = Automerge.clone(doc1) // this is only needed if weakrefs are not supported Automerge.free(doc1) Automerge.free(doc2) }) it("should be able to make a view with specifc heads", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => (d.value = 1)) let heads2 = Automerge.getHeads(doc2) let doc3 = Automerge.change(doc2, d => (d.value = 2)) let doc2_v2 = Automerge.view(doc3, heads2) assert.deepEqual(doc2, doc2_v2) let doc2_v2_clone = Automerge.clone(doc2, "aabbcc") assert.deepEqual(doc2, doc2_v2_clone) assert.equal(Automerge.getActorId(doc2_v2_clone), "aabbcc") }) it("should allow you to change a clone of a view", () => { let doc1 = Automerge.init() doc1 = Automerge.change(doc1, d => (d.key = "value")) let heads = Automerge.getHeads(doc1) doc1 = Automerge.change(doc1, d => (d.key = "value2")) let fork = Automerge.clone(Automerge.view(doc1, heads)) assert.deepEqual(fork, { key: "value" }) fork = Automerge.change(fork, d => (d.key = "value3")) assert.deepEqual(fork, { key: "value3" }) }) it("handle basic set and read on root object", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.hello = "world" d.big = "little" d.zip = "zop" d.app = "dap" assert.deepEqual(d, { hello: "world", big: "little", zip: "zop", app: "dap", }) }) assert.deepEqual(doc2, { hello: "world", big: "little", zip: "zop", app: "dap", }) }) it("should be able to insert and delete a large number of properties", () => { let doc = Automerge.init() doc = Automerge.change(doc, doc => { doc["k1"] = true }) for (let idx = 1; idx <= 200; idx++) { doc = Automerge.change(doc, doc => { delete doc["k" + idx] doc["k" + (idx + 1)] = true assert(Object.keys(doc).length == 1) }) } }) it("can detect an automerge doc with isAutomerge()", () => { const doc1 = Automerge.from({ sub: { object: true } }) assert(Automerge.isAutomerge(doc1)) assert(!Automerge.isAutomerge(doc1.sub)) assert(!Automerge.isAutomerge("String")) assert(!Automerge.isAutomerge({ sub: { object: true } })) assert(!Automerge.isAutomerge(undefined)) const jsObj = Automerge.toJS(doc1) assert(!Automerge.isAutomerge(jsObj)) assert.deepEqual(jsObj, doc1) }) it("it should recursively freeze the document if requested", () => { let doc1 = Automerge.init({ freeze: true }) let doc2 = Automerge.init() assert(Object.isFrozen(doc1)) assert(!Object.isFrozen(doc2)) // will also freeze sub objects doc1 = Automerge.change( doc1, doc => (doc.book = { title: "how to win friends" }) ) doc2 = Automerge.merge(doc2, doc1) assert(Object.isFrozen(doc1)) assert(Object.isFrozen(doc1.book)) assert(!Object.isFrozen(doc2)) assert(!Object.isFrozen(doc2.book)) // works on from let doc3 = Automerge.from({ sub: { obj: "inner" } }, { freeze: true }) assert(Object.isFrozen(doc3)) assert(Object.isFrozen(doc3.sub)) // works on load let doc4 = Automerge.load(Automerge.save(doc3), { freeze: true }) assert(Object.isFrozen(doc4)) assert(Object.isFrozen(doc4.sub)) // follows clone let doc5 = Automerge.clone(doc4) assert(Object.isFrozen(doc5)) assert(Object.isFrozen(doc5.sub)) // toJS does not freeze let exported = Automerge.toJS(doc5) assert(!Object.isFrozen(exported)) }) it("handle basic sets over many changes", () => { let doc1 = Automerge.init() let timestamp = new Date() let counter = new Automerge.Counter(100) let bytes = new Uint8Array([10, 11, 12]) let doc2 = Automerge.change(doc1, d => { d.hello = "world" }) let doc3 = Automerge.change(doc2, d => { d.counter1 = counter }) let doc4 = Automerge.change(doc3, d => { d.timestamp1 = timestamp }) let doc5 = Automerge.change(doc4, d => { d.app = null }) let doc6 = Automerge.change(doc5, d => { d.bytes1 = bytes }) let doc7 = Automerge.change(doc6, d => { d.uint = new Automerge.Uint(1) d.int = new Automerge.Int(-1) d.float64 = new Automerge.Float64(5.5) d.number1 = 100 d.number2 = -45.67 d.true = true d.false = false }) assert.deepEqual(doc7, { hello: "world", true: true, false: false, int: -1, uint: 1, float64: 5.5, number1: 100, number2: -45.67, counter1: counter, timestamp1: timestamp, bytes1: bytes, app: null, }) let changes = Automerge.getAllChanges(doc7) let t1 = Automerge.init() let [t2] = Automerge.applyChanges(t1, changes) assert.deepEqual(doc7, t2) }) it("handle overwrites to values", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.hello = "world1" }) let doc3 = Automerge.change(doc2, d => { d.hello = "world2" }) let doc4 = Automerge.change(doc3, d => { d.hello = "world3" }) let doc5 = Automerge.change(doc4, d => { d.hello = "world4" }) assert.deepEqual(doc5, { hello: "world4" }) }) it("handle set with object value", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.subobj = { hello: "world", subsubobj: { zip: "zop" } } }) assert.deepEqual(doc2, { subobj: { hello: "world", subsubobj: { zip: "zop" } }, }) }) it("handle simple list creation", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => (d.list = [])) assert.deepEqual(doc2, { list: [] }) }) it("handle simple lists", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.list = [1, 2, 3] }) assert.deepEqual(doc2.list.length, 3) assert.deepEqual(doc2.list[0], 1) assert.deepEqual(doc2.list[1], 2) assert.deepEqual(doc2.list[2], 3) assert.deepEqual(doc2, { list: [1, 2, 3] }) // assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] }) let doc3 = Automerge.change(doc2, d => { d.list[1] = "a" }) assert.deepEqual(doc3.list.length, 3) assert.deepEqual(doc3.list[0], 1) assert.deepEqual(doc3.list[1], "a") assert.deepEqual(doc3.list[2], 3) assert.deepEqual(doc3, { list: [1, "a", 3] }) }) it("handle simple lists", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.list = [1, 2, 3] }) let changes = Automerge.getChanges(doc1, doc2) let docB1 = Automerge.init() let [docB2] = Automerge.applyChanges(docB1, changes) assert.deepEqual(docB2, doc2) }) it("handle text", () => { let doc1 = Automerge.init() let doc2 = Automerge.change(doc1, d => { d.list = "hello" Automerge.splice(d, "list", 2, 0, "Z") }) let changes = Automerge.getChanges(doc1, doc2) let docB1 = Automerge.init() let [docB2] = Automerge.applyChanges(docB1, changes) assert.deepEqual(docB2, doc2) }) it("handle non-text strings", () => { let doc1 = WASM.create(true) doc1.put("_root", "text", "hello world") let doc2 = Automerge.load(doc1.save()) assert.throws(() => { Automerge.change(doc2, d => { Automerge.splice(d, "text", 1, 0, "Z") }) }, /Cannot splice/) }) it("have many list methods", () => { let doc1 = Automerge.from({ list: [1, 2, 3] }) assert.deepEqual(doc1, { list: [1, 2, 3] }) let doc2 = Automerge.change(doc1, d => { d.list.splice(1, 1, 9, 10) }) assert.deepEqual(doc2, { list: [1, 9, 10, 3] }) let doc3 = Automerge.change(doc2, d => { d.list.push(11, 12) }) assert.deepEqual(doc3, { list: [1, 9, 10, 3, 11, 12] }) let doc4 = Automerge.change(doc3, d => { d.list.unshift(2, 2) }) assert.deepEqual(doc4, { list: [2, 2, 1, 9, 10, 3, 11, 12] }) let doc5 = Automerge.change(doc4, d => { d.list.shift() }) assert.deepEqual(doc5, { list: [2, 1, 9, 10, 3, 11, 12] }) let doc6 = Automerge.change(doc5, d => { d.list.insertAt(3, 100, 101) }) assert.deepEqual(doc6, { list: [2, 1, 9, 100, 101, 10, 3, 11, 12] }) }) it("allows access to the backend", () => { let doc = Automerge.init() assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"]) }) it("lists and text have indexof", () => { let doc = Automerge.from({ list: [0, 1, 2, 3, 4, 5, 6], text: "hello world", }) assert.deepEqual(doc.list.indexOf(5), 5) assert.deepEqual(doc.text.indexOf("world"), 6) }) }) describe("emptyChange", () => { it("should generate a hash", () => { let doc = Automerge.init() doc = Automerge.change(doc, d => { d.key = "value" }) Automerge.save(doc) let headsBefore = Automerge.getHeads(doc) headsBefore.sort() doc = Automerge.emptyChange(doc, "empty change") let headsAfter = Automerge.getHeads(doc) headsAfter.sort() assert.notDeepEqual(headsBefore, headsAfter) }) }) describe("proxy lists", () => { it("behave like arrays", () => { let doc = Automerge.from({ chars: ["a", "b", "c"], numbers: [20, 3, 100], repeats: [20, 20, 3, 3, 3, 3, 100, 100], }) let r1: Array = [] doc = Automerge.change(doc, d => { assert.deepEqual((d.chars as any[]).concat([1, 2]), [ "a", "b", "c", 1, 2, ]) assert.deepEqual( d.chars.map(n => n + "!"), ["a!", "b!", "c!"] ) assert.deepEqual( d.numbers.map(n => n + 10), [30, 13, 110] ) assert.deepEqual(d.numbers.toString(), "20,3,100") assert.deepEqual(d.numbers.toLocaleString(), "20,3,100") assert.deepEqual( d.numbers.forEach((n: number) => r1.push(n)), undefined ) assert.deepEqual( d.numbers.every(n => n > 1), true ) assert.deepEqual( d.numbers.every(n => n > 10), false ) assert.deepEqual( d.numbers.filter(n => n > 10), [20, 100] ) assert.deepEqual( d.repeats.find(n => n < 10), 3 ) assert.deepEqual( d.repeats.find(n => n < 10), 3 ) assert.deepEqual( d.repeats.find(n => n < 0), undefined ) assert.deepEqual( d.repeats.findIndex(n => n < 10), 2 ) assert.deepEqual( d.repeats.findIndex(n => n < 0), -1 ) assert.deepEqual( d.repeats.findIndex(n => n < 10), 2 ) assert.deepEqual( d.repeats.findIndex(n => n < 0), -1 ) assert.deepEqual(d.numbers.includes(3), true) assert.deepEqual(d.numbers.includes(-3), false) assert.deepEqual(d.numbers.join("|"), "20|3|100") assert.deepEqual(d.numbers.join(), "20,3,100") assert.deepEqual( d.numbers.some(f => f === 3), true ) assert.deepEqual( d.numbers.some(f => f < 0), false ) assert.deepEqual( d.numbers.reduce((sum, n) => sum + n, 100), 223 ) assert.deepEqual( d.repeats.reduce((sum, n) => sum + n, 100), 352 ) assert.deepEqual( d.chars.reduce((sum, n) => sum + n, "="), "=abc" ) assert.deepEqual( d.chars.reduceRight((sum, n) => sum + n, "="), "=cba" ) assert.deepEqual( d.numbers.reduceRight((sum, n) => sum + n, 100), 223 ) assert.deepEqual(d.repeats.lastIndexOf(3), 5) assert.deepEqual(d.repeats.lastIndexOf(3, 3), 3) }) doc = Automerge.change(doc, d => { assert.deepEqual(d.numbers.fill(-1, 1, 2), [20, -1, 100]) assert.deepEqual(d.chars.fill("z", 1, 100), ["a", "z", "z"]) }) assert.deepEqual(r1, [20, 3, 100]) assert.deepEqual(doc.numbers, [20, -1, 100]) assert.deepEqual(doc.chars, ["a", "z", "z"]) }) }) it("should obtain the same conflicts, regardless of merge order", () => { let s1 = Automerge.init() let s2 = Automerge.init() s1 = Automerge.change(s1, doc => { doc.x = 1 doc.y = 2 }) s2 = Automerge.change(s2, doc => { doc.x = 3 doc.y = 4 }) const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2)) const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1)) assert.deepStrictEqual( Automerge.getConflicts(m1, "x"), Automerge.getConflicts(m2, "x") ) }) describe("getObjectId", () => { let s1 = Automerge.from({ string: "string", number: 1, null: null, date: new Date(), counter: new Automerge.Counter(), bytes: new Uint8Array(10), text: "", list: [], map: {}, }) it("should return null for scalar values", () => { assert.equal(Automerge.getObjectId(s1.string), null) assert.equal(Automerge.getObjectId(s1.number), null) assert.equal(Automerge.getObjectId(s1.null!), null) assert.equal(Automerge.getObjectId(s1.date), null) assert.equal(Automerge.getObjectId(s1.counter), null) assert.equal(Automerge.getObjectId(s1.bytes), null) }) it("should return _root for the root object", () => { assert.equal(Automerge.getObjectId(s1), "_root") }) it("should return non-null for map, list, text, and objects", () => { assert.equal(Automerge.getObjectId(s1.text), null) assert.notEqual(Automerge.getObjectId(s1.list), null) assert.notEqual(Automerge.getObjectId(s1.map), null) }) }) })