TextTransformTests.coffee 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. text = require "../../../../app/js/sharejs/types/text"
  2. require("chai").should()
  3. RangesTracker = require "../../../../app/js/RangesTracker"
  4. describe "ShareJS text type", ->
  5. beforeEach ->
  6. @t = "mock-thread-id"
  7. describe "transform", ->
  8. describe "insert / insert", ->
  9. it "with an insert before", ->
  10. dest = []
  11. text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 })
  12. dest.should.deep.equal [{ i: "foo", p: 12 }]
  13. it "with an insert after", ->
  14. dest = []
  15. text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 })
  16. dest.should.deep.equal [{ i: "foo", p: 3 }]
  17. it "with an insert at the same place with side == 'right'", ->
  18. dest = []
  19. text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
  20. dest.should.deep.equal [{ i: "foo", p: 6 }]
  21. it "with an insert at the same place with side == 'left'", ->
  22. dest = []
  23. text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
  24. dest.should.deep.equal [{ i: "foo", p: 3 }]
  25. describe "insert / delete", ->
  26. it "with a delete before", ->
  27. dest = []
  28. text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 })
  29. dest.should.deep.equal [{ i: "foo", p: 6 }]
  30. it "with a delete after", ->
  31. dest = []
  32. text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 })
  33. dest.should.deep.equal [{ i: "foo", p: 3 }]
  34. it "with a delete at the same place with side == 'right'", ->
  35. dest = []
  36. text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right')
  37. dest.should.deep.equal [{ i: "foo", p: 3 }]
  38. it "with a delete at the same place with side == 'left'", ->
  39. dest = []
  40. text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left')
  41. dest.should.deep.equal [{ i: "foo", p: 3 }]
  42. describe "delete / insert", ->
  43. it "with an insert before", ->
  44. dest = []
  45. text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 })
  46. dest.should.deep.equal [{ d: "foo", p: 12 }]
  47. it "with an insert after", ->
  48. dest = []
  49. text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 })
  50. dest.should.deep.equal [{ d: "foo", p: 3 }]
  51. it "with an insert at the same place with side == 'right'", ->
  52. dest = []
  53. text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
  54. dest.should.deep.equal [{ d: "foo", p: 6 }]
  55. it "with an insert at the same place with side == 'left'", ->
  56. dest = []
  57. text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
  58. dest.should.deep.equal [{ d: "foo", p: 6 }]
  59. it "with a delete that overlaps the insert location", ->
  60. dest = []
  61. text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 })
  62. dest.should.deep.equal [{ d: "f", p: 3 }, { d: "oo", p: 6 }]
  63. describe "delete / delete", ->
  64. it "with a delete before", ->
  65. dest = []
  66. text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 })
  67. dest.should.deep.equal [{ d: "foo", p: 6 }]
  68. it "with a delete after", ->
  69. dest = []
  70. text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 })
  71. dest.should.deep.equal [{ d: "foo", p: 3 }]
  72. it "with deleting the same content", ->
  73. dest = []
  74. text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right')
  75. dest.should.deep.equal []
  76. it "with the delete overlapping before", ->
  77. dest = []
  78. text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right')
  79. dest.should.deep.equal [{ d: "bar", p: 0 }]
  80. it "with the delete overlapping after", ->
  81. dest = []
  82. text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 })
  83. dest.should.deep.equal [{ d: "abc", p: 3 }]
  84. it "with the delete overlapping the whole delete", ->
  85. dest = []
  86. text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 })
  87. dest.should.deep.equal [{ d: "abc123", p: 3 }]
  88. it "with the delete inside the whole delete", ->
  89. dest = []
  90. text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 })
  91. dest.should.deep.equal []
  92. describe "comment / insert", ->
  93. it "with an insert before", ->
  94. dest = []
  95. text._tc(dest, { c: "foo", p: 9, @t }, { i: "bar", p: 3 })
  96. dest.should.deep.equal [{ c: "foo", p: 12, @t }]
  97. it "with an insert after", ->
  98. dest = []
  99. text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
  100. dest.should.deep.equal [{ c: "foo", p: 3, @t }]
  101. it "with an insert at the left edge", ->
  102. dest = []
  103. text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 3 })
  104. # RangesTracker doesn't inject inserts into comments on edges, so neither should we
  105. dest.should.deep.equal [{ c: "foo", p: 6, @t }]
  106. it "with an insert at the right edge", ->
  107. dest = []
  108. text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 6 })
  109. # RangesTracker doesn't inject inserts into comments on edges, so neither should we
  110. dest.should.deep.equal [{ c: "foo", p: 3, @t }]
  111. it "with an insert in the middle", ->
  112. dest = []
  113. text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 5 })
  114. dest.should.deep.equal [{ c: "fobaro", p: 3, @t }]
  115. describe "comment / delete", ->
  116. it "with a delete before", ->
  117. dest = []
  118. text._tc(dest, { c: "foo", p: 9, @t }, { d: "bar", p: 3 })
  119. dest.should.deep.equal [{ c: "foo", p: 6, @t }]
  120. it "with a delete after", ->
  121. dest = []
  122. text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
  123. dest.should.deep.equal [{ c: "foo", p: 3, @t }]
  124. it "with a delete overlapping the comment content before", ->
  125. dest = []
  126. text._tc(dest, { c: "foobar", p: 6, @t }, { d: "123foo", p: 3 })
  127. dest.should.deep.equal [{ c: "bar", p: 3, @t }]
  128. it "with a delete overlapping the comment content after", ->
  129. dest = []
  130. text._tc(dest, { c: "foobar", p: 6, @t }, { d: "bar123", p: 9 })
  131. dest.should.deep.equal [{ c: "foo", p: 6, @t }]
  132. it "with a delete overlapping the comment content in the middle", ->
  133. dest = []
  134. text._tc(dest, { c: "foo123bar", p: 6, @t }, { d: "123", p: 9 })
  135. dest.should.deep.equal [{ c: "foobar", p: 6, @t }]
  136. it "with a delete overlapping the whole comment", ->
  137. dest = []
  138. text._tc(dest, { c: "foo", p: 6, @t }, { d: "123foo456", p: 3 })
  139. dest.should.deep.equal [{ c: "", p: 3, @t }]
  140. describe "comment / insert", ->
  141. it "should not do anything", ->
  142. dest = []
  143. text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 })
  144. dest.should.deep.equal [{ i: "foo", p: 6 }]
  145. describe "comment / delete", ->
  146. it "should not do anything", ->
  147. dest = []
  148. text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 })
  149. dest.should.deep.equal [{ d: "foo", p: 6 }]
  150. describe "comment / comment", ->
  151. it "should not do anything", ->
  152. dest = []
  153. text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 })
  154. dest.should.deep.equal [{ c: "foo", p: 6 }]
  155. describe "apply", ->
  156. it "should apply an insert", ->
  157. text.apply("foo", [{ i: "bar", p: 2 }]).should.equal "fobaro"
  158. it "should apply a delete", ->
  159. text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal "foobar"
  160. it "should do nothing with a comment", ->
  161. text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal "foo123bar"
  162. it "should throw an error when deleted content does not match", ->
  163. (() ->
  164. text.apply("foo123bar", [{ d: "456", p: 3 }])
  165. ).should.throw(Error)
  166. it "should throw an error when comment content does not match", ->
  167. (() ->
  168. text.apply("foo123bar", [{ c: "456", p: 3 }])
  169. ).should.throw(Error)
  170. describe "applying ops and comments in different orders", ->
  171. it "should not matter which op or comment is applied first", ->
  172. transform = (op1, op2, side) ->
  173. d = []
  174. text._tc(d, op1, op2, side)
  175. return d
  176. applySnapshot = (snapshot, op) ->
  177. return text.apply(snapshot, op)
  178. applyRanges = (rangesTracker, ops) ->
  179. for op in ops
  180. rangesTracker.applyOp(op, {})
  181. return rangesTracker
  182. commentsEqual = (comments1, comments2) ->
  183. return false if comments1.length != comments2.length
  184. comments1.sort (a,b) ->
  185. if a.offset - b.offset == 0
  186. return a.length - b.length
  187. else
  188. return a.offset - b.offset
  189. comments2.sort (a,b) ->
  190. if a.offset - b.offset == 0
  191. return a.length - b.length
  192. else
  193. return a.offset - b.offset
  194. for comment1, i in comments1
  195. comment2 = comments2[i]
  196. if comment1.offset != comment2.offset or comment1.length != comment2.length
  197. return false
  198. return true
  199. SNAPSHOT = "123"
  200. OPS = []
  201. # Insert ops
  202. for p in [0..SNAPSHOT.length]
  203. OPS.push {i: "a", p: p}
  204. OPS.push {i: "bc", p: p}
  205. for p in [0..(SNAPSHOT.length-1)]
  206. for length in [1..(SNAPSHOT.length - p)]
  207. OPS.push {d: SNAPSHOT.slice(p, p+length), p}
  208. for p in [0..(SNAPSHOT.length-1)]
  209. for length in [1..(SNAPSHOT.length - p)]
  210. OPS.push {c: SNAPSHOT.slice(p, p+length), p, @t}
  211. for op1 in OPS
  212. for op2 in OPS
  213. op1_t = transform(op1, op2, "left")
  214. op2_t = transform(op2, op1, "right")
  215. rt12 = new RangesTracker()
  216. snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t)
  217. applyRanges(rt12, [op1])
  218. applyRanges(rt12, op2_t)
  219. rt21 = new RangesTracker()
  220. snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t)
  221. applyRanges(rt21, [op2])
  222. applyRanges(rt21, op1_t)
  223. if snapshot12 != snapshot21
  224. console.error {op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent"
  225. throw new Error("OT is inconsistent")
  226. if !commentsEqual(rt12.comments, rt21.comments)
  227. console.log rt12.comments
  228. console.log rt21.comments
  229. console.error {op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent"
  230. throw new Error("OT is inconsistent")