TextTransformTests.js 17 KB

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