DiffGeneratorTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /* eslint-disable
  2. no-return-assign,
  3. no-unused-vars,
  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. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. const sinon = require('sinon')
  14. const chai = require('chai')
  15. const should = chai.should()
  16. const { expect } = chai
  17. const modulePath = '../../../../app/js/DiffGenerator.js'
  18. const SandboxedModule = require('sandboxed-module')
  19. describe('DiffGenerator', function () {
  20. beforeEach(function () {
  21. this.DiffGenerator = SandboxedModule.require(modulePath, {
  22. requires: {
  23. 'logger-sharelatex': { warn: sinon.stub() }
  24. }
  25. })
  26. this.ts = Date.now()
  27. this.user_id = 'mock-user-id'
  28. this.user_id_2 = 'mock-user-id-2'
  29. return (this.meta = {
  30. start_ts: this.ts,
  31. end_ts: this.ts,
  32. user_id: this.user_id
  33. })
  34. })
  35. describe('rewindOp', function () {
  36. describe('rewinding an insert', function () {
  37. return it('should undo the insert', function () {
  38. const content = 'hello world'
  39. const rewoundContent = this.DiffGenerator.rewindOp(content, {
  40. p: 6,
  41. i: 'wo'
  42. })
  43. return rewoundContent.should.equal('hello rld')
  44. })
  45. })
  46. describe('rewinding a delete', function () {
  47. return it('should undo the delete', function () {
  48. const content = 'hello rld'
  49. const rewoundContent = this.DiffGenerator.rewindOp(content, {
  50. p: 6,
  51. d: 'wo'
  52. })
  53. return rewoundContent.should.equal('hello world')
  54. })
  55. })
  56. describe('with an inconsistent update', function () {
  57. return it('should throw an error', function () {
  58. const content = 'hello world'
  59. return expect(() => {
  60. return this.DiffGenerator.rewindOp(content, { p: 6, i: 'foo' })
  61. }).to.throw(this.DiffGenerator.ConsistencyError)
  62. })
  63. })
  64. return describe('with an update which is beyond the length of the content', function () {
  65. return it('should undo the insert as if it were at the end of the content', function () {
  66. const content = 'foobar'
  67. const rewoundContent = this.DiffGenerator.rewindOp(content, {
  68. p: 4,
  69. i: 'bar'
  70. })
  71. return rewoundContent.should.equal('foo')
  72. })
  73. })
  74. })
  75. describe('rewindUpdate', function () {
  76. return it('should rewind ops in reverse', function () {
  77. const content = 'aaabbbccc'
  78. const update = {
  79. op: [
  80. { p: 3, i: 'bbb' },
  81. { p: 6, i: 'ccc' }
  82. ]
  83. }
  84. const rewoundContent = this.DiffGenerator.rewindUpdate(content, update)
  85. return rewoundContent.should.equal('aaa')
  86. })
  87. })
  88. describe('rewindUpdates', function () {
  89. return it('should rewind updates in reverse', function () {
  90. const content = 'aaabbbccc'
  91. const updates = [
  92. { op: [{ p: 3, i: 'bbb' }] },
  93. { op: [{ p: 6, i: 'ccc' }] }
  94. ]
  95. const rewoundContent = this.DiffGenerator.rewindUpdates(content, updates)
  96. return rewoundContent.should.equal('aaa')
  97. })
  98. })
  99. describe('buildDiff', function () {
  100. beforeEach(function () {
  101. this.diff = [{ u: 'mock-diff' }]
  102. this.content = 'Hello world'
  103. this.updates = [
  104. { i: 'mock-update-1' },
  105. { i: 'mock-update-2' },
  106. { i: 'mock-update-3' }
  107. ]
  108. this.DiffGenerator.applyUpdateToDiff = sinon.stub().returns(this.diff)
  109. this.DiffGenerator.compressDiff = sinon.stub().returns(this.diff)
  110. return (this.result = this.DiffGenerator.buildDiff(
  111. this.content,
  112. this.updates
  113. ))
  114. })
  115. it('should return the diff', function () {
  116. return this.result.should.deep.equal(this.diff)
  117. })
  118. it('should build the content into an initial diff', function () {
  119. return this.DiffGenerator.applyUpdateToDiff
  120. .calledWith(
  121. [
  122. {
  123. u: this.content
  124. }
  125. ],
  126. this.updates[0]
  127. )
  128. .should.equal(true)
  129. })
  130. it('should apply each update', function () {
  131. return Array.from(this.updates).map((update) =>
  132. this.DiffGenerator.applyUpdateToDiff
  133. .calledWith(sinon.match.any, update)
  134. .should.equal(true)
  135. )
  136. })
  137. return it('should compress the diff', function () {
  138. return this.DiffGenerator.compressDiff
  139. .calledWith(this.diff)
  140. .should.equal(true)
  141. })
  142. })
  143. describe('compressDiff', function () {
  144. describe('with adjacent inserts with the same user_id', function () {
  145. return it('should create one update with combined meta data and min/max timestamps', function () {
  146. const diff = this.DiffGenerator.compressDiff([
  147. {
  148. i: 'foo',
  149. meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }
  150. },
  151. {
  152. i: 'bar',
  153. meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }
  154. }
  155. ])
  156. return expect(diff).to.deep.equal([
  157. {
  158. i: 'foobar',
  159. meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }
  160. }
  161. ])
  162. })
  163. })
  164. describe('with adjacent inserts with different user_ids', function () {
  165. return it('should leave the inserts unchanged', function () {
  166. const input = [
  167. {
  168. i: 'foo',
  169. meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }
  170. },
  171. {
  172. i: 'bar',
  173. meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }
  174. }
  175. ]
  176. const output = this.DiffGenerator.compressDiff(input)
  177. return expect(output).to.deep.equal(input)
  178. })
  179. })
  180. describe('with adjacent deletes with the same user_id', function () {
  181. return it('should create one update with combined meta data and min/max timestamps', function () {
  182. const diff = this.DiffGenerator.compressDiff([
  183. {
  184. d: 'foo',
  185. meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }
  186. },
  187. {
  188. d: 'bar',
  189. meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } }
  190. }
  191. ])
  192. return expect(diff).to.deep.equal([
  193. {
  194. d: 'foobar',
  195. meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } }
  196. }
  197. ])
  198. })
  199. })
  200. return describe('with adjacent deletes with different user_ids', function () {
  201. return it('should leave the deletes unchanged', function () {
  202. const input = [
  203. {
  204. d: 'foo',
  205. meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } }
  206. },
  207. {
  208. d: 'bar',
  209. meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } }
  210. }
  211. ]
  212. const output = this.DiffGenerator.compressDiff(input)
  213. return expect(output).to.deep.equal(input)
  214. })
  215. })
  216. })
  217. return describe('applyUpdateToDiff', function () {
  218. describe('an insert', function () {
  219. it('should insert into the middle of (u)nchanged text', function () {
  220. const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
  221. op: [{ p: 3, i: 'baz' }],
  222. meta: this.meta
  223. })
  224. return expect(diff).to.deep.equal([
  225. { u: 'foo' },
  226. { i: 'baz', meta: this.meta },
  227. { u: 'bar' }
  228. ])
  229. })
  230. it('should insert into the start of (u)changed text', function () {
  231. const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
  232. op: [{ p: 0, i: 'baz' }],
  233. meta: this.meta
  234. })
  235. return expect(diff).to.deep.equal([
  236. { i: 'baz', meta: this.meta },
  237. { u: 'foobar' }
  238. ])
  239. })
  240. it('should insert into the end of (u)changed text', function () {
  241. const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
  242. op: [{ p: 6, i: 'baz' }],
  243. meta: this.meta
  244. })
  245. return expect(diff).to.deep.equal([
  246. { u: 'foobar' },
  247. { i: 'baz', meta: this.meta }
  248. ])
  249. })
  250. it('should insert into the middle of (i)inserted text', function () {
  251. const diff = this.DiffGenerator.applyUpdateToDiff(
  252. [{ i: 'foobar', meta: this.meta }],
  253. { op: [{ p: 3, i: 'baz' }], meta: this.meta }
  254. )
  255. return expect(diff).to.deep.equal([
  256. { i: 'foo', meta: this.meta },
  257. { i: 'baz', meta: this.meta },
  258. { i: 'bar', meta: this.meta }
  259. ])
  260. })
  261. return it('should not count deletes in the running length total', function () {
  262. const diff = this.DiffGenerator.applyUpdateToDiff(
  263. [{ d: 'deleted', meta: this.meta }, { u: 'foobar' }],
  264. { op: [{ p: 3, i: 'baz' }], meta: this.meta }
  265. )
  266. return expect(diff).to.deep.equal([
  267. { d: 'deleted', meta: this.meta },
  268. { u: 'foo' },
  269. { i: 'baz', meta: this.meta },
  270. { u: 'bar' }
  271. ])
  272. })
  273. })
  274. return describe('a delete', function () {
  275. describe('deleting unchanged text', function () {
  276. it('should delete from the middle of (u)nchanged text', function () {
  277. const diff = this.DiffGenerator.applyUpdateToDiff(
  278. [{ u: 'foobazbar' }],
  279. { op: [{ p: 3, d: 'baz' }], meta: this.meta }
  280. )
  281. return expect(diff).to.deep.equal([
  282. { u: 'foo' },
  283. { d: 'baz', meta: this.meta },
  284. { u: 'bar' }
  285. ])
  286. })
  287. it('should delete from the start of (u)nchanged text', function () {
  288. const diff = this.DiffGenerator.applyUpdateToDiff(
  289. [{ u: 'foobazbar' }],
  290. { op: [{ p: 0, d: 'foo' }], meta: this.meta }
  291. )
  292. return expect(diff).to.deep.equal([
  293. { d: 'foo', meta: this.meta },
  294. { u: 'bazbar' }
  295. ])
  296. })
  297. it('should delete from the end of (u)nchanged text', function () {
  298. const diff = this.DiffGenerator.applyUpdateToDiff(
  299. [{ u: 'foobazbar' }],
  300. { op: [{ p: 6, d: 'bar' }], meta: this.meta }
  301. )
  302. return expect(diff).to.deep.equal([
  303. { u: 'foobaz' },
  304. { d: 'bar', meta: this.meta }
  305. ])
  306. })
  307. return it('should delete across multiple (u)changed text parts', function () {
  308. const diff = this.DiffGenerator.applyUpdateToDiff(
  309. [{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }],
  310. { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
  311. )
  312. return expect(diff).to.deep.equal([
  313. { u: 'fo' },
  314. { d: 'o', meta: this.meta },
  315. { d: 'baz', meta: this.meta },
  316. { d: 'b', meta: this.meta },
  317. { u: 'ar' }
  318. ])
  319. })
  320. })
  321. describe('deleting inserts', function () {
  322. it('should delete from the middle of (i)nserted text', function () {
  323. const diff = this.DiffGenerator.applyUpdateToDiff(
  324. [{ i: 'foobazbar', meta: this.meta }],
  325. { op: [{ p: 3, d: 'baz' }], meta: this.meta }
  326. )
  327. return expect(diff).to.deep.equal([
  328. { i: 'foo', meta: this.meta },
  329. { i: 'bar', meta: this.meta }
  330. ])
  331. })
  332. it('should delete from the start of (u)nchanged text', function () {
  333. const diff = this.DiffGenerator.applyUpdateToDiff(
  334. [{ i: 'foobazbar', meta: this.meta }],
  335. { op: [{ p: 0, d: 'foo' }], meta: this.meta }
  336. )
  337. return expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }])
  338. })
  339. it('should delete from the end of (u)nchanged text', function () {
  340. const diff = this.DiffGenerator.applyUpdateToDiff(
  341. [{ i: 'foobazbar', meta: this.meta }],
  342. { op: [{ p: 6, d: 'bar' }], meta: this.meta }
  343. )
  344. return expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }])
  345. })
  346. return it('should delete across multiple (u)changed and (i)nserted text parts', function () {
  347. const diff = this.DiffGenerator.applyUpdateToDiff(
  348. [{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }],
  349. { op: [{ p: 2, d: 'obazb' }], meta: this.meta }
  350. )
  351. return expect(diff).to.deep.equal([
  352. { u: 'fo' },
  353. { d: 'o', meta: this.meta },
  354. { d: 'b', meta: this.meta },
  355. { u: 'ar' }
  356. ])
  357. })
  358. })
  359. describe('deleting over existing deletes', function () {
  360. return it('should delete across multiple (u)changed and (d)deleted text parts', function () {
  361. const diff = this.DiffGenerator.applyUpdateToDiff(
  362. [{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }],
  363. { op: [{ p: 2, d: 'ob' }], meta: this.meta }
  364. )
  365. return expect(diff).to.deep.equal([
  366. { u: 'fo' },
  367. { d: 'o', meta: this.meta },
  368. { d: 'baz', meta: this.meta },
  369. { d: 'b', meta: this.meta },
  370. { u: 'ar' }
  371. ])
  372. })
  373. })
  374. describe("deleting when the text doesn't match", function () {
  375. it('should throw an error when deleting from the middle of (u)nchanged text', function () {
  376. return expect(() =>
  377. this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
  378. op: [{ p: 3, d: 'xxx' }],
  379. meta: this.meta
  380. })
  381. ).to.throw(this.DiffGenerator.ConsistencyError)
  382. })
  383. it('should throw an error when deleting from the start of (u)nchanged text', function () {
  384. return expect(() =>
  385. this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
  386. op: [{ p: 0, d: 'xxx' }],
  387. meta: this.meta
  388. })
  389. ).to.throw(this.DiffGenerator.ConsistencyError)
  390. })
  391. return it('should throw an error when deleting from the end of (u)nchanged text', function () {
  392. return expect(() =>
  393. this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
  394. op: [{ p: 6, d: 'xxx' }],
  395. meta: this.meta
  396. })
  397. ).to.throw(this.DiffGenerator.ConsistencyError)
  398. })
  399. })
  400. describe('when the last update in the existing diff is a delete', function () {
  401. return it('should insert the new update before the delete', function () {
  402. const diff = this.DiffGenerator.applyUpdateToDiff(
  403. [{ u: 'foo' }, { d: 'bar', meta: this.meta }],
  404. { op: [{ p: 3, i: 'baz' }], meta: this.meta }
  405. )
  406. return expect(diff).to.deep.equal([
  407. { u: 'foo' },
  408. { i: 'baz', meta: this.meta },
  409. { d: 'bar', meta: this.meta }
  410. ])
  411. })
  412. })
  413. return describe('when the only update in the existing diff is a delete', function () {
  414. return it('should insert the new update after the delete', function () {
  415. const diff = this.DiffGenerator.applyUpdateToDiff(
  416. [{ d: 'bar', meta: this.meta }],
  417. { op: [{ p: 0, i: 'baz' }], meta: this.meta }
  418. )
  419. return expect(diff).to.deep.equal([
  420. { d: 'bar', meta: this.meta },
  421. { i: 'baz', meta: this.meta }
  422. ])
  423. })
  424. })
  425. })
  426. })
  427. })