| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- //
- // 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/
- )
- })
- })
|