74a8af6ca6
We add a script for running the js tests in `scripts/ci/js_tests`. This script can also be run locally. We move the `automerge-js` package to below the `automerge-wasm` crate as it is specifically testing the wasm interface. We also add an action to the github actions workflow for CI to run the js tests.
697 lines
22 KiB
JavaScript
697 lines
22 KiB
JavaScript
const assert = require('assert')
|
|
const Automerge = require('..')
|
|
const { assertEqualsOneOf } = require('./helpers')
|
|
|
|
function attributeStateToAttributes(accumulatedAttributes) {
|
|
const attributes = {}
|
|
Object.entries(accumulatedAttributes).forEach(([key, values]) => {
|
|
if (values.length && values[0] !== null) {
|
|
attributes[key] = values[0]
|
|
}
|
|
})
|
|
return attributes
|
|
}
|
|
|
|
function isEquivalent(a, b) {
|
|
const aProps = Object.getOwnPropertyNames(a)
|
|
const bProps = Object.getOwnPropertyNames(b)
|
|
|
|
if (aProps.length != bProps.length) {
|
|
return false
|
|
}
|
|
|
|
for (let i = 0; i < aProps.length; i++) {
|
|
const propName = aProps[i]
|
|
if (a[propName] !== b[propName]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function isControlMarker(pseudoCharacter) {
|
|
return typeof pseudoCharacter === 'object' && pseudoCharacter.attributes
|
|
}
|
|
|
|
function opFrom(text, attributes) {
|
|
let op = { insert: text }
|
|
if (Object.keys(attributes).length > 0) {
|
|
op.attributes = attributes
|
|
}
|
|
return op
|
|
}
|
|
|
|
function accumulateAttributes(span, accumulatedAttributes) {
|
|
Object.entries(span).forEach(([key, value]) => {
|
|
if (!accumulatedAttributes[key]) {
|
|
accumulatedAttributes[key] = []
|
|
}
|
|
if (value === null) {
|
|
if (accumulatedAttributes[key].length === 0 || accumulatedAttributes[key] === null) {
|
|
accumulatedAttributes[key].unshift(null)
|
|
} else {
|
|
accumulatedAttributes[key].shift()
|
|
}
|
|
} else {
|
|
if (accumulatedAttributes[key][0] === null) {
|
|
accumulatedAttributes[key].shift()
|
|
} else {
|
|
accumulatedAttributes[key].unshift(value)
|
|
}
|
|
}
|
|
})
|
|
return accumulatedAttributes
|
|
}
|
|
|
|
function automergeTextToDeltaDoc(text) {
|
|
let ops = []
|
|
let controlState = {}
|
|
let currentString = ""
|
|
let attributes = {}
|
|
text.toSpans().forEach((span) => {
|
|
if (isControlMarker(span)) {
|
|
controlState = accumulateAttributes(span.attributes, controlState)
|
|
} else {
|
|
let next = attributeStateToAttributes(controlState)
|
|
|
|
// if the next span has the same calculated attributes as the current span
|
|
// don't bother outputting it as a separate span, just let it ride
|
|
if (typeof span === 'string' && isEquivalent(next, attributes)) {
|
|
currentString = currentString + span
|
|
return
|
|
}
|
|
|
|
if (currentString) {
|
|
ops.push(opFrom(currentString, attributes))
|
|
}
|
|
|
|
// If we've got a string, we might be able to concatenate it to another
|
|
// same-attributed-string, so remember it and go to the next iteration.
|
|
if (typeof span === 'string') {
|
|
currentString = span
|
|
attributes = next
|
|
} else {
|
|
// otherwise we have an embed "character" and should output it immediately.
|
|
// embeds are always one-"character" in length.
|
|
ops.push(opFrom(span, next))
|
|
currentString = ''
|
|
attributes = {}
|
|
}
|
|
}
|
|
})
|
|
|
|
// at the end, flush any accumulated string out
|
|
if (currentString) {
|
|
ops.push(opFrom(currentString, attributes))
|
|
}
|
|
|
|
return ops
|
|
}
|
|
|
|
function inverseAttributes(attributes) {
|
|
let invertedAttributes = {}
|
|
Object.keys(attributes).forEach((key) => {
|
|
invertedAttributes[key] = null
|
|
})
|
|
return invertedAttributes
|
|
}
|
|
|
|
function applyDeleteOp(text, offset, op) {
|
|
let length = op.delete
|
|
while (length > 0) {
|
|
if (isControlMarker(text.get(offset))) {
|
|
offset += 1
|
|
} else {
|
|
// we need to not delete control characters, but we do delete embed characters
|
|
text.deleteAt(offset, 1)
|
|
length -= 1
|
|
}
|
|
}
|
|
return [text, offset]
|
|
}
|
|
|
|
function applyRetainOp(text, offset, op) {
|
|
let length = op.retain
|
|
|
|
if (op.attributes) {
|
|
text.insertAt(offset, { attributes: op.attributes })
|
|
offset += 1
|
|
}
|
|
|
|
while (length > 0) {
|
|
const char = text.get(offset)
|
|
offset += 1
|
|
if (!isControlMarker(char)) {
|
|
length -= 1
|
|
}
|
|
}
|
|
|
|
if (op.attributes) {
|
|
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
|
|
offset += 1
|
|
}
|
|
|
|
return [text, offset]
|
|
}
|
|
|
|
|
|
function applyInsertOp(text, offset, op) {
|
|
let originalOffset = offset
|
|
|
|
if (typeof op.insert === 'string') {
|
|
text.insertAt(offset, ...op.insert.split(''))
|
|
offset += op.insert.length
|
|
} else {
|
|
// we have an embed or something similar
|
|
text.insertAt(offset, op.insert)
|
|
offset += 1
|
|
}
|
|
|
|
if (op.attributes) {
|
|
text.insertAt(originalOffset, { attributes: op.attributes })
|
|
offset += 1
|
|
}
|
|
if (op.attributes) {
|
|
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
|
|
offset += 1
|
|
}
|
|
return [text, offset]
|
|
}
|
|
|
|
// XXX: uhhhhh, why can't I pass in text?
|
|
function applyDeltaDocToAutomergeText(delta, doc) {
|
|
let offset = 0
|
|
|
|
delta.forEach(op => {
|
|
if (op.retain) {
|
|
[, offset] = applyRetainOp(doc.text, offset, op)
|
|
} else if (op.delete) {
|
|
[, offset] = applyDeleteOp(doc.text, offset, op)
|
|
} else if (op.insert) {
|
|
[, offset] = applyInsertOp(doc.text, offset, op)
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('Automerge.Text', () => {
|
|
let s1, s2
|
|
beforeEach(() => {
|
|
s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text())
|
|
s2 = Automerge.merge(Automerge.init(), s1)
|
|
})
|
|
|
|
it('should support insertion', () => {
|
|
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a'))
|
|
assert.strictEqual(s1.text.length, 1)
|
|
assert.strictEqual(s1.text.get(0), 'a')
|
|
assert.strictEqual(s1.text.toString(), 'a')
|
|
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
|
|
})
|
|
|
|
it('should support deletion', () => {
|
|
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
|
|
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
|
|
assert.strictEqual(s1.text.length, 2)
|
|
assert.strictEqual(s1.text.get(0), 'a')
|
|
assert.strictEqual(s1.text.get(1), 'c')
|
|
assert.strictEqual(s1.text.toString(), 'ac')
|
|
})
|
|
|
|
it("should support implicit and explicit deletion", () => {
|
|
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
|
|
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
|
|
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
|
|
assert.strictEqual(s1.text.length, 2)
|
|
assert.strictEqual(s1.text.get(0), "a")
|
|
assert.strictEqual(s1.text.get(1), "c")
|
|
assert.strictEqual(s1.text.toString(), "ac")
|
|
})
|
|
|
|
it('should handle concurrent insertion', () => {
|
|
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
|
|
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, 'x', 'y', 'z'))
|
|
s1 = Automerge.merge(s1, s2)
|
|
assert.strictEqual(s1.text.length, 6)
|
|
assertEqualsOneOf(s1.text.toString(), 'abcxyz', 'xyzabc')
|
|
assertEqualsOneOf(s1.text.join(''), 'abcxyz', 'xyzabc')
|
|
})
|
|
|
|
it('should handle text and other ops in the same change', () => {
|
|
s1 = Automerge.change(s1, doc => {
|
|
doc.foo = 'bar'
|
|
doc.text.insertAt(0, 'a')
|
|
})
|
|
assert.strictEqual(s1.foo, 'bar')
|
|
assert.strictEqual(s1.text.toString(), 'a')
|
|
assert.strictEqual(s1.text.join(''), 'a')
|
|
})
|
|
|
|
it('should serialize to JSON as a simple string', () => {
|
|
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', '"', 'b'))
|
|
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
|
|
})
|
|
|
|
it('should allow modification before an object is assigned to a document', () => {
|
|
s1 = Automerge.change(Automerge.init(), doc => {
|
|
const text = new Automerge.Text()
|
|
text.insertAt(0, 'a', 'b', 'c', 'd')
|
|
text.deleteAt(2)
|
|
doc.text = text
|
|
assert.strictEqual(doc.text.toString(), 'abd')
|
|
assert.strictEqual(doc.text.join(''), 'abd')
|
|
})
|
|
assert.strictEqual(s1.text.toString(), 'abd')
|
|
assert.strictEqual(s1.text.join(''), 'abd')
|
|
})
|
|
|
|
it('should allow modification after an object is assigned to a document', () => {
|
|
s1 = Automerge.change(Automerge.init(), doc => {
|
|
const text = new Automerge.Text()
|
|
doc.text = text
|
|
doc.text.insertAt(0, 'a', 'b', 'c', 'd')
|
|
doc.text.deleteAt(2)
|
|
assert.strictEqual(doc.text.toString(), 'abd')
|
|
assert.strictEqual(doc.text.join(''), 'abd')
|
|
})
|
|
assert.strictEqual(s1.text.join(''), 'abd')
|
|
})
|
|
|
|
it('should not allow modification outside of a change callback', () => {
|
|
assert.throws(() => s1.text.insertAt(0, 'a'), /object cannot be modified outside of a change block/)
|
|
})
|
|
|
|
describe('with initial value', () => {
|
|
it('should accept a string as initial value', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('init'))
|
|
assert.strictEqual(s1.text.length, 4)
|
|
assert.strictEqual(s1.text.get(0), 'i')
|
|
assert.strictEqual(s1.text.get(1), 'n')
|
|
assert.strictEqual(s1.text.get(2), 'i')
|
|
assert.strictEqual(s1.text.get(3), 't')
|
|
assert.strictEqual(s1.text.toString(), 'init')
|
|
})
|
|
|
|
it('should accept an array as initial value', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text(['i', 'n', 'i', 't']))
|
|
assert.strictEqual(s1.text.length, 4)
|
|
assert.strictEqual(s1.text.get(0), 'i')
|
|
assert.strictEqual(s1.text.get(1), 'n')
|
|
assert.strictEqual(s1.text.get(2), 'i')
|
|
assert.strictEqual(s1.text.get(3), 't')
|
|
assert.strictEqual(s1.text.toString(), 'init')
|
|
})
|
|
|
|
it('should initialize text in Automerge.from()', () => {
|
|
let s1 = Automerge.from({text: new Automerge.Text('init')})
|
|
assert.strictEqual(s1.text.length, 4)
|
|
assert.strictEqual(s1.text.get(0), 'i')
|
|
assert.strictEqual(s1.text.get(1), 'n')
|
|
assert.strictEqual(s1.text.get(2), 'i')
|
|
assert.strictEqual(s1.text.get(3), 't')
|
|
assert.strictEqual(s1.text.toString(), 'init')
|
|
})
|
|
|
|
it('should encode the initial value as a change', () => {
|
|
const s1 = Automerge.from({text: new Automerge.Text('init')})
|
|
const changes = Automerge.getAllChanges(s1)
|
|
assert.strictEqual(changes.length, 1)
|
|
const [s2] = Automerge.applyChanges(Automerge.init(), changes)
|
|
assert.strictEqual(s2.text instanceof Automerge.Text, true)
|
|
assert.strictEqual(s2.text.toString(), 'init')
|
|
assert.strictEqual(s2.text.join(''), 'init')
|
|
})
|
|
|
|
it('should allow immediate access to the value', () => {
|
|
Automerge.change(Automerge.init(), doc => {
|
|
const text = new Automerge.Text('init')
|
|
assert.strictEqual(text.length, 4)
|
|
assert.strictEqual(text.get(0), 'i')
|
|
assert.strictEqual(text.toString(), 'init')
|
|
doc.text = text
|
|
assert.strictEqual(doc.text.length, 4)
|
|
assert.strictEqual(doc.text.get(0), 'i')
|
|
assert.strictEqual(doc.text.toString(), 'init')
|
|
})
|
|
})
|
|
|
|
it('should allow pre-assignment modification of the initial value', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
const text = new Automerge.Text('init')
|
|
text.deleteAt(3)
|
|
assert.strictEqual(text.join(''), 'ini')
|
|
doc.text = text
|
|
assert.strictEqual(doc.text.join(''), 'ini')
|
|
assert.strictEqual(doc.text.toString(), 'ini')
|
|
})
|
|
assert.strictEqual(s1.text.toString(), 'ini')
|
|
assert.strictEqual(s1.text.join(''), 'ini')
|
|
})
|
|
|
|
it('should allow post-assignment modification of the initial value', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
const text = new Automerge.Text('init')
|
|
doc.text = text
|
|
doc.text.deleteAt(0)
|
|
doc.text.insertAt(0, 'I')
|
|
assert.strictEqual(doc.text.join(''), 'Init')
|
|
assert.strictEqual(doc.text.toString(), 'Init')
|
|
})
|
|
assert.strictEqual(s1.text.join(''), 'Init')
|
|
assert.strictEqual(s1.text.toString(), 'Init')
|
|
})
|
|
})
|
|
|
|
describe('non-textual control characters', () => {
|
|
let s1
|
|
beforeEach(() => {
|
|
s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text()
|
|
doc.text.insertAt(0, 'a')
|
|
doc.text.insertAt(1, { attribute: 'bold' })
|
|
})
|
|
})
|
|
|
|
it('should allow fetching non-textual characters', () => {
|
|
assert.deepEqual(s1.text.get(1), { attribute: 'bold' })
|
|
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
|
|
})
|
|
|
|
it('should include control characters in string length', () => {
|
|
assert.strictEqual(s1.text.length, 2)
|
|
assert.strictEqual(s1.text.get(0), 'a')
|
|
})
|
|
|
|
it('should exclude control characters from toString()', () => {
|
|
assert.strictEqual(s1.text.toString(), 'a')
|
|
})
|
|
|
|
it('should allow control characters to be updated', () => {
|
|
const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')
|
|
const s3 = Automerge.load(Automerge.save(s2))
|
|
assert.strictEqual(s1.text.get(1).attribute, 'bold')
|
|
assert.strictEqual(s2.text.get(1).attribute, 'italic')
|
|
assert.strictEqual(s3.text.get(1).attribute, 'italic')
|
|
})
|
|
|
|
describe('spans interface to Text', () => {
|
|
it('should return a simple string as a single span', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('hello world')
|
|
})
|
|
assert.deepEqual(s1.text.toSpans(), ['hello world'])
|
|
})
|
|
it('should return an empty string as an empty array', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text()
|
|
})
|
|
assert.deepEqual(s1.text.toSpans(), [])
|
|
})
|
|
it('should split a span at a control character', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('hello world')
|
|
doc.text.insertAt(5, { attributes: { bold: true } })
|
|
})
|
|
assert.deepEqual(s1.text.toSpans(),
|
|
['hello', { attributes: { bold: true } }, ' world'])
|
|
})
|
|
it('should allow consecutive control characters', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('hello world')
|
|
doc.text.insertAt(5, { attributes: { bold: true } })
|
|
doc.text.insertAt(6, { attributes: { italic: true } })
|
|
})
|
|
assert.deepEqual(s1.text.toSpans(),
|
|
['hello',
|
|
{ attributes: { bold: true } },
|
|
{ attributes: { italic: true } },
|
|
' world'
|
|
])
|
|
})
|
|
it('should allow non-consecutive control characters', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('hello world')
|
|
doc.text.insertAt(5, { attributes: { bold: true } })
|
|
doc.text.insertAt(12, { attributes: { italic: true } })
|
|
})
|
|
assert.deepEqual(s1.text.toSpans(),
|
|
['hello',
|
|
{ attributes: { bold: true } },
|
|
' world',
|
|
{ attributes: { italic: true } }
|
|
])
|
|
})
|
|
|
|
it('should be convertable into a Quill delta', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Gandalf the Grey')
|
|
doc.text.insertAt(0, { attributes: { bold: true } })
|
|
doc.text.insertAt(7 + 1, { attributes: { bold: null } })
|
|
doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })
|
|
})
|
|
|
|
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
|
|
|
// From https://quilljs.com/docs/delta/
|
|
let expectedDoc = [
|
|
{ insert: 'Gandalf', attributes: { bold: true } },
|
|
{ insert: ' the ' },
|
|
{ insert: 'Grey', attributes: { color: '#cccccc' } }
|
|
]
|
|
|
|
assert.deepEqual(deltaDoc, expectedDoc)
|
|
})
|
|
|
|
it('should support embeds', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('')
|
|
doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })
|
|
doc.text.insertAt(1, {
|
|
image: 'https://quilljs.com/assets/images/icon.png'
|
|
})
|
|
doc.text.insertAt(2, { attributes: { link: null } })
|
|
})
|
|
|
|
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
|
|
|
// From https://quilljs.com/docs/delta/
|
|
let expectedDoc = [{
|
|
// An image link
|
|
insert: {
|
|
image: 'https://quilljs.com/assets/images/icon.png'
|
|
},
|
|
attributes: {
|
|
link: 'https://quilljs.com'
|
|
}
|
|
}]
|
|
|
|
assert.deepEqual(deltaDoc, expectedDoc)
|
|
})
|
|
|
|
it('should handle concurrent overlapping spans', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Gandalf the Grey')
|
|
})
|
|
|
|
let s2 = Automerge.merge(Automerge.init(), s1)
|
|
|
|
let s3 = Automerge.change(s1, doc => {
|
|
doc.text.insertAt(8, { attributes: { bold: true } })
|
|
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
|
})
|
|
|
|
let s4 = Automerge.change(s2, doc => {
|
|
doc.text.insertAt(0, { attributes: { bold: true } })
|
|
doc.text.insertAt(11 + 1, { attributes: { bold: null } })
|
|
})
|
|
|
|
let merged = Automerge.merge(s3, s4)
|
|
|
|
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
|
|
|
// From https://quilljs.com/docs/delta/
|
|
let expectedDoc = [
|
|
{ insert: 'Gandalf the Grey', attributes: { bold: true } },
|
|
]
|
|
|
|
assert.deepEqual(deltaDoc, expectedDoc)
|
|
})
|
|
|
|
it('should handle debolding spans', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Gandalf the Grey')
|
|
})
|
|
|
|
let s2 = Automerge.merge(Automerge.init(), s1)
|
|
|
|
let s3 = Automerge.change(s1, doc => {
|
|
doc.text.insertAt(0, { attributes: { bold: true } })
|
|
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
|
})
|
|
|
|
let s4 = Automerge.change(s2, doc => {
|
|
doc.text.insertAt(8, { attributes: { bold: null } })
|
|
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
|
})
|
|
|
|
|
|
let merged = Automerge.merge(s3, s4)
|
|
|
|
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
|
|
|
// From https://quilljs.com/docs/delta/
|
|
let expectedDoc = [
|
|
{ insert: 'Gandalf ', attributes: { bold: true } },
|
|
{ insert: 'the' },
|
|
{ insert: ' Grey', attributes: { bold: true } },
|
|
]
|
|
|
|
assert.deepEqual(deltaDoc, expectedDoc)
|
|
})
|
|
|
|
// xxx: how would this work for colors?
|
|
it('should handle destyling across destyled spans', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Gandalf the Grey')
|
|
})
|
|
|
|
let s2 = Automerge.merge(Automerge.init(), s1)
|
|
|
|
let s3 = Automerge.change(s1, doc => {
|
|
doc.text.insertAt(0, { attributes: { bold: true } })
|
|
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
|
})
|
|
|
|
let s4 = Automerge.change(s2, doc => {
|
|
doc.text.insertAt(8, { attributes: { bold: null } })
|
|
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
|
})
|
|
|
|
let merged = Automerge.merge(s3, s4)
|
|
|
|
let final = Automerge.change(merged, doc => {
|
|
doc.text.insertAt(3 + 1, { attributes: { bold: null } })
|
|
doc.text.insertAt(doc.text.length, { attributes: { bold: true } })
|
|
})
|
|
|
|
let deltaDoc = automergeTextToDeltaDoc(final.text)
|
|
|
|
// From https://quilljs.com/docs/delta/
|
|
let expectedDoc = [
|
|
{ insert: 'Gan', attributes: { bold: true } },
|
|
{ insert: 'dalf the Grey' },
|
|
]
|
|
|
|
assert.deepEqual(deltaDoc, expectedDoc)
|
|
})
|
|
|
|
it('should apply an insert', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Hello world')
|
|
})
|
|
|
|
const delta = [
|
|
{ retain: 6 },
|
|
{ insert: 'reader' },
|
|
{ delete: 5 }
|
|
]
|
|
|
|
let s2 = Automerge.change(s1, doc => {
|
|
applyDeltaDocToAutomergeText(delta, doc)
|
|
})
|
|
|
|
assert.strictEqual(s2.text.join(''), 'Hello reader')
|
|
})
|
|
|
|
it('should apply an insert with control characters', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Hello world')
|
|
})
|
|
|
|
const delta = [
|
|
{ retain: 6 },
|
|
{ insert: 'reader', attributes: { bold: true } },
|
|
{ delete: 5 },
|
|
{ insert: '!' }
|
|
]
|
|
|
|
let s2 = Automerge.change(s1, doc => {
|
|
applyDeltaDocToAutomergeText(delta, doc)
|
|
})
|
|
|
|
assert.strictEqual(s2.text.toString(), 'Hello reader!')
|
|
assert.deepEqual(s2.text.toSpans(), [
|
|
"Hello ",
|
|
{ attributes: { bold: true } },
|
|
"reader",
|
|
{ attributes: { bold: null } },
|
|
"!"
|
|
])
|
|
})
|
|
|
|
it('should account for control characters in retain/delete lengths', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('Hello world')
|
|
doc.text.insertAt(4, { attributes: { color: '#ccc' } })
|
|
doc.text.insertAt(10, { attributes: { color: '#f00' } })
|
|
})
|
|
|
|
const delta = [
|
|
{ retain: 6 },
|
|
{ insert: 'reader', attributes: { bold: true } },
|
|
{ delete: 5 },
|
|
{ insert: '!' }
|
|
]
|
|
|
|
let s2 = Automerge.change(s1, doc => {
|
|
applyDeltaDocToAutomergeText(delta, doc)
|
|
})
|
|
|
|
assert.strictEqual(s2.text.toString(), 'Hello reader!')
|
|
assert.deepEqual(s2.text.toSpans(), [
|
|
"Hell",
|
|
{ attributes: { color: '#ccc'} },
|
|
"o ",
|
|
{ attributes: { bold: true } },
|
|
"reader",
|
|
{ attributes: { bold: null } },
|
|
{ attributes: { color: '#f00'} },
|
|
"!"
|
|
])
|
|
})
|
|
|
|
it('should support embeds', () => {
|
|
let s1 = Automerge.change(Automerge.init(), doc => {
|
|
doc.text = new Automerge.Text('')
|
|
})
|
|
|
|
let deltaDoc = [{
|
|
// An image link
|
|
insert: {
|
|
image: 'https://quilljs.com/assets/images/icon.png'
|
|
},
|
|
attributes: {
|
|
link: 'https://quilljs.com'
|
|
}
|
|
}]
|
|
|
|
let s2 = Automerge.change(s1, doc => {
|
|
applyDeltaDocToAutomergeText(deltaDoc, doc)
|
|
})
|
|
|
|
assert.deepEqual(s2.text.toSpans(), [
|
|
{ attributes: { link: 'https://quilljs.com' } },
|
|
{ image: 'https://quilljs.com/assets/images/icon.png'},
|
|
{ attributes: { link: null } },
|
|
])
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should support unicode when creating text', () => {
|
|
s1 = Automerge.from({
|
|
text: new Automerge.Text('🐦')
|
|
})
|
|
assert.strictEqual(s1.text.get(0), '🐦')
|
|
})
|
|
})
|