DiffGeneratorTests.coffee 10 KB

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