410 lines
11 KiB
JavaScript
410 lines
11 KiB
JavaScript
import * as Y from './testHelper.js'
|
|
import * as t from 'lib0/testing.js'
|
|
import * as prng from 'lib0/prng.js'
|
|
import * as math from 'lib0/math.js'
|
|
|
|
const { init, compare } = Y
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testBasicInsertAndDelete = tc => {
|
|
const { users, text0 } = init(tc, { users: 2 })
|
|
let delta
|
|
|
|
text0.observe(event => {
|
|
delta = event.delta
|
|
})
|
|
|
|
text0.delete(0, 0)
|
|
t.assert(true, 'Does not throw when deleting zero elements with position 0')
|
|
|
|
text0.insert(0, 'abc')
|
|
t.assert(text0.toString() === 'abc', 'Basic insert works')
|
|
t.compare(delta, [{ insert: 'abc' }])
|
|
|
|
text0.delete(0, 1)
|
|
t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)')
|
|
t.compare(delta, [{ delete: 1 }])
|
|
|
|
text0.delete(1, 1)
|
|
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
|
|
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
|
|
|
|
users[0].transact(() => {
|
|
text0.insert(0, '1')
|
|
text0.delete(0, 1)
|
|
})
|
|
t.compare(delta, [])
|
|
|
|
compare(users)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testBasicFormat = tc => {
|
|
const { users, text0 } = init(tc, { users: 2 })
|
|
let delta
|
|
text0.observe(event => {
|
|
delta = event.delta
|
|
})
|
|
text0.insert(0, 'abc', { bold: true })
|
|
t.assert(text0.toString() === 'abc', 'Basic insert with attributes works')
|
|
t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }])
|
|
t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }])
|
|
text0.delete(0, 1)
|
|
t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)')
|
|
t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }])
|
|
t.compare(delta, [{ delete: 1 }])
|
|
text0.delete(1, 1)
|
|
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
|
|
t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }])
|
|
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
|
|
text0.insert(0, 'z', { bold: true })
|
|
t.assert(text0.toString() === 'zb')
|
|
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
|
|
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
|
|
// @ts-ignore
|
|
t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
|
|
text0.insert(0, 'y')
|
|
t.assert(text0.toString() === 'yzb')
|
|
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
|
|
t.compare(delta, [{ insert: 'y' }])
|
|
text0.format(0, 2, { bold: null })
|
|
t.assert(text0.toString() === 'yzb')
|
|
t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }])
|
|
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
|
|
compare(users)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testGetDeltaWithEmbeds = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.applyDelta([{
|
|
insert: { linebreak: 's' }
|
|
}])
|
|
t.compare(text0.toDelta(), [{
|
|
insert: { linebreak: 's' }
|
|
}])
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testSnapshot = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
|
doc0.gc = false
|
|
text0.applyDelta([{
|
|
insert: 'abcd'
|
|
}])
|
|
const snapshot1 = Y.snapshot(doc0)
|
|
text0.applyDelta([{
|
|
retain: 1
|
|
}, {
|
|
insert: 'x'
|
|
}, {
|
|
delete: 1
|
|
}])
|
|
const snapshot2 = Y.snapshot(doc0)
|
|
text0.applyDelta([{
|
|
retain: 2
|
|
}, {
|
|
delete: 3
|
|
}, {
|
|
insert: 'x'
|
|
}, {
|
|
delete: 1
|
|
}])
|
|
const state1 = text0.toDelta(snapshot1)
|
|
t.compare(state1, [{ insert: 'abcd' }])
|
|
const state2 = text0.toDelta(snapshot2)
|
|
t.compare(state2, [{ insert: 'axcd' }])
|
|
const state2Diff = text0.toDelta(snapshot2, snapshot1)
|
|
// @ts-ignore Remove userid info
|
|
state2Diff.forEach(v => {
|
|
if (v.attributes && v.attributes.ychange) {
|
|
delete v.attributes.ychange.user
|
|
}
|
|
})
|
|
t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testSnapshotDeleteAfter = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
|
doc0.gc = false
|
|
text0.applyDelta([{
|
|
insert: 'abcd'
|
|
}])
|
|
const snapshot1 = Y.snapshot(doc0)
|
|
text0.applyDelta([{
|
|
retain: 4
|
|
}, {
|
|
insert: 'e'
|
|
}])
|
|
const state1 = text0.toDelta(snapshot1)
|
|
t.compare(state1, [{ insert: 'abcd' }])
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testToJson = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.insert(0, 'abc', { bold: true })
|
|
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testToDeltaEmbedAttributes = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.insert(0, 'ab', { bold: true })
|
|
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
|
|
const delta0 = text0.toDelta()
|
|
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }])
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testToDeltaEmbedNoAttributes = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.insert(0, 'ab', { bold: true })
|
|
text0.insertEmbed(1, { image: 'imageSrc.png' })
|
|
const delta0 = text0.toDelta()
|
|
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present')
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testFormattingRemoved = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.insert(0, 'ab', { bold: true })
|
|
text0.delete(0, 2)
|
|
t.assert(Y.getTypeChildren(text0).length === 1)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testFormattingRemovedInMidText = tc => {
|
|
const { text0 } = init(tc, { users: 1 })
|
|
text0.insert(0, '1234')
|
|
text0.insert(2, 'ab', { bold: true })
|
|
text0.delete(2, 2)
|
|
t.assert(Y.getTypeChildren(text0).length === 3)
|
|
}
|
|
|
|
const tryGc = () => {
|
|
if (typeof global !== 'undefined' && global.gc) {
|
|
global.gc()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testLargeFragmentedDocument = tc => {
|
|
const itemsToInsert = 1000000
|
|
let update = /** @type {any} */ (null)
|
|
;(() => {
|
|
const doc1 = new Y.Doc()
|
|
const text0 = doc1.getText('txt')
|
|
tryGc()
|
|
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
|
|
doc1.transact(() => {
|
|
for (let i = 0; i < itemsToInsert; i++) {
|
|
text0.insert(0, '0')
|
|
}
|
|
})
|
|
})
|
|
tryGc()
|
|
t.measureTime('time to encode document', () => {
|
|
update = Y.encodeStateAsUpdateV2(doc1)
|
|
})
|
|
t.describe('Document size:', update.byteLength)
|
|
})()
|
|
;(() => {
|
|
const doc2 = new Y.Doc()
|
|
tryGc()
|
|
t.measureTime(`time to apply ${itemsToInsert} updates`, () => {
|
|
Y.applyUpdateV2(doc2, update)
|
|
})
|
|
})()
|
|
}
|
|
|
|
// RANDOM TESTS
|
|
|
|
let charCounter = 0
|
|
|
|
const marks = [
|
|
{ bold: true },
|
|
{ italic: true },
|
|
{ italic: true, color: '#888' }
|
|
]
|
|
|
|
const marksChoices = [
|
|
undefined,
|
|
...marks
|
|
]
|
|
|
|
/**
|
|
* @type Array<function(any,prng.PRNG):void>
|
|
*/
|
|
const qChanges = [
|
|
/**
|
|
* @param {Y.Doc} y
|
|
* @param {prng.PRNG} gen
|
|
*/
|
|
(y, gen) => { // insert text
|
|
const ytext = y.getText('text')
|
|
const insertPos = prng.int32(gen, 0, ytext.toString().length)
|
|
const attrs = prng.oneOf(gen, marksChoices)
|
|
const text = charCounter++ + prng.word(gen)
|
|
ytext.insert(insertPos, text, attrs)
|
|
},
|
|
/**
|
|
* @param {Y.Doc} y
|
|
* @param {prng.PRNG} gen
|
|
*/
|
|
(y, gen) => { // insert embed
|
|
const ytext = y.getText('text')
|
|
const insertPos = prng.int32(gen, 0, ytext.toString().length)
|
|
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
|
|
},
|
|
/**
|
|
* @param {Y.Doc} y
|
|
* @param {prng.PRNG} gen
|
|
*/
|
|
(y, gen) => { // delete text
|
|
const ytext = y.getText('text')
|
|
const contentLen = ytext.toString().length
|
|
const insertPos = prng.int32(gen, 0, contentLen)
|
|
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
|
|
ytext.delete(insertPos, overwrite)
|
|
},
|
|
/**
|
|
* @param {Y.Doc} y
|
|
* @param {prng.PRNG} gen
|
|
*/
|
|
(y, gen) => { // format text
|
|
const ytext = y.getText('text')
|
|
const contentLen = ytext.toString().length
|
|
const insertPos = prng.int32(gen, 0, contentLen)
|
|
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
|
|
const format = prng.oneOf(gen, marks)
|
|
ytext.format(insertPos, overwrite, format)
|
|
},
|
|
/**
|
|
* @param {Y.Doc} y
|
|
* @param {prng.PRNG} gen
|
|
*/
|
|
(y, gen) => { // insert codeblock
|
|
const ytext = y.getText('text')
|
|
const insertPos = prng.int32(gen, 0, ytext.toString().length)
|
|
const text = charCounter++ + prng.word(gen)
|
|
const ops = []
|
|
if (insertPos > 0) {
|
|
ops.push({ retain: insertPos })
|
|
}
|
|
ops.push({ insert: text }, { insert: '\n', format: { 'code-block': true } })
|
|
ytext.applyDelta(ops)
|
|
}
|
|
]
|
|
|
|
/**
|
|
* @param {any} result
|
|
*/
|
|
const checkResult = result => {
|
|
for (let i = 1; i < result.testObjects.length; i++) {
|
|
const p1 = result.users[i].getText('text').toDelta()
|
|
const p2 = result.users[i].getText('text').toDelta()
|
|
t.compare(p1, p2)
|
|
}
|
|
// Uncomment this to find formatting-cleanup issues
|
|
// const cleanups = Y.cleanupYTextFormatting(result.users[0].getText('text'))
|
|
// t.assert(cleanups === 0)
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges1 = tc => {
|
|
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 1))
|
|
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
|
|
t.assert(cleanups === 0)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges2 = tc => {
|
|
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2))
|
|
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
|
|
t.assert(cleanups === 0)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges2Repeat = tc => {
|
|
for (let i = 0; i < 1000; i++) {
|
|
const { users } = checkResult(Y.applyRandomTests(tc, qChanges, 2))
|
|
const cleanups = Y.cleanupYTextFormatting(users[0].getText('text'))
|
|
t.assert(cleanups === 0)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges3 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 3))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges30 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 30))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges40 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 40))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges70 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 70))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges100 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 100))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} tc
|
|
*/
|
|
export const testRepeatGenerateQuillChanges300 = tc => {
|
|
checkResult(Y.applyRandomTests(tc, qChanges, 300))
|
|
}
|