mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-13 11:39:22 -05:00
ee85d948e2
GitOrigin-RevId: ef2ef77e26df59d1af3df6dc664e284d3c70102d
269 lines
7.2 KiB
JavaScript
269 lines
7.2 KiB
JavaScript
//
|
|
// These tests are based on the OT.js tests:
|
|
// https://github.com/Operational-Transformation/ot.js/blob/
|
|
// 8873b7e28e83f9adbf6c3a28ec639c9151a838ae/test/lib/test-text-operation.js
|
|
//
|
|
'use strict'
|
|
|
|
const { expect } = require('chai')
|
|
const random = require('./support/random')
|
|
|
|
const ot = require('..')
|
|
const TextOperation = ot.TextOperation
|
|
|
|
function randomOperation(str) {
|
|
const operation = new TextOperation()
|
|
let left
|
|
while (true) {
|
|
left = str.length - operation.baseLength
|
|
if (left === 0) break
|
|
const r = Math.random()
|
|
const l = 1 + random.int(Math.min(left - 1, 20))
|
|
if (r < 0.2) {
|
|
operation.insert(random.string(l))
|
|
} else if (r < 0.4) {
|
|
operation.remove(l)
|
|
} else {
|
|
operation.retain(l)
|
|
}
|
|
}
|
|
if (Math.random() < 0.3) {
|
|
operation.insert(1 + random.string(10))
|
|
}
|
|
return operation
|
|
}
|
|
|
|
describe('TextOperation', function () {
|
|
const numTrials = 500
|
|
|
|
it('tracks base and target lengths', function () {
|
|
const o = new TextOperation()
|
|
expect(o.baseLength).to.equal(0)
|
|
expect(o.targetLength).to.equal(0)
|
|
o.retain(5)
|
|
expect(o.baseLength).to.equal(5)
|
|
expect(o.targetLength).to.equal(5)
|
|
o.insert('abc')
|
|
expect(o.baseLength).to.equal(5)
|
|
expect(o.targetLength).to.equal(8)
|
|
o.retain(2)
|
|
expect(o.baseLength).to.equal(7)
|
|
expect(o.targetLength).to.equal(10)
|
|
o.remove(2)
|
|
expect(o.baseLength).to.equal(9)
|
|
expect(o.targetLength).to.equal(10)
|
|
})
|
|
|
|
it('supports chaining', function () {
|
|
const o = new TextOperation()
|
|
.retain(5)
|
|
.retain(0)
|
|
.insert('lorem')
|
|
.insert('')
|
|
.remove('abc')
|
|
.remove(3)
|
|
.remove(0)
|
|
.remove('')
|
|
expect(o.ops.length).to.equal(3)
|
|
})
|
|
|
|
it('ignores empty operations', function () {
|
|
const o = new TextOperation()
|
|
o.retain(0)
|
|
o.insert('')
|
|
o.remove('')
|
|
expect(o.ops.length).to.equal(0)
|
|
})
|
|
|
|
it('checks for equality', function () {
|
|
const op1 = new TextOperation().remove(1).insert('lo').retain(2).retain(3)
|
|
const op2 = new TextOperation().remove(-1).insert('l').insert('o').retain(5)
|
|
expect(op1.equals(op2)).to.be.true
|
|
op1.remove(1)
|
|
op2.retain(1)
|
|
expect(op1.equals(op2)).to.be.false
|
|
})
|
|
|
|
it('merges ops', function () {
|
|
function last(arr) {
|
|
return arr[arr.length - 1]
|
|
}
|
|
const o = new TextOperation()
|
|
expect(o.ops.length).to.equal(0)
|
|
o.retain(2)
|
|
expect(o.ops.length).to.equal(1)
|
|
expect(last(o.ops)).to.equal(2)
|
|
o.retain(3)
|
|
expect(o.ops.length).to.equal(1)
|
|
expect(last(o.ops)).to.equal(5)
|
|
o.insert('abc')
|
|
expect(o.ops.length).to.equal(2)
|
|
expect(last(o.ops)).to.equal('abc')
|
|
o.insert('xyz')
|
|
expect(o.ops.length).to.equal(2)
|
|
expect(last(o.ops)).to.equal('abcxyz')
|
|
o.remove('d')
|
|
expect(o.ops.length).to.equal(3)
|
|
expect(last(o.ops)).to.equal(-1)
|
|
o.remove('d')
|
|
expect(o.ops.length).to.equal(3)
|
|
expect(last(o.ops)).to.equal(-2)
|
|
})
|
|
|
|
it('checks for no-ops', function () {
|
|
const o = new TextOperation()
|
|
expect(o.isNoop()).to.be.true
|
|
o.retain(5)
|
|
expect(o.isNoop()).to.be.true
|
|
o.retain(3)
|
|
expect(o.isNoop()).to.be.true
|
|
o.insert('lorem')
|
|
expect(o.isNoop()).to.be.false
|
|
})
|
|
|
|
it('converts to string', function () {
|
|
const o = new TextOperation()
|
|
o.retain(2)
|
|
o.insert('lorem')
|
|
o.remove('ipsum')
|
|
o.retain(5)
|
|
expect(o.toString()).to.equal(
|
|
"retain 2, insert 'lorem', remove 5, retain 5"
|
|
)
|
|
})
|
|
|
|
it('converts from JSON', function () {
|
|
const ops = [2, -1, -1, 'cde']
|
|
const o = TextOperation.fromJSON(ops)
|
|
expect(o.ops.length).to.equal(3)
|
|
expect(o.baseLength).to.equal(4)
|
|
expect(o.targetLength).to.equal(5)
|
|
|
|
function assertIncorrectAfter(fn) {
|
|
const ops2 = ops.slice(0)
|
|
fn(ops2)
|
|
expect(() => {
|
|
TextOperation.fromJSON(ops2)
|
|
}).to.throw
|
|
}
|
|
|
|
assertIncorrectAfter(ops2 => {
|
|
ops2.push({ insert: 'x' })
|
|
})
|
|
assertIncorrectAfter(ops2 => {
|
|
ops2.push(null)
|
|
})
|
|
})
|
|
|
|
it(
|
|
'applies (randomised)',
|
|
random.test(numTrials, () => {
|
|
const str = random.string(50)
|
|
const o = randomOperation(str)
|
|
expect(str.length).to.equal(o.baseLength)
|
|
expect(o.apply(str).length).to.equal(o.targetLength)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'inverts (randomised)',
|
|
random.test(numTrials, () => {
|
|
const str = random.string(50)
|
|
const o = randomOperation(str)
|
|
const p = o.invert(str)
|
|
expect(o.baseLength).to.equal(p.targetLength)
|
|
expect(o.targetLength).to.equal(p.baseLength)
|
|
expect(p.apply(o.apply(str))).to.equal(str)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'converts to/from JSON (randomised)',
|
|
random.test(numTrials, () => {
|
|
const doc = random.string(50)
|
|
const operation = randomOperation(doc)
|
|
const roundTripOperation = TextOperation.fromJSON(operation.toJSON())
|
|
expect(operation.equals(roundTripOperation)).to.be.true
|
|
})
|
|
)
|
|
|
|
it(
|
|
'composes (randomised)',
|
|
random.test(numTrials, () => {
|
|
// invariant: apply(str, compose(a, b)) === apply(apply(str, a), b)
|
|
const str = random.string(20)
|
|
const a = randomOperation(str)
|
|
const afterA = a.apply(str)
|
|
expect(afterA.length).to.equal(a.targetLength)
|
|
const b = randomOperation(afterA)
|
|
const afterB = b.apply(afterA)
|
|
expect(afterB.length).to.equal(b.targetLength)
|
|
const ab = a.compose(b)
|
|
expect(ab.targetLength).to.equal(b.targetLength)
|
|
const afterAB = ab.apply(str)
|
|
expect(afterAB).to.equal(afterB)
|
|
})
|
|
)
|
|
|
|
it(
|
|
'transforms (randomised)',
|
|
random.test(numTrials, () => {
|
|
// invariant: compose(a, b') = compose(b, a')
|
|
// where (a', b') = transform(a, b)
|
|
const str = random.string(20)
|
|
const a = randomOperation(str)
|
|
const b = randomOperation(str)
|
|
const primes = TextOperation.transform(a, b)
|
|
const aPrime = primes[0]
|
|
const bPrime = primes[1]
|
|
const abPrime = a.compose(bPrime)
|
|
const baPrime = b.compose(aPrime)
|
|
const afterAbPrime = abPrime.apply(str)
|
|
const afterBaPrime = baPrime.apply(str)
|
|
expect(abPrime.equals(baPrime)).to.be.true
|
|
expect(afterAbPrime).to.equal(afterBaPrime)
|
|
})
|
|
)
|
|
|
|
it('throws when invalid operations are applied', function () {
|
|
const operation = new TextOperation().retain(1)
|
|
expect(() => {
|
|
operation.apply('')
|
|
}).to.throw(TextOperation.ApplyError)
|
|
expect(() => {
|
|
operation.apply(' ')
|
|
}).not.to.throw
|
|
})
|
|
|
|
it('throws when insert text contains non BMP chars', function () {
|
|
const operation = new TextOperation()
|
|
const str = '𝌆\n'
|
|
expect(() => {
|
|
operation.insert(str)
|
|
}).to.throw(
|
|
TextOperation.UnprocessableError,
|
|
/inserted text contains non BMP characters/
|
|
)
|
|
})
|
|
|
|
it('throws when base string contains non BMP chars', function () {
|
|
const operation = new TextOperation()
|
|
const str = '𝌆\n'
|
|
expect(() => {
|
|
operation.apply(str)
|
|
}).to.throw(
|
|
TextOperation.UnprocessableError,
|
|
/string contains non BMP characters/
|
|
)
|
|
})
|
|
|
|
it('throws at from JSON when it contains non BMP chars', function () {
|
|
const operation = ['𝌆\n']
|
|
expect(() => {
|
|
TextOperation.fromJSON(operation)
|
|
}).to.throw(
|
|
TextOperation.UnprocessableError,
|
|
/inserted text contains non BMP characters/
|
|
)
|
|
})
|
|
})
|