text_operation.test.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. //
  2. // These tests are based on the OT.js tests:
  3. // https://github.com/Operational-Transformation/ot.js/blob/
  4. // 8873b7e28e83f9adbf6c3a28ec639c9151a838ae/test/lib/test-text-operation.js
  5. //
  6. 'use strict'
  7. const { expect } = require('chai')
  8. const random = require('./support/random')
  9. const randomOperation = require('./support/random_text_operation')
  10. const ot = require('..')
  11. const TextOperation = ot.TextOperation
  12. const StringFileData = require('../lib/file_data/string_file_data')
  13. describe('TextOperation', function () {
  14. const numTrials = 500
  15. it('tracks base and target lengths', function () {
  16. const o = new TextOperation()
  17. expect(o.baseLength).to.equal(0)
  18. expect(o.targetLength).to.equal(0)
  19. o.retain(5)
  20. expect(o.baseLength).to.equal(5)
  21. expect(o.targetLength).to.equal(5)
  22. o.insert('abc')
  23. expect(o.baseLength).to.equal(5)
  24. expect(o.targetLength).to.equal(8)
  25. o.retain(2)
  26. expect(o.baseLength).to.equal(7)
  27. expect(o.targetLength).to.equal(10)
  28. o.remove(2)
  29. expect(o.baseLength).to.equal(9)
  30. expect(o.targetLength).to.equal(10)
  31. })
  32. it('supports chaining', function () {
  33. const o = new TextOperation()
  34. .retain(5)
  35. .retain(0)
  36. .insert('lorem')
  37. .insert('')
  38. .remove('abc')
  39. .remove(3)
  40. .remove(0)
  41. .remove('')
  42. expect(o.ops.length).to.equal(3)
  43. })
  44. it('ignores empty operations', function () {
  45. const o = new TextOperation()
  46. o.retain(0)
  47. o.insert('')
  48. o.remove('')
  49. expect(o.ops.length).to.equal(0)
  50. })
  51. it('checks for equality', function () {
  52. const op1 = new TextOperation().remove(1).insert('lo').retain(2).retain(3)
  53. const op2 = new TextOperation().remove(-1).insert('l').insert('o').retain(5)
  54. expect(op1.equals(op2)).to.be.true
  55. op1.remove(1)
  56. op2.retain(1)
  57. expect(op1.equals(op2)).to.be.false
  58. })
  59. it('merges ops', function () {
  60. function last(arr) {
  61. return arr[arr.length - 1]
  62. }
  63. const o = new TextOperation()
  64. expect(o.ops.length).to.equal(0)
  65. o.retain(2)
  66. expect(o.ops.length).to.equal(1)
  67. expect(last(o.ops)).to.equal(2)
  68. o.retain(3)
  69. expect(o.ops.length).to.equal(1)
  70. expect(last(o.ops)).to.equal(5)
  71. o.insert('abc')
  72. expect(o.ops.length).to.equal(2)
  73. expect(last(o.ops)).to.equal('abc')
  74. o.insert('xyz')
  75. expect(o.ops.length).to.equal(2)
  76. expect(last(o.ops)).to.equal('abcxyz')
  77. o.remove('d')
  78. expect(o.ops.length).to.equal(3)
  79. expect(last(o.ops)).to.equal(-1)
  80. o.remove('d')
  81. expect(o.ops.length).to.equal(3)
  82. expect(last(o.ops)).to.equal(-2)
  83. })
  84. it('checks for no-ops', function () {
  85. const o = new TextOperation()
  86. expect(o.isNoop()).to.be.true
  87. o.retain(5)
  88. expect(o.isNoop()).to.be.true
  89. o.retain(3)
  90. expect(o.isNoop()).to.be.true
  91. o.insert('lorem')
  92. expect(o.isNoop()).to.be.false
  93. })
  94. it('converts to string', function () {
  95. const o = new TextOperation()
  96. o.retain(2)
  97. o.insert('lorem')
  98. o.remove('ipsum')
  99. o.retain(5)
  100. expect(o.toString()).to.equal(
  101. "retain 2, insert 'lorem', remove 5, retain 5"
  102. )
  103. })
  104. it('converts from JSON', function () {
  105. const ops = [2, -1, -1, 'cde']
  106. const o = TextOperation.fromJSON({ textOperation: ops })
  107. expect(o.ops.length).to.equal(3)
  108. expect(o.baseLength).to.equal(4)
  109. expect(o.targetLength).to.equal(5)
  110. function assertIncorrectAfter(fn) {
  111. const ops2 = ops.slice(0)
  112. fn(ops2)
  113. expect(() => {
  114. TextOperation.fromJSON({ textOperations: ops2 })
  115. }).to.throw
  116. }
  117. assertIncorrectAfter(ops2 => {
  118. ops2.push({ insert: 'x' })
  119. })
  120. assertIncorrectAfter(ops2 => {
  121. ops2.push(null)
  122. })
  123. })
  124. it(
  125. 'applies (randomised)',
  126. random.test(numTrials, () => {
  127. const str = random.string(50)
  128. const o = randomOperation(str)
  129. expect(str.length).to.equal(o.baseLength)
  130. const file = new StringFileData(str)
  131. o.apply(file)
  132. const result = file.getContent()
  133. expect(result.length).to.equal(o.targetLength)
  134. })
  135. )
  136. it(
  137. 'inverts (randomised)',
  138. random.test(numTrials, () => {
  139. const str = random.string(50)
  140. const o = randomOperation(str)
  141. const p = o.invert(new StringFileData(str))
  142. expect(o.baseLength).to.equal(p.targetLength)
  143. expect(o.targetLength).to.equal(p.baseLength)
  144. const file = new StringFileData(str)
  145. o.apply(file)
  146. p.apply(file)
  147. const result = file.getContent()
  148. expect(result).to.equal(str)
  149. })
  150. )
  151. it(
  152. 'converts to/from JSON (randomised)',
  153. random.test(numTrials, () => {
  154. const doc = random.string(50)
  155. const operation = randomOperation(doc)
  156. const roundTripOperation = TextOperation.fromJSON(operation.toJSON())
  157. expect(operation.equals(roundTripOperation)).to.be.true
  158. })
  159. )
  160. it(
  161. 'composes (randomised)',
  162. random.test(numTrials, () => {
  163. // invariant: apply(str, compose(a, b)) === apply(apply(str, a), b)
  164. const str = random.string(20)
  165. const a = randomOperation(str)
  166. const file = new StringFileData(str)
  167. a.apply(file)
  168. const afterA = file.getContent()
  169. expect(afterA.length).to.equal(a.targetLength)
  170. const b = randomOperation(afterA)
  171. b.apply(file)
  172. const afterB = file.getContent()
  173. expect(afterB.length).to.equal(b.targetLength)
  174. const ab = a.compose(b)
  175. expect(ab.targetLength).to.equal(b.targetLength)
  176. ab.apply(new StringFileData(str))
  177. const afterAB = file.getContent()
  178. expect(afterAB).to.equal(afterB)
  179. })
  180. )
  181. it(
  182. 'transforms (randomised)',
  183. random.test(numTrials, () => {
  184. // invariant: compose(a, b') = compose(b, a')
  185. // where (a', b') = transform(a, b)
  186. const str = random.string(20)
  187. const a = randomOperation(str)
  188. const b = randomOperation(str)
  189. const primes = TextOperation.transform(a, b)
  190. const aPrime = primes[0]
  191. const bPrime = primes[1]
  192. const abPrime = a.compose(bPrime)
  193. const baPrime = b.compose(aPrime)
  194. const abFile = new StringFileData(str)
  195. const baFile = new StringFileData(str)
  196. abPrime.apply(abFile)
  197. baPrime.apply(baFile)
  198. expect(abPrime.equals(baPrime)).to.be.true
  199. expect(abFile.getContent()).to.equal(baFile.getContent())
  200. })
  201. )
  202. it('throws when invalid operations are applied', function () {
  203. const operation = new TextOperation().retain(1)
  204. expect(() => {
  205. operation.apply(new StringFileData(''))
  206. }).to.throw(TextOperation.ApplyError)
  207. expect(() => {
  208. operation.apply(new StringFileData(' '))
  209. }).not.to.throw
  210. })
  211. it('throws when insert text contains non BMP chars', function () {
  212. const operation = new TextOperation()
  213. const str = '𝌆\n'
  214. expect(() => {
  215. operation.insert(str)
  216. }).to.throw(
  217. TextOperation.UnprocessableError,
  218. /inserted text contains non BMP characters/
  219. )
  220. })
  221. it('throws when base string contains non BMP chars', function () {
  222. const operation = new TextOperation()
  223. const str = '𝌆\n'
  224. expect(() => {
  225. operation.apply(new StringFileData(str))
  226. }).to.throw(
  227. TextOperation.UnprocessableError,
  228. /string contains non BMP characters/
  229. )
  230. })
  231. it('throws at from JSON when it contains non BMP chars', function () {
  232. const operation = ['𝌆\n']
  233. expect(() => {
  234. TextOperation.fromJSON({ textOperation: operation })
  235. }).to.throw(
  236. TextOperation.UnprocessableError,
  237. /inserted text contains non BMP characters/
  238. )
  239. })
  240. })