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
JavaScript
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()
});