overleaf/services/document-updater/test/unit/coffee/ShareJS/TextTransformTests.js

283 lines
9.4 KiB
JavaScript

text = require "../../../../app/js/sharejs/types/text"
require("chai").should()
RangesTracker = require "../../../../app/js/RangesTracker"
describe "ShareJS text type", ->
beforeEach ->
@t = "mock-thread-id"
describe "transform", ->
describe "insert / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 12 }]
it "with an insert after", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 })
dest.should.deep.equal [{ i: "foo", p: 3 }]
it "with an insert at the same place with side == 'right'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ i: "foo", p: 6 }]
it "with an insert at the same place with side == 'left'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ i: "foo", p: 3 }]
describe "insert / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 6 }]
it "with a delete after", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 })
dest.should.deep.equal [{ i: "foo", p: 3 }]
it "with a delete at the same place with side == 'right'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ i: "foo", p: 3 }]
it "with a delete at the same place with side == 'left'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ i: "foo", p: 3 }]
describe "delete / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 12 }]
it "with an insert after", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 })
dest.should.deep.equal [{ d: "foo", p: 3 }]
it "with an insert at the same place with side == 'right'", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ d: "foo", p: 6 }]
it "with an insert at the same place with side == 'left'", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ d: "foo", p: 6 }]
it "with a delete that overlaps the insert location", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 })
dest.should.deep.equal [{ d: "f", p: 3 }, { d: "oo", p: 6 }]
describe "delete / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 6 }]
it "with a delete after", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 })
dest.should.deep.equal [{ d: "foo", p: 3 }]
it "with deleting the same content", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right')
dest.should.deep.equal []
it "with the delete overlapping before", ->
dest = []
text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right')
dest.should.deep.equal [{ d: "bar", p: 0 }]
it "with the delete overlapping after", ->
dest = []
text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 })
dest.should.deep.equal [{ d: "abc", p: 3 }]
it "with the delete overlapping the whole delete", ->
dest = []
text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 })
dest.should.deep.equal [{ d: "abc123", p: 3 }]
it "with the delete inside the whole delete", ->
dest = []
text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 })
dest.should.deep.equal []
describe "comment / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { c: "foo", p: 9, @t }, { i: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 12, @t }]
it "with an insert after", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it "with an insert at the left edge", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 3 })
# RangesTracker doesn't inject inserts into comments on edges, so neither should we
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
it "with an insert at the right edge", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 6 })
# RangesTracker doesn't inject inserts into comments on edges, so neither should we
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it "with an insert in the middle", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 5 })
dest.should.deep.equal [{ c: "fobaro", p: 3, @t }]
describe "comment / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { c: "foo", p: 9, @t }, { d: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
it "with a delete after", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it "with a delete overlapping the comment content before", ->
dest = []
text._tc(dest, { c: "foobar", p: 6, @t }, { d: "123foo", p: 3 })
dest.should.deep.equal [{ c: "bar", p: 3, @t }]
it "with a delete overlapping the comment content after", ->
dest = []
text._tc(dest, { c: "foobar", p: 6, @t }, { d: "bar123", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
it "with a delete overlapping the comment content in the middle", ->
dest = []
text._tc(dest, { c: "foo123bar", p: 6, @t }, { d: "123", p: 9 })
dest.should.deep.equal [{ c: "foobar", p: 6, @t }]
it "with a delete overlapping the whole comment", ->
dest = []
text._tc(dest, { c: "foo", p: 6, @t }, { d: "123foo456", p: 3 })
dest.should.deep.equal [{ c: "", p: 3, @t }]
describe "comment / insert", ->
it "should not do anything", ->
dest = []
text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 6 }]
describe "comment / delete", ->
it "should not do anything", ->
dest = []
text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 6 }]
describe "comment / comment", ->
it "should not do anything", ->
dest = []
text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 6 }]
describe "apply", ->
it "should apply an insert", ->
text.apply("foo", [{ i: "bar", p: 2 }]).should.equal "fobaro"
it "should apply a delete", ->
text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal "foobar"
it "should do nothing with a comment", ->
text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal "foo123bar"
it "should throw an error when deleted content does not match", ->
(() ->
text.apply("foo123bar", [{ d: "456", p: 3 }])
).should.throw(Error)
it "should throw an error when comment content does not match", ->
(() ->
text.apply("foo123bar", [{ c: "456", p: 3 }])
).should.throw(Error)
describe "applying ops and comments in different orders", ->
it "should not matter which op or comment is applied first", ->
transform = (op1, op2, side) ->
d = []
text._tc(d, op1, op2, side)
return d
applySnapshot = (snapshot, op) ->
return text.apply(snapshot, op)
applyRanges = (rangesTracker, ops) ->
for op in ops
rangesTracker.applyOp(op, {})
return rangesTracker
commentsEqual = (comments1, comments2) ->
return false if comments1.length != comments2.length
comments1.sort (a,b) ->
if a.offset - b.offset == 0
return a.length - b.length
else
return a.offset - b.offset
comments2.sort (a,b) ->
if a.offset - b.offset == 0
return a.length - b.length
else
return a.offset - b.offset
for comment1, i in comments1
comment2 = comments2[i]
if comment1.offset != comment2.offset or comment1.length != comment2.length
return false
return true
SNAPSHOT = "123"
OPS = []
# Insert ops
for p in [0..SNAPSHOT.length]
OPS.push {i: "a", p: p}
OPS.push {i: "bc", p: p}
for p in [0..(SNAPSHOT.length-1)]
for length in [1..(SNAPSHOT.length - p)]
OPS.push {d: SNAPSHOT.slice(p, p+length), p}
for p in [0..(SNAPSHOT.length-1)]
for length in [1..(SNAPSHOT.length - p)]
OPS.push {c: SNAPSHOT.slice(p, p+length), p, @t}
for op1 in OPS
for op2 in OPS
op1_t = transform(op1, op2, "left")
op2_t = transform(op2, op1, "right")
rt12 = new RangesTracker()
snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t)
applyRanges(rt12, [op1])
applyRanges(rt12, op2_t)
rt21 = new RangesTracker()
snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t)
applyRanges(rt21, [op2])
applyRanges(rt21, op1_t)
if snapshot12 != snapshot21
console.error {op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent"
throw new Error("OT is inconsistent")
if !commentsEqual(rt12.comments, rt21.comments)
console.log rt12.comments
console.log rt21.comments
console.error {op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent"
throw new Error("OT is inconsistent")