DiffGeneratorTests.js 13 KB

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