DiffGeneratorTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. expect = chai.expect
  5. modulePath = "../../../../app/js/DiffGenerator.js"
  6. SandboxedModule = require('sandboxed-module')
  7. describe "DiffGenerator", ->
  8. beforeEach ->
  9. @DiffGenerator = SandboxedModule.require modulePath, requires:
  10. "logger-sharelatex": { warn: sinon.stub() }
  11. @ts = Date.now()
  12. @user_id = "mock-user-id"
  13. @user_id_2 = "mock-user-id-2"
  14. @meta = {
  15. start_ts: @ts, end_ts: @ts, user_id: @user_id
  16. }
  17. describe "rewindOp", ->
  18. describe "rewinding an insert", ->
  19. it "should undo the insert", ->
  20. content = "hello world"
  21. rewoundContent = @DiffGenerator.rewindOp content, { p: 6, i: "wo" }
  22. rewoundContent.should.equal "hello rld"
  23. describe "rewinding a delete", ->
  24. it "should undo the delete", ->
  25. content = "hello rld"
  26. rewoundContent = @DiffGenerator.rewindOp content, { p: 6, d: "wo" }
  27. rewoundContent.should.equal "hello world"
  28. describe "with an inconsistent update", ->
  29. it "should throw an error", ->
  30. content = "hello world"
  31. expect( () =>
  32. @DiffGenerator.rewindOp content, { p: 6, i: "foo" }
  33. ).to.throw(@DiffGenerator.ConsistencyError)
  34. describe "with an update which is beyond the length of the content", ->
  35. it "should undo the insert as if it were at the end of the content", ->
  36. content = "foobar"
  37. rewoundContent = @DiffGenerator.rewindOp content, { p: 4, i: "bar" }
  38. rewoundContent.should.equal "foo"
  39. describe "rewindUpdate", ->
  40. it "should rewind ops in reverse", ->
  41. content = "aaabbbccc"
  42. update =
  43. op: [{ p: 3, i: "bbb" }, { p: 6, i: "ccc" }]
  44. rewoundContent = @DiffGenerator.rewindUpdate content, update
  45. rewoundContent.should.equal "aaa"
  46. describe "rewindUpdates", ->
  47. it "should rewind updates in reverse", ->
  48. content = "aaabbbccc"
  49. updates = [
  50. { op: [{ p: 3, i: "bbb" }] },
  51. { op: [{ p: 6, i: "ccc" }] }
  52. ]
  53. rewoundContent = @DiffGenerator.rewindUpdates content, updates
  54. rewoundContent.should.equal "aaa"
  55. describe "buildDiff", ->
  56. beforeEach ->
  57. @diff = [ u: "mock-diff" ]
  58. @content = "Hello world"
  59. @updates = [
  60. { i: "mock-update-1" }
  61. { i: "mock-update-2" }
  62. { i: "mock-update-3" }
  63. ]
  64. @DiffGenerator.applyUpdateToDiff = sinon.stub().returns(@diff)
  65. @DiffGenerator.compressDiff = sinon.stub().returns(@diff)
  66. @result = @DiffGenerator.buildDiff(@content, @updates)
  67. it "should return the diff", ->
  68. @result.should.deep.equal @diff
  69. it "should build the content into an initial diff", ->
  70. @DiffGenerator.applyUpdateToDiff
  71. .calledWith([{
  72. u: @content
  73. }], @updates[0])
  74. .should.equal true
  75. it "should apply each update", ->
  76. for update in @updates
  77. @DiffGenerator.applyUpdateToDiff
  78. .calledWith(sinon.match.any, update)
  79. .should.equal true
  80. it "should compress the diff", ->
  81. @DiffGenerator.compressDiff
  82. .calledWith(@diff)
  83. .should.equal true
  84. describe "compressDiff", ->
  85. describe "with adjacent inserts with the same user_id", ->
  86. it "should create one update with combined meta data and min/max timestamps", ->
  87. diff = @DiffGenerator.compressDiff([
  88. { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
  89. { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: @user_id } }}
  90. ])
  91. expect(diff).to.deep.equal([
  92. { i: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: @user_id } }}
  93. ])
  94. describe "with adjacent inserts with different user_ids", ->
  95. it "should leave the inserts unchanged", ->
  96. input = [
  97. { i: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
  98. { i: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: @user_id_2 } }}
  99. ]
  100. output = @DiffGenerator.compressDiff(input)
  101. expect(output).to.deep.equal(input)
  102. describe "with adjacent deletes with the same user_id", ->
  103. it "should create one update with combined meta data and min/max timestamps", ->
  104. diff = @DiffGenerator.compressDiff([
  105. { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
  106. { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: @user_id } }}
  107. ])
  108. expect(diff).to.deep.equal([
  109. { d: "foobar", meta: { start_ts: 5, end_ts: 20, user: { id: @user_id } }}
  110. ])
  111. describe "with adjacent deletes with different user_ids", ->
  112. it "should leave the deletes unchanged", ->
  113. input = [
  114. { d: "foo", meta: { start_ts: 10, end_ts: 20, user: { id: @user_id } }}
  115. { d: "bar", meta: { start_ts: 5, end_ts: 15, user: { id: @user_id_2 } }}
  116. ]
  117. output = @DiffGenerator.compressDiff(input)
  118. expect(output).to.deep.equal(input)
  119. describe "applyUpdateToDiff", ->
  120. describe "an insert", ->
  121. it "should insert into the middle of (u)nchanged text", ->
  122. diff = @DiffGenerator.applyUpdateToDiff(
  123. [ { u: "foobar" } ],
  124. { op: [{ p: 3, i: "baz" }], meta: @meta }
  125. )
  126. expect(diff).to.deep.equal([
  127. { u: "foo" }
  128. { i: "baz", meta: @meta }
  129. { u: "bar" }
  130. ])
  131. it "should insert into the start of (u)changed text", ->
  132. diff = @DiffGenerator.applyUpdateToDiff(
  133. [ { u: "foobar" } ],
  134. { op: [{ p: 0, i: "baz" }], meta: @meta }
  135. )
  136. expect(diff).to.deep.equal([
  137. { i: "baz", meta: @meta }
  138. { u: "foobar" }
  139. ])
  140. it "should insert into the end of (u)changed text", ->
  141. diff = @DiffGenerator.applyUpdateToDiff(
  142. [ { u: "foobar" } ],
  143. { op: [{ p: 6, i: "baz" }], meta: @meta }
  144. )
  145. expect(diff).to.deep.equal([
  146. { u: "foobar" }
  147. { i: "baz", meta: @meta }
  148. ])
  149. it "should insert into the middle of (i)inserted text", ->
  150. diff = @DiffGenerator.applyUpdateToDiff(
  151. [ { i: "foobar", meta: @meta } ],
  152. { op: [{ p: 3, i: "baz" }], meta: @meta }
  153. )
  154. expect(diff).to.deep.equal([
  155. { i: "foo", meta: @meta }
  156. { i: "baz", meta: @meta }
  157. { i: "bar", meta: @meta }
  158. ])
  159. it "should not count deletes in the running length total", ->
  160. diff = @DiffGenerator.applyUpdateToDiff(
  161. [
  162. { d: "deleted", meta: @meta }
  163. { u: "foobar" }
  164. ],
  165. { op: [{ p: 3, i: "baz" }], meta: @meta }
  166. )
  167. expect(diff).to.deep.equal([
  168. { d: "deleted", meta: @meta }
  169. { u: "foo" }
  170. { i: "baz", meta: @meta }
  171. { u: "bar" }
  172. ])
  173. describe "a delete", ->
  174. describe "deleting unchanged text", ->
  175. it "should delete from the middle of (u)nchanged text", ->
  176. diff = @DiffGenerator.applyUpdateToDiff(
  177. [ { u: "foobazbar" } ],
  178. { op: [{ p: 3, d: "baz" }], meta: @meta }
  179. )
  180. expect(diff).to.deep.equal([
  181. { u: "foo" }
  182. { d: "baz", meta: @meta }
  183. { u: "bar" }
  184. ])
  185. it "should delete from the start of (u)nchanged text", ->
  186. diff = @DiffGenerator.applyUpdateToDiff(
  187. [ { u: "foobazbar" } ],
  188. { op: [{ p: 0, d: "foo" }], meta: @meta }
  189. )
  190. expect(diff).to.deep.equal([
  191. { d: "foo", meta: @meta }
  192. { u: "bazbar" }
  193. ])
  194. it "should delete from the end of (u)nchanged text", ->
  195. diff = @DiffGenerator.applyUpdateToDiff(
  196. [ { u: "foobazbar" } ],
  197. { op: [{ p: 6, d: "bar" }], meta: @meta }
  198. )
  199. expect(diff).to.deep.equal([
  200. { u: "foobaz" }
  201. { d: "bar", meta: @meta }
  202. ])
  203. it "should delete across multiple (u)changed text parts", ->
  204. diff = @DiffGenerator.applyUpdateToDiff(
  205. [ { u: "foo" }, { u: "baz" }, { u: "bar" } ],
  206. { op: [{ p: 2, d: "obazb" }], meta: @meta }
  207. )
  208. expect(diff).to.deep.equal([
  209. { u: "fo" }
  210. { d: "o", meta: @meta }
  211. { d: "baz", meta: @meta }
  212. { d: "b", meta: @meta }
  213. { u: "ar" }
  214. ])
  215. describe "deleting inserts", ->
  216. it "should delete from the middle of (i)nserted text", ->
  217. diff = @DiffGenerator.applyUpdateToDiff(
  218. [ { i: "foobazbar", meta: @meta } ],
  219. { op: [{ p: 3, d: "baz" }], meta: @meta }
  220. )
  221. expect(diff).to.deep.equal([
  222. { i: "foo", meta: @meta }
  223. { i: "bar", meta: @meta }
  224. ])
  225. it "should delete from the start of (u)nchanged text", ->
  226. diff = @DiffGenerator.applyUpdateToDiff(
  227. [ { i: "foobazbar", meta: @meta } ],
  228. { op: [{ p: 0, d: "foo" }], meta: @meta }
  229. )
  230. expect(diff).to.deep.equal([
  231. { i: "bazbar", meta: @meta }
  232. ])
  233. it "should delete from the end of (u)nchanged text", ->
  234. diff = @DiffGenerator.applyUpdateToDiff(
  235. [ { i: "foobazbar", meta: @meta } ],
  236. { op: [{ p: 6, d: "bar" }], meta: @meta }
  237. )
  238. expect(diff).to.deep.equal([
  239. { i: "foobaz", meta: @meta }
  240. ])
  241. it "should delete across multiple (u)changed and (i)nserted text parts", ->
  242. diff = @DiffGenerator.applyUpdateToDiff(
  243. [ { u: "foo" }, { i: "baz", meta: @meta }, { u: "bar" } ],
  244. { op: [{ p: 2, d: "obazb" }], meta: @meta }
  245. )
  246. expect(diff).to.deep.equal([
  247. { u: "fo" }
  248. { d: "o", meta: @meta }
  249. { d: "b", meta: @meta }
  250. { u: "ar" }
  251. ])
  252. describe "deleting over existing deletes", ->
  253. it "should delete across multiple (u)changed and (d)deleted text parts", ->
  254. diff = @DiffGenerator.applyUpdateToDiff(
  255. [ { u: "foo" }, { d: "baz", meta: @meta }, { u: "bar" } ],
  256. { op: [{ p: 2, d: "ob" }], meta: @meta }
  257. )
  258. expect(diff).to.deep.equal([
  259. { u: "fo" }
  260. { d: "o", meta: @meta }
  261. { d: "baz", meta: @meta }
  262. { d: "b", meta: @meta }
  263. { u: "ar" }
  264. ])
  265. describe "deleting when the text doesn't match", ->
  266. it "should throw an error when deleting from the middle of (u)nchanged text", ->
  267. expect(
  268. () => @DiffGenerator.applyUpdateToDiff(
  269. [ { u: "foobazbar" } ],
  270. { op: [{ p: 3, d: "xxx" }], meta: @meta }
  271. )
  272. ).to.throw(@DiffGenerator.ConsistencyError)
  273. it "should throw an error when deleting from the start of (u)nchanged text", ->
  274. expect(
  275. () => @DiffGenerator.applyUpdateToDiff(
  276. [ { u: "foobazbar" } ],
  277. { op: [{ p: 0, d: "xxx" }], meta: @meta }
  278. )
  279. ).to.throw(@DiffGenerator.ConsistencyError)
  280. it "should throw an error when deleting from the end of (u)nchanged text", ->
  281. expect(
  282. () => @DiffGenerator.applyUpdateToDiff(
  283. [ { u: "foobazbar" } ],
  284. { op: [{ p: 6, d: "xxx" }] , meta: @meta }
  285. )
  286. ).to.throw(@DiffGenerator.ConsistencyError)
  287. describe "when the last update in the existing diff is a delete", ->
  288. it "should insert the new update before the delete", ->
  289. diff = @DiffGenerator.applyUpdateToDiff(
  290. [ { u: "foo" }, { d: "bar", meta: @meta } ],
  291. { op: [{ p: 3, i: "baz" }], meta: @meta }
  292. )
  293. expect(diff).to.deep.equal([
  294. { u: "foo" }
  295. { i: "baz", meta: @meta }
  296. { d: "bar", meta: @meta }
  297. ])
  298. describe "when the only update in the existing diff is a delete", ->
  299. it "should insert the new update after the delete", ->
  300. diff = @DiffGenerator.applyUpdateToDiff(
  301. [ { d: "bar", meta: @meta } ],
  302. { op: [{ p: 0, i: "baz" }], meta: @meta }
  303. )
  304. expect(diff).to.deep.equal([
  305. { d: "bar", meta: @meta }
  306. { i: "baz", meta: @meta }
  307. ])