TextTransformTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /* eslint-disable
  2. mocha/no-identical-title,
  3. no-return-assign,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS202: Simplify dynamic range loops
  12. * DS205: Consider reworking code to avoid use of IIFEs
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. const text = require('../../../../app/js/sharejs/types/text')
  16. const RangesTracker = require('@overleaf/ranges-tracker')
  17. describe('ShareJS text type', function () {
  18. beforeEach(function () {
  19. return (this.t = 'mock-thread-id')
  20. })
  21. describe('transform', function () {
  22. describe('insert / insert', function () {
  23. it('with an insert before', function () {
  24. const dest = []
  25. text._tc(dest, { i: 'foo', p: 9 }, { i: 'bar', p: 3 })
  26. return dest.should.deep.equal([{ i: 'foo', p: 12 }])
  27. })
  28. it('with an insert after', function () {
  29. const dest = []
  30. text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 9 })
  31. return dest.should.deep.equal([{ i: 'foo', p: 3 }])
  32. })
  33. it("with an insert at the same place with side == 'right'", function () {
  34. const dest = []
  35. text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right')
  36. return dest.should.deep.equal([{ i: 'foo', p: 6 }])
  37. })
  38. return it("with an insert at the same place with side == 'left'", function () {
  39. const dest = []
  40. text._tc(dest, { i: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left')
  41. return dest.should.deep.equal([{ i: 'foo', p: 3 }])
  42. })
  43. })
  44. describe('insert / delete', function () {
  45. it('with a delete before', function () {
  46. const dest = []
  47. text._tc(dest, { i: 'foo', p: 9 }, { d: 'bar', p: 3 })
  48. return dest.should.deep.equal([{ i: 'foo', p: 6 }])
  49. })
  50. it('with a delete after', function () {
  51. const dest = []
  52. text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 9 })
  53. return dest.should.deep.equal([{ i: 'foo', p: 3 }])
  54. })
  55. it("with a delete at the same place with side == 'right'", function () {
  56. const dest = []
  57. text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'right')
  58. return dest.should.deep.equal([{ i: 'foo', p: 3 }])
  59. })
  60. return it("with a delete at the same place with side == 'left'", function () {
  61. const dest = []
  62. text._tc(dest, { i: 'foo', p: 3 }, { d: 'bar', p: 3 }, 'left')
  63. return dest.should.deep.equal([{ i: 'foo', p: 3 }])
  64. })
  65. })
  66. describe('delete / insert', function () {
  67. it('with an insert before', function () {
  68. const dest = []
  69. text._tc(dest, { d: 'foo', p: 9 }, { i: 'bar', p: 3 })
  70. return dest.should.deep.equal([{ d: 'foo', p: 12 }])
  71. })
  72. it('with an insert after', function () {
  73. const dest = []
  74. text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 9 })
  75. return dest.should.deep.equal([{ d: 'foo', p: 3 }])
  76. })
  77. it("with an insert at the same place with side == 'right'", function () {
  78. const dest = []
  79. text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'right')
  80. return dest.should.deep.equal([{ d: 'foo', p: 6 }])
  81. })
  82. it("with an insert at the same place with side == 'left'", function () {
  83. const dest = []
  84. text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 3 }, 'left')
  85. return dest.should.deep.equal([{ d: 'foo', p: 6 }])
  86. })
  87. return it('with a delete that overlaps the insert location', function () {
  88. const dest = []
  89. text._tc(dest, { d: 'foo', p: 3 }, { i: 'bar', p: 4 })
  90. return dest.should.deep.equal([
  91. { d: 'f', p: 3 },
  92. { d: 'oo', p: 6 },
  93. ])
  94. })
  95. })
  96. describe('delete / delete', function () {
  97. it('with a delete before', function () {
  98. const dest = []
  99. text._tc(dest, { d: 'foo', p: 9 }, { d: 'bar', p: 3 })
  100. return dest.should.deep.equal([{ d: 'foo', p: 6 }])
  101. })
  102. it('with a delete after', function () {
  103. const dest = []
  104. text._tc(dest, { d: 'foo', p: 3 }, { d: 'bar', p: 9 })
  105. return dest.should.deep.equal([{ d: 'foo', p: 3 }])
  106. })
  107. it('with deleting the same content', function () {
  108. const dest = []
  109. text._tc(dest, { d: 'foo', p: 3 }, { d: 'foo', p: 3 }, 'right')
  110. return dest.should.deep.equal([])
  111. })
  112. it('with the delete overlapping before', function () {
  113. const dest = []
  114. text._tc(dest, { d: 'foobar', p: 3 }, { d: 'abcfoo', p: 0 }, 'right')
  115. return dest.should.deep.equal([{ d: 'bar', p: 0 }])
  116. })
  117. it('with the delete overlapping after', function () {
  118. const dest = []
  119. text._tc(dest, { d: 'abcfoo', p: 3 }, { d: 'foobar', p: 6 })
  120. return dest.should.deep.equal([{ d: 'abc', p: 3 }])
  121. })
  122. it('with the delete overlapping the whole delete', function () {
  123. const dest = []
  124. text._tc(dest, { d: 'abcfoo123', p: 3 }, { d: 'foo', p: 6 })
  125. return dest.should.deep.equal([{ d: 'abc123', p: 3 }])
  126. })
  127. return it('with the delete inside the whole delete', function () {
  128. const dest = []
  129. text._tc(dest, { d: 'foo', p: 6 }, { d: 'abcfoo123', p: 3 })
  130. return dest.should.deep.equal([])
  131. })
  132. })
  133. describe('comment / insert', function () {
  134. it('with an insert before', function () {
  135. const dest = []
  136. text._tc(dest, { c: 'foo', p: 9, t: this.t }, { i: 'bar', p: 3 })
  137. return dest.should.deep.equal([{ c: 'foo', p: 12, t: this.t }])
  138. })
  139. it('with an insert after', function () {
  140. const dest = []
  141. text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 })
  142. return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
  143. })
  144. it('with an insert at the left edge', function () {
  145. const dest = []
  146. text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 3 })
  147. // RangesTracker doesn't inject inserts into comments on edges, so neither should we
  148. return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
  149. })
  150. it('with an insert at the right edge', function () {
  151. const dest = []
  152. text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 6 })
  153. // RangesTracker doesn't inject inserts into comments on edges, so neither should we
  154. return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
  155. })
  156. return it('with an insert in the middle', function () {
  157. const dest = []
  158. text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 5 })
  159. return dest.should.deep.equal([{ c: 'fobaro', p: 3, t: this.t }])
  160. })
  161. })
  162. describe('comment / delete', function () {
  163. it('with a delete before', function () {
  164. const dest = []
  165. text._tc(dest, { c: 'foo', p: 9, t: this.t }, { d: 'bar', p: 3 })
  166. return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
  167. })
  168. it('with a delete after', function () {
  169. const dest = []
  170. text._tc(dest, { c: 'foo', p: 3, t: this.t }, { i: 'bar', p: 9 })
  171. return dest.should.deep.equal([{ c: 'foo', p: 3, t: this.t }])
  172. })
  173. it('with a delete overlapping the comment content before', function () {
  174. const dest = []
  175. text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: '123foo', p: 3 })
  176. return dest.should.deep.equal([{ c: 'bar', p: 3, t: this.t }])
  177. })
  178. it('with a delete overlapping the comment content after', function () {
  179. const dest = []
  180. text._tc(dest, { c: 'foobar', p: 6, t: this.t }, { d: 'bar123', p: 9 })
  181. return dest.should.deep.equal([{ c: 'foo', p: 6, t: this.t }])
  182. })
  183. it('with a delete overlapping the comment content in the middle', function () {
  184. const dest = []
  185. text._tc(dest, { c: 'foo123bar', p: 6, t: this.t }, { d: '123', p: 9 })
  186. return dest.should.deep.equal([{ c: 'foobar', p: 6, t: this.t }])
  187. })
  188. return it('with a delete overlapping the whole comment', function () {
  189. const dest = []
  190. text._tc(dest, { c: 'foo', p: 6, t: this.t }, { d: '123foo456', p: 3 })
  191. return dest.should.deep.equal([{ c: '', p: 3, t: this.t }])
  192. })
  193. })
  194. describe('comment / insert', function () {
  195. return it('should not do anything', function () {
  196. const dest = []
  197. text._tc(dest, { i: 'foo', p: 6 }, { c: 'bar', p: 3 })
  198. return dest.should.deep.equal([{ i: 'foo', p: 6 }])
  199. })
  200. })
  201. describe('comment / delete', function () {
  202. return it('should not do anything', function () {
  203. const dest = []
  204. text._tc(dest, { d: 'foo', p: 6 }, { c: 'bar', p: 3 })
  205. return dest.should.deep.equal([{ d: 'foo', p: 6 }])
  206. })
  207. })
  208. return describe('comment / comment', function () {
  209. return it('should not do anything', function () {
  210. const dest = []
  211. text._tc(dest, { c: 'foo', p: 6 }, { c: 'bar', p: 3 })
  212. return dest.should.deep.equal([{ c: 'foo', p: 6 }])
  213. })
  214. })
  215. })
  216. describe('apply', function () {
  217. it('should apply an insert', function () {
  218. return text.apply('foo', [{ i: 'bar', p: 2 }]).should.equal('fobaro')
  219. })
  220. it('should apply a delete', function () {
  221. return text
  222. .apply('foo123bar', [{ d: '123', p: 3 }])
  223. .should.equal('foobar')
  224. })
  225. it('should do nothing with a comment', function () {
  226. return text
  227. .apply('foo123bar', [{ c: '123', p: 3 }])
  228. .should.equal('foo123bar')
  229. })
  230. it('should throw an error when deleted content does not match', function () {
  231. return (() => text.apply('foo123bar', [{ d: '456', p: 3 }])).should.throw(
  232. Error
  233. )
  234. })
  235. return it('should throw an error when comment content does not match', function () {
  236. return (() => text.apply('foo123bar', [{ c: '456', p: 3 }])).should.throw(
  237. Error
  238. )
  239. })
  240. })
  241. return describe('applying ops and comments in different orders', function () {
  242. return it('should not matter which op or comment is applied first', function () {
  243. let length, p
  244. let asc, end
  245. let asc1, end1
  246. let asc3, end3
  247. const transform = function (op1, op2, side) {
  248. const d = []
  249. text._tc(d, op1, op2, side)
  250. return d
  251. }
  252. const applySnapshot = (snapshot, op) => text.apply(snapshot, op)
  253. const applyRanges = function (rangesTracker, ops) {
  254. for (const op of Array.from(ops)) {
  255. rangesTracker.applyOp(op, {})
  256. }
  257. return rangesTracker
  258. }
  259. const commentsEqual = function (comments1, comments2) {
  260. if (comments1.length !== comments2.length) {
  261. return false
  262. }
  263. comments1.sort((a, b) => {
  264. if (a.offset - b.offset === 0) {
  265. return a.length - b.length
  266. } else {
  267. return a.offset - b.offset
  268. }
  269. })
  270. comments2.sort((a, b) => {
  271. if (a.offset - b.offset === 0) {
  272. return a.length - b.length
  273. } else {
  274. return a.offset - b.offset
  275. }
  276. })
  277. for (let i = 0; i < comments1.length; i++) {
  278. const comment1 = comments1[i]
  279. const comment2 = comments2[i]
  280. if (
  281. comment1.offset !== comment2.offset ||
  282. comment1.length !== comment2.length
  283. ) {
  284. return false
  285. }
  286. }
  287. return true
  288. }
  289. const SNAPSHOT = '123'
  290. const OPS = []
  291. // Insert ops
  292. for (
  293. p = 0, end = SNAPSHOT.length, asc = end >= 0;
  294. asc ? p <= end : p >= end;
  295. asc ? p++ : p--
  296. ) {
  297. OPS.push({ i: 'a', p })
  298. OPS.push({ i: 'bc', p })
  299. }
  300. for (
  301. p = 0, end1 = SNAPSHOT.length - 1, asc1 = end1 >= 0;
  302. asc1 ? p <= end1 : p >= end1;
  303. asc1 ? p++ : p--
  304. ) {
  305. let asc2, end2
  306. for (
  307. length = 1, end2 = SNAPSHOT.length - p, asc2 = end2 >= 1;
  308. asc2 ? length <= end2 : length >= end2;
  309. asc2 ? length++ : length--
  310. ) {
  311. OPS.push({ d: SNAPSHOT.slice(p, p + length), p })
  312. }
  313. }
  314. for (
  315. p = 0, end3 = SNAPSHOT.length - 1, asc3 = end3 >= 0;
  316. asc3 ? p <= end3 : p >= end3;
  317. asc3 ? p++ : p--
  318. ) {
  319. let asc4, end4
  320. for (
  321. length = 1, end4 = SNAPSHOT.length - p, asc4 = end4 >= 1;
  322. asc4 ? length <= end4 : length >= end4;
  323. asc4 ? length++ : length--
  324. ) {
  325. OPS.push({ c: SNAPSHOT.slice(p, p + length), p, t: this.t })
  326. }
  327. }
  328. return (() => {
  329. const result = []
  330. for (const op1 of Array.from(OPS)) {
  331. result.push(
  332. (() => {
  333. const result1 = []
  334. for (const op2 of Array.from(OPS)) {
  335. const op1T = transform(op1, op2, 'left')
  336. const op2T = transform(op2, op1, 'right')
  337. const rt12 = new RangesTracker()
  338. const snapshot12 = applySnapshot(
  339. applySnapshot(SNAPSHOT, [op1]),
  340. op2T
  341. )
  342. applyRanges(rt12, [op1])
  343. applyRanges(rt12, op2T)
  344. const rt21 = new RangesTracker()
  345. const snapshot21 = applySnapshot(
  346. applySnapshot(SNAPSHOT, [op2]),
  347. op1T
  348. )
  349. applyRanges(rt21, [op2])
  350. applyRanges(rt21, op1T)
  351. if (snapshot12 !== snapshot21) {
  352. console.error(
  353. {
  354. op1,
  355. op2,
  356. op1T,
  357. op2T,
  358. snapshot12,
  359. snapshot21,
  360. },
  361. 'Ops are not consistent'
  362. )
  363. throw new Error('OT is inconsistent')
  364. }
  365. if (!commentsEqual(rt12.comments, rt21.comments)) {
  366. console.log(rt12.comments)
  367. console.log(rt21.comments)
  368. console.error(
  369. {
  370. op1,
  371. op2,
  372. op1T,
  373. op2T,
  374. rt12_comments: rt12.comments,
  375. rt21_comments: rt21.comments,
  376. },
  377. 'Comments are not consistent'
  378. )
  379. throw new Error('OT is inconsistent')
  380. } else {
  381. result1.push(undefined)
  382. }
  383. }
  384. return result1
  385. })()
  386. )
  387. }
  388. return result
  389. })()
  390. })
  391. })
  392. })