UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

447 lines (410 loc) 13.8 kB
import { module } from 'substance-test' import { ObjectOperation, ArrayOperation, TextOperation, PathObject, cloneDeep } from 'substance' const test = module('ObjectOperation') function checkObjectOperationTransform(test, a, b, input, expected) { let ops = ObjectOperation.transform(a, b) let output = ops[1].apply(a.apply(cloneDeep(input))) test.deepEqual(output, expected, `(b' o a)('${JSON.stringify(input)}') == '${JSON.stringify(expected)}' with a=${a.toString()}, b'=${ops[1].toString()}`) output = ops[0].apply(b.apply(cloneDeep(input))) test.deepEqual(output, expected, `(a' o b)('${JSON.stringify(input)}') == '${JSON.stringify(expected)}' with b=${b.toString()}, a'=${ops[0].toString()}`) } test("Creating values.", (t) => { let path = ["a"] let val = { bla: "blupp" } let expected = {a: { bla: "blupp" } } let op = ObjectOperation.Create(path, val) let obj = {} op.apply(obj) t.deepEqual(obj, expected, 'Should create value.') t.end() }) test("Creating nested values.", (t) => { let path = ["a", "b"] let val = { bla: "blupp" } let expected = {a: { b: { bla: "blupp" } } } let op = ObjectOperation.Create(path, val) let obj = {"a": {}} op.apply(obj) t.deepEqual(obj, expected, 'Should create nested value.') t.end() }) test("Deleting values.", (t) => { let path = ["a"] let val = "bla" let op = ObjectOperation.Delete(path, val) let expected = {} let obj = {"a": "bla"} op.apply(obj) t.deepEqual(obj, expected, 'Should delete value.') t.end() }) test("Deleting nested values.", (t) => { let path = ["a", "b"] let val = "bla" let op = ObjectOperation.Delete(path, val) let expected = { a: {} } let obj = { a: { b: "bla"} } op.apply(obj) t.deepEqual(obj, expected, 'Should delete nested value.') t.end() }) test("Updating a text property.", (t) => { let obj = {a: "bla"} let path = ["a"] let op1 = ObjectOperation.Update(path, TextOperation.Delete(2, 'a')) let op2 = ObjectOperation.Update(path, TextOperation.Insert(2, 'upp')) let expected = {a: "blupp"} op1.apply(obj) op2.apply(obj) t.deepEqual(obj, expected) t.end() }) test("Updating an array property.", (t) => { let obj = {a: [1,2,3,4,5]} let path = ["a"] let op1 = ObjectOperation.Update(path, ArrayOperation.Delete(2, 3)) let op2 = ObjectOperation.Update(path, ArrayOperation.Insert(4, 6)) let expected = {a: [1,2,4,5,6]} op1.apply(obj) op2.apply(obj) t.deepEqual(obj, expected) t.end() }) test("Creating an update operation with invalid diff.", (t) => { t.throws(function() { ObjectOperation.Update(['test'], "foo") }, "Should throw.") t.end() }) test("Creating a top-level property using id.", (t) => { let obj = {} let id = "foo" let op = ObjectOperation.Create(id, "bar") let expected = { "foo": "bar" } op.apply(obj) t.deepEqual(obj, expected) t.end() }) test("Deleting a top-level property using id.", (t) => { let obj = { foo: "bar" } let id = "foo" let op = ObjectOperation.Delete(id, "bar") let expected = {} op.apply(obj) t.deepEqual(obj, expected) t.end() }) test("Apply operation on PathObject.", (t) => { let myObj = new PathObject() let op = ObjectOperation.Set(['foo', 'bar'], null, 'bla') op.apply(myObj) t.equal(myObj.get(['foo', 'bar']), 'bla') t.end() }) test("Creating operation with invalid data.", (t) => { t.throws(function() { new ObjectOperation() }, "Should throw when data is undefined.") t.throws(function() { new ObjectOperation({}) }, "Should throw when type is missing.") t.throws(function() { new ObjectOperation({ type: 'foo', path: ['test'] }) }, "Should throw when type is invalid.") t.throws(function() { new ObjectOperation({ type: ObjectOperation.CREATE }) }, "Should throw when path is missing.") t.throws(function() { new ObjectOperation({ type: ObjectOperation.CREATE, path: ["test"], }) }, "Should throw when created value is missing.") t.throws(function() { new ObjectOperation({ type: ObjectOperation.UPDATE, path: ["test"] }) }, "Should throw when update diff is missing.") t.throws(function() { new ObjectOperation({ type: ObjectOperation.UPDATE, path: ["test"], diff: "foo" }) }, "Should throw when update diff is invalid.") // we have relaxed that, so that it is possible to 'delete' a property by setting it to undefined // t.throws(function() { // new ObjectOperation({ // type: ObjectOperation.SET, // path: ["test"], // }) // }, "Should throw when value is missing.") // t.throws(function() { // new ObjectOperation({ // type: ObjectOperation.SET, // path: ["test"], // val: 1 // }) // }, "Should throw when old value is missing.") t.end() }) test("Inverse of NOP is NOP", (t) => { let op = new ObjectOperation({type: ObjectOperation.NOP}) let inverse = op.invert() t.ok(inverse.isNOP()) t.end() }) test("Inverse of Create is Delete and vice versa", (t) => { let op = ObjectOperation.Create('test', 'foo') let inverse = op.invert() t.ok(inverse.isDelete()) t.equal(inverse.getValue(), 'foo') inverse = inverse.invert() t.ok(inverse.isCreate()) t.equal(inverse.getValue(), 'foo') t.end() }) test("Inverse of Update is Update", (t) => { let op = ObjectOperation.Update(['test', 'foo'], ArrayOperation.Insert(1, 'a')) let inverse = op.invert() t.ok(inverse.isUpdate()) t.deepEqual(inverse.getPath(), ['test', 'foo']) t.ok(inverse.diff instanceof ArrayOperation) t.deepEqual(inverse.diff.toJSON(), op.diff.invert().toJSON()) // same with a text operation as diff op = ObjectOperation.Update(['test', 'foo'], TextOperation.Insert(1, 'a')) inverse = op.invert() t.ok(inverse.diff instanceof TextOperation) t.deepEqual(inverse.diff.toJSON(), op.diff.invert().toJSON()) t.end() }) test("Inverse of Set is Set", (t) => { let op = ObjectOperation.Set(['test', 'foo'], 'foo', 'bar') let inverse = op.invert() t.ok(inverse.isSet()) t.deepEqual(inverse.getPath(), ['test', 'foo']) t.equal(inverse.getValue(), 'foo') t.equal(inverse.original, 'bar') t.end() }) test("Transformation: everything easy when not the same property", (t) => { let path1 = ["a"] let path2 = ["b"] let val1 = "bla" let val2 = "blupp" let a = ObjectOperation.Create(path1, val1) let b = ObjectOperation.Create(path2, val2) let ops = ObjectOperation.transform(a, b) t.deepEqual(ops[0].toJSON(), a.toJSON()) t.deepEqual(ops[1].toJSON(), b.toJSON()) t.end() }) test("Transformation: everything easy when NOP involved", (t) => { let path1 = ["a"] let val1 = "bla" let a = ObjectOperation.Create(path1, val1) let b = new ObjectOperation({type: ObjectOperation.NOP}) let ops = ObjectOperation.transform(a, b) t.deepEqual(ops[0].toJSON(), a.toJSON()) t.deepEqual(ops[1].toJSON(), b.toJSON()) t.end() }) test("Transformation: creating the same value (unresolvable conflict)", (t) => { let path = ["a"] let val1 = "bla" let val2 = "blupp" let a = ObjectOperation.Create(path, val1) let b = ObjectOperation.Create(path, val2) t.throws(function() { ObjectOperation.transform(a, b) }) t.end() }) test("Transformation: creating and updating the same value (unresolvable conflict)", (t) => { let path = ["a"] let val1 = "bla" let a = ObjectOperation.Create(path, val1) let b = ObjectOperation.Update(path, TextOperation.Insert(1, 'b')) t.throws(function() { ObjectOperation.transform(a, b) }) t.end() }) test("Transformation: creating and setting the same value (unresolvable conflict)", (t) => { let path = ["a"] let val1 = "bla" let val2 = "blupp" let a = ObjectOperation.Create(path, val1) let b = ObjectOperation.Set(path, val1, val2) t.throws(function() { ObjectOperation.transform(a, b) }) t.end() }) test("Transformation: creating and deleting the same value (unresolvable conflict)", (t) => { let path = ["a"] let val1 = "bla" let a = ObjectOperation.Create(path, val1) let b = ObjectOperation.Delete(path, val1) t.throws(function() { ObjectOperation.transform(a, b) }) t.end() }) test("Transformation: deleting the same value", (t) => { let path = ["a"] let val = "bla" let input = {"a": val} let expected = {} let a = ObjectOperation.Delete(path, val) let b = ObjectOperation.Delete(path, val) checkObjectOperationTransform(t, a, b, input, expected) checkObjectOperationTransform(t, b, a, input, expected) t.end() }) test("Transformation: deleting and updating the same value", (t) => { let path = ["a"] let a = ObjectOperation.Delete(path, "bla") let b = ObjectOperation.Update(path, TextOperation.Insert(3, "pp")) let input = {a : "bla"} let expected1 = {a: "blapp"} let expected2 = {} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) // same with an array operation a = ObjectOperation.Delete(path, [1,2,3]) b = ObjectOperation.Update(path, ArrayOperation.Insert(3, 4)) input = {a : [1,2,3]} expected1 = {a: [1,2,3,4]} expected2 = {} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) t.end() }) test("Transformation: deleting and setting the same value", (t) => { let path = ["a"] let a = ObjectOperation.Delete(path, "bla") let b = ObjectOperation.Set(path, "bla", "blupp") let input = {a : "bla"} let expected1 = {a: "blupp"} let expected2 = {} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) t.end() }) test("Transformation: updating the same value", (t) => { let path = ["a"] let a = ObjectOperation.Update(path, TextOperation.Insert(3, "pp")) let b = ObjectOperation.Update(path, TextOperation.Insert(3, "ff")) let input = {a : "bla"} let expected1 = {a: "blappff"} let expected2 = {a: "blaffpp"} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) // same with array updates a = ObjectOperation.Update(path, ArrayOperation.Insert(2, 3)) b = ObjectOperation.Update(path, ArrayOperation.Insert(2, 4)) input = {a : [1,2,5]} expected1 = {a: [1,2,3,4,5]} expected2 = {a: [1,2,4,3,5]} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) t.end() }) test("Transformation: updating and setting the same value (unresolvable conflict)", (t) => { let path = ["a"] let a = ObjectOperation.Update(path, TextOperation.Insert(3, "ff")) let b = ObjectOperation.Set(path, "bla", "blupp") t.throws(function() { ObjectOperation.transform(a, b) }) t.end() }) test("Transformation: setting the same value", (t) => { let path = ["a"] let a = ObjectOperation.Set(path, "bla", "blapp") let b = ObjectOperation.Set(path, "bla", "blupp") let input = {a : "bla"} let expected1 = {a: "blupp"} let expected2 = {a: "blapp"} checkObjectOperationTransform(t, a, b, input, expected1) checkObjectOperationTransform(t, b, a, input, expected2) t.end() }) test("ObjectOperation with the same path are conflicts.", (t) => { let a = ObjectOperation.Set(['bla', 'blupp'], null, 'foo') let b = ObjectOperation.Set(['bla', 'blupp'], null, 'bar') t.ok(a.hasConflict(b) && b.hasConflict(a)) t.end() }) test("NOPs have never conflicts.", (t) => { let a = ObjectOperation.Set(['bla', 'blupp'], null, 'foo') let b = new ObjectOperation({type: ObjectOperation.NOP}) t.ok(!a.hasConflict(b) && !b.hasConflict(a)) t.end() }) test("With option 'no-conflict' conflicting operations can not be transformed.", (t) => { let a = ObjectOperation.Create('bla', 'blupp') let b = ObjectOperation.Create('bla', 'blupp') t.throws(function() { ObjectOperation.transform(a, b, { "no-conflict": true }) }, 'Transforming conflicting ops should throw when option "no-conflict" is enabled.') t.end() }) test("JSON deserialisation", (t) => { let path = ["test", "foo"] let op = ObjectOperation.fromJSON({ type: ObjectOperation.SET, path: path, val: "bla", original: "blupp" }) t.equal(op.getType(), ObjectOperation.SET) t.equal(op.getValue(), "bla") op = ObjectOperation.fromJSON({ type: ObjectOperation.UPDATE, path: path, diff: ArrayOperation.Insert(1, 'a'), propertyType: 'array' }) t.ok(op.diff instanceof ArrayOperation) t.deepEqual(op.getPath(), path) op = ObjectOperation.fromJSON({ type: ObjectOperation.UPDATE, path: path, diff: TextOperation.Insert(1, 'a'), propertyType: 'string' }) t.ok(op.diff instanceof TextOperation) t.deepEqual(op.getPath(), path) t.throws(function() { ObjectOperation.fromJSON({ type: ObjectOperation.UPDATE, path: path, diff: "bla", propertyType: 'foo' }) }, "Should throw for unknown update diff type.") t.end() }) test("JSON serialisation", (t) => { let data = ObjectOperation.Create('test', "bla").toJSON() t.equal(data.type, ObjectOperation.CREATE) t.equal(data.val, "bla") data = ObjectOperation.Delete('test', "bla").toJSON() t.equal(data.type, ObjectOperation.DELETE) t.equal(data.val, "bla") data = ObjectOperation.Update(['test', 'foo'], ArrayOperation.Insert(1, 'a')).toJSON() t.equal(data.type, ObjectOperation.UPDATE) t.equal(data.propertyType, 'array') data = ObjectOperation.Update(['test', 'foo'], TextOperation.Insert(1, 'a')).toJSON() t.equal(data.propertyType, 'string') data = ObjectOperation.Set(['test', 'foo'], 'foo', 'bar').toJSON() t.equal(data.type, ObjectOperation.SET) t.equal(data.val, 'bar') t.equal(data.original, 'foo') t.end() });