DiffGeneratorTests.js 15 KB

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