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 system. It is developed to power our online editing platform [Substance](http://substance.io).
446 lines (409 loc) • 14.7 kB
JavaScript
import { test } from 'substance-test'
import { ObjectOperation, ArrayOperation, TextOperation, PathObject, cloneDeep } from 'substance'
function checkObjectOperationTransform (test, a, b, input, expected) {
const ops = ObjectOperation.transform(a.clone(), b.clone())
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('ObjectOperation: Creating values.', t => {
const path = ['a']
const val = { bla: 'blupp' }
const expected = { a: { bla: 'blupp' } }
const op = ObjectOperation.Create(path, val)
const obj = {}
op.apply(obj)
t.deepEqual(obj, expected, 'Should create value.')
t.end()
})
test('ObjectOperation: Creating nested values.', t => {
const path = ['a', 'b']
const val = { bla: 'blupp' }
const expected = { a: { b: { bla: 'blupp' } } }
const op = ObjectOperation.Create(path, val)
const obj = { a: {} }
op.apply(obj)
t.deepEqual(obj, expected, 'Should create nested value.')
t.end()
})
test('ObjectOperation: Deleting values.', t => {
const path = ['a']
const val = 'bla'
const op = ObjectOperation.Delete(path, val)
const expected = {}
const obj = { a: 'bla' }
op.apply(obj)
t.deepEqual(obj, expected, 'Should delete value.')
t.end()
})
test('ObjectOperation: Deleting nested values.', t => {
const path = ['a', 'b']
const val = 'bla'
const op = ObjectOperation.Delete(path, val)
const expected = { a: {} }
const obj = { a: { b: 'bla' } }
op.apply(obj)
t.deepEqual(obj, expected, 'Should delete nested value.')
t.end()
})
test('ObjectOperation: Updating a text property.', t => {
const obj = { a: 'bla' }
const path = ['a']
const op1 = ObjectOperation.Update(path, TextOperation.Delete(2, 'a'))
const op2 = ObjectOperation.Update(path, TextOperation.Insert(2, 'upp'))
const expected = { a: 'blupp' }
op1.apply(obj)
op2.apply(obj)
t.deepEqual(obj, expected)
t.end()
})
test('ObjectOperation: Updating an array property.', t => {
const obj = { a: [1, 2, 3, 4, 5] }
const path = ['a']
const op1 = ObjectOperation.Update(path, ArrayOperation.Delete(2, 3))
const op2 = ObjectOperation.Update(path, ArrayOperation.Insert(4, 6))
const expected = { a: [1, 2, 4, 5, 6] }
op1.apply(obj)
op2.apply(obj)
t.deepEqual(obj, expected)
t.end()
})
test('ObjectOperation: Creating an update operation with invalid diff.', t => {
t.throws(function () {
ObjectOperation.Update(['test'], 'foo')
}, 'Should throw.')
t.end()
})
test('ObjectOperation: Creating a top-level property using id.', t => {
const obj = {}
const id = 'foo'
const op = ObjectOperation.Create(id, 'bar')
const expected = { foo: 'bar' }
op.apply(obj)
t.deepEqual(obj, expected)
t.end()
})
test('ObjectOperation: Deleting a top-level property using id.', t => {
const obj = { foo: 'bar' }
const id = 'foo'
const op = ObjectOperation.Delete(id, 'bar')
const expected = {}
op.apply(obj)
t.deepEqual(obj, expected)
t.end()
})
test('ObjectOperation: Apply operation on PathObject.', t => {
const myObj = new PathObject()
const op = ObjectOperation.Set(['foo', 'bar'], null, 'bla')
op.apply(myObj)
t.equal(myObj.get(['foo', 'bar']), 'bla')
t.end()
})
test('ObjectOperation: Creating operation with invalid data.', t => {
t.throws(function () {
new ObjectOperation() // eslint-disable-line no-new
}, 'Should throw when data is undefined.')
t.throws(function () {
new ObjectOperation({}) // eslint-disable-line no-new
}, 'Should throw when type is missing.')
t.throws(function () {
new ObjectOperation({ type: 'foo', path: ['test'] }) // eslint-disable-line no-new
}, 'Should throw when type is invalid.')
t.throws(function () {
new ObjectOperation({ // eslint-disable-line no-new
type: ObjectOperation.CREATE
})
}, 'Should throw when path is missing.')
t.throws(function () {
new ObjectOperation({ // eslint-disable-line no-new
type: ObjectOperation.CREATE,
path: ['test']
})
}, 'Should throw when created value is missing.')
t.throws(function () {
new ObjectOperation({ // eslint-disable-line no-new
type: ObjectOperation.UPDATE,
path: ['test']
})
}, 'Should throw when update diff is missing.')
t.throws(function () {
new ObjectOperation({ // eslint-disable-line no-new
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('ObjectOperation: Inverse of NOP is NOP', t => {
const op = new ObjectOperation({ type: ObjectOperation.NOP })
const inverse = op.invert()
t.ok(inverse.isNOP())
t.end()
})
test('ObjectOperation: Inverse of Create is Delete and vice versa', t => {
const 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('ObjectOperation: 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('ObjectOperation: Inverse of Set is Set', t => {
const op = ObjectOperation.Set(['test', 'foo'], 'foo', 'bar')
const 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('ObjectOperation: Transformation: everything easy when not the same property', t => {
const path1 = ['a']
const path2 = ['b']
const val1 = 'bla'
const val2 = 'blupp'
const a = ObjectOperation.Create(path1, val1)
const b = ObjectOperation.Create(path2, val2)
const ops = ObjectOperation.transform(a, b)
t.deepEqual(ops[0].toJSON(), a.toJSON())
t.deepEqual(ops[1].toJSON(), b.toJSON())
t.end()
})
test('ObjectOperation: Transformation: everything easy when NOP involved', t => {
const path1 = ['a']
const val1 = 'bla'
const a = ObjectOperation.Create(path1, val1)
const b = new ObjectOperation({ type: ObjectOperation.NOP })
const ops = ObjectOperation.transform(a, b)
t.deepEqual(ops[0].toJSON(), a.toJSON())
t.deepEqual(ops[1].toJSON(), b.toJSON())
t.end()
})
test('ObjectOperation: Transformation: creating the same value (unresolvable conflict)', t => {
const path = ['a']
const val1 = 'bla'
const val2 = 'blupp'
const a = ObjectOperation.Create(path, val1)
const b = ObjectOperation.Create(path, val2)
t.throws(function () {
ObjectOperation.transform(a, b)
})
t.end()
})
test('ObjectOperation: Transformation: creating and updating the same value (unresolvable conflict)', t => {
const path = ['a']
const val1 = 'bla'
const a = ObjectOperation.Create(path, val1)
const b = ObjectOperation.Update(path, TextOperation.Insert(1, 'b'))
t.throws(function () {
ObjectOperation.transform(a, b)
})
t.end()
})
test('ObjectOperation: Transformation: creating and setting the same value (unresolvable conflict)', t => {
const path = ['a']
const val1 = 'bla'
const val2 = 'blupp'
const a = ObjectOperation.Create(path, val1)
const b = ObjectOperation.Set(path, val1, val2)
t.throws(function () {
ObjectOperation.transform(a, b)
})
t.end()
})
test('ObjectOperation: Transformation: creating and deleting the same value (unresolvable conflict)', t => {
const path = ['a']
const val1 = 'bla'
const a = ObjectOperation.Create(path, val1)
const b = ObjectOperation.Delete(path, val1)
t.throws(function () {
ObjectOperation.transform(a, b)
})
t.end()
})
test('ObjectOperation: Transformation: deleting the same value', t => {
const path = ['a']
const val = 'bla'
const input = { a: val }
const expected = {}
const a = ObjectOperation.Delete(path, val)
const b = ObjectOperation.Delete(path, val)
checkObjectOperationTransform(t, a, b, input, expected)
checkObjectOperationTransform(t, b, a, input, expected)
t.end()
})
test('ObjectOperation: Transformation: deleting and updating the same value', t => {
const 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('ObjectOperation: Transformation: deleting and setting the same value', t => {
const path = ['a']
const a = ObjectOperation.Delete(path, 'bla')
const b = ObjectOperation.Set(path, 'bla', 'blupp')
const input = { a: 'bla' }
const expected1 = { a: 'blupp' }
const expected2 = {}
checkObjectOperationTransform(t, a, b, input, expected1)
checkObjectOperationTransform(t, b, a, input, expected2)
t.end()
})
test('ObjectOperation: Transformation: updating the same value', t => {
const 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('ObjectOperation: Transformation: updating and setting the same value (unresolvable conflict)', t => {
const path = ['a']
const a = ObjectOperation.Update(path, TextOperation.Insert(3, 'ff'))
const b = ObjectOperation.Set(path, 'bla', 'blupp')
t.throws(function () {
ObjectOperation.transform(a, b)
})
t.end()
})
test('ObjectOperation: Transformation: setting the same value', t => {
const path = ['a']
const a = ObjectOperation.Set(path, 'bla', 'blapp')
const b = ObjectOperation.Set(path, 'bla', 'blupp')
const input = { a: 'bla' }
const expected1 = { a: 'blupp' }
const expected2 = { a: 'blapp' }
checkObjectOperationTransform(t, a, b, input, expected1)
checkObjectOperationTransform(t, b, a, input, expected2)
t.end()
})
test('ObjectOperation: ObjectOperation with the same path are conflicts.', t => {
const a = ObjectOperation.Set(['bla', 'blupp'], null, 'foo')
const b = ObjectOperation.Set(['bla', 'blupp'], null, 'bar')
t.ok(a.hasConflict(b) && b.hasConflict(a))
t.end()
})
test('ObjectOperation: NOPs have never conflicts.', t => {
const a = ObjectOperation.Set(['bla', 'blupp'], null, 'foo')
const b = new ObjectOperation({ type: ObjectOperation.NOP })
t.ok(!a.hasConflict(b) && !b.hasConflict(a))
t.end()
})
test("ObjectOperation: With option 'no-conflict' conflicting operations can not be transformed.", t => {
const a = ObjectOperation.Create('bla', 'blupp')
const 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('ObjectOperation: JSON deserialisation', t => {
const 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('ObjectOperation: 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()
})