mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 11:47:46 +01:00
353 lines
9.8 KiB
JavaScript
353 lines
9.8 KiB
JavaScript
import * as Y from '../src/index.js'
|
|
import * as t from 'lib0/testing'
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testAfterTransactionRecursion = _tc => {
|
|
const ydoc = new Y.Doc()
|
|
const yxml = ydoc.getXmlFragment('')
|
|
ydoc.on('afterTransaction', tr => {
|
|
if (tr.origin === 'test') {
|
|
yxml.toJSON()
|
|
}
|
|
})
|
|
ydoc.transact(_tr => {
|
|
for (let i = 0; i < 15000; i++) {
|
|
yxml.push([new Y.XmlText('a')])
|
|
}
|
|
}, 'test')
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testFindTypeInOtherDoc = _tc => {
|
|
const ydoc = new Y.Doc()
|
|
const ymap = ydoc.getMap()
|
|
const ytext = ymap.set('ytext', new Y.Text())
|
|
const ydocClone = new Y.Doc()
|
|
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
|
|
/**
|
|
* @template {Y.AbstractType<any>} Type
|
|
* @param {Type} ytype
|
|
* @param {Y.Doc} otherYdoc
|
|
* @return {Type}
|
|
*/
|
|
const findTypeInOtherYdoc = (ytype, otherYdoc) => {
|
|
const ydoc = /** @type {Y.Doc} */ (ytype.doc)
|
|
if (ytype._item === null) {
|
|
/**
|
|
* If is a root type, we need to find the root key in the original ydoc
|
|
* and use it to get the type in the other ydoc.
|
|
*/
|
|
const rootKey = Array.from(ydoc.share.keys()).find(
|
|
(key) => ydoc.share.get(key) === ytype
|
|
)
|
|
if (rootKey == null) {
|
|
throw new Error('type does not exist in other ydoc')
|
|
}
|
|
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {typeof Y.AbstractType<any>} */ (ytype.constructor)))
|
|
} else {
|
|
/**
|
|
* If it is a sub type, we use the item id to find the history type.
|
|
*/
|
|
const ytypeItem = ytype._item
|
|
const otherStructs = otherYdoc.store.clients.get(ytypeItem.id.client) ?? []
|
|
const itemIndex = Y.findIndexSS(
|
|
otherStructs,
|
|
ytypeItem.id.clock
|
|
)
|
|
const otherItem = /** @type {Y.Item} */ (otherStructs[itemIndex])
|
|
const otherContent = /** @type {Y.ContentType} */ (otherItem.content)
|
|
return /** @type {Type} */ (otherContent.type)
|
|
}
|
|
}
|
|
t.assert(findTypeInOtherYdoc(ymap, ydocClone) != null)
|
|
t.assert(findTypeInOtherYdoc(ytext, ydocClone) != null)
|
|
}
|
|
|
|
/**
|
|
* Client id should be changed when an instance receives updates from another client using the same client id.
|
|
*
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testClientIdDuplicateChange = _tc => {
|
|
const doc1 = new Y.Doc()
|
|
doc1.clientID = 0
|
|
const doc2 = new Y.Doc()
|
|
doc2.clientID = 0
|
|
t.assert(doc2.clientID === doc1.clientID)
|
|
doc1.getArray('a').insert(0, [1, 2])
|
|
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
|
|
t.assert(doc2.clientID !== doc1.clientID)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testGetTypeEmptyId = _tc => {
|
|
const doc1 = new Y.Doc()
|
|
doc1.getText('').insert(0, 'h')
|
|
doc1.getText().insert(1, 'i')
|
|
const doc2 = new Y.Doc()
|
|
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
|
|
t.assert(doc2.getText().toString() === 'hi')
|
|
t.assert(doc2.getText('').toString() === 'hi')
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testToJSON = _tc => {
|
|
const doc = new Y.Doc()
|
|
t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
|
|
|
|
const arr = doc.getArray('array')
|
|
arr.push(['test1'])
|
|
|
|
const map = doc.getMap('map')
|
|
map.set('k1', 'v1')
|
|
const map2 = new Y.Map()
|
|
map.set('k2', map2)
|
|
map2.set('m2k1', 'm2v1')
|
|
|
|
t.compare(doc.toJSON(), {
|
|
array: ['test1'],
|
|
map: {
|
|
k1: 'v1',
|
|
k2: {
|
|
m2k1: 'm2v1'
|
|
}
|
|
}
|
|
}, 'doc.toJSON has array and recursive map')
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testSubdoc = _tc => {
|
|
const doc = new Y.Doc()
|
|
doc.load() // doesn't do anything
|
|
{
|
|
/**
|
|
* @type {Array<any>|null}
|
|
*/
|
|
let event = /** @type {any} */ (null)
|
|
doc.on('subdocs', subdocs => {
|
|
event = [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)]
|
|
})
|
|
const subdocs = doc.getMap('mysubdocs')
|
|
const docA = new Y.Doc({ guid: 'a' })
|
|
docA.load()
|
|
subdocs.set('a', docA)
|
|
t.compare(event, [['a'], [], ['a']])
|
|
|
|
event = null
|
|
subdocs.get('a').load()
|
|
t.assert(event === null)
|
|
|
|
event = null
|
|
subdocs.get('a').destroy()
|
|
t.compare(event, [['a'], ['a'], []])
|
|
subdocs.get('a').load()
|
|
t.compare(event, [[], [], ['a']])
|
|
|
|
subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
|
|
t.compare(event, [['a'], [], []])
|
|
subdocs.get('b').load()
|
|
t.compare(event, [[], [], ['a']])
|
|
|
|
const docC = new Y.Doc({ guid: 'c' })
|
|
docC.load()
|
|
subdocs.set('c', docC)
|
|
t.compare(event, [['c'], [], ['c']])
|
|
|
|
t.compare(Array.from(doc.getSubdocGuids()), ['a', 'c'])
|
|
}
|
|
|
|
const doc2 = new Y.Doc()
|
|
{
|
|
t.compare(Array.from(doc2.getSubdocs()), [])
|
|
/**
|
|
* @type {Array<any>|null}
|
|
*/
|
|
let event = /** @type {any} */ (null)
|
|
doc2.on('subdocs', subdocs => {
|
|
event = [Array.from(subdocs.added).map(d => d.guid), Array.from(subdocs.removed).map(d => d.guid), Array.from(subdocs.loaded).map(d => d.guid)]
|
|
})
|
|
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
|
|
t.compare(event, [['a', 'a', 'c'], [], []])
|
|
|
|
doc2.getMap('mysubdocs').get('a').load()
|
|
t.compare(event, [[], [], ['a']])
|
|
|
|
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
|
|
|
|
doc2.getMap('mysubdocs').delete('a')
|
|
t.compare(event, [[], ['a'], []])
|
|
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testSubdocLoadEdgeCases = _tc => {
|
|
const ydoc = new Y.Doc()
|
|
const yarray = ydoc.getArray()
|
|
const subdoc1 = new Y.Doc()
|
|
/**
|
|
* @type {any}
|
|
*/
|
|
let lastEvent = null
|
|
ydoc.on('subdocs', event => {
|
|
lastEvent = event
|
|
})
|
|
yarray.insert(0, [subdoc1])
|
|
t.assert(subdoc1.shouldLoad)
|
|
t.assert(subdoc1.autoLoad === false)
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
|
|
// destroy and check whether lastEvent adds it again to added (it shouldn't)
|
|
subdoc1.destroy()
|
|
const subdoc2 = yarray.get(0)
|
|
t.assert(subdoc1 !== subdoc2)
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
|
|
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
|
|
// load
|
|
subdoc2.load()
|
|
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
|
|
// apply from remote
|
|
const ydoc2 = new Y.Doc()
|
|
ydoc2.on('subdocs', event => {
|
|
lastEvent = event
|
|
})
|
|
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
|
|
const subdoc3 = ydoc2.getArray().get(0)
|
|
t.assert(subdoc3.shouldLoad === false)
|
|
t.assert(subdoc3.autoLoad === false)
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
|
|
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc3))
|
|
// load
|
|
subdoc3.load()
|
|
t.assert(subdoc3.shouldLoad)
|
|
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc3))
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testSubdocLoadEdgeCasesAutoload = _tc => {
|
|
const ydoc = new Y.Doc()
|
|
const yarray = ydoc.getArray()
|
|
const subdoc1 = new Y.Doc({ autoLoad: true })
|
|
/**
|
|
* @type {any}
|
|
*/
|
|
let lastEvent = null
|
|
ydoc.on('subdocs', event => {
|
|
lastEvent = event
|
|
})
|
|
yarray.insert(0, [subdoc1])
|
|
t.assert(subdoc1.shouldLoad)
|
|
t.assert(subdoc1.autoLoad)
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
|
|
// destroy and check whether lastEvent adds it again to added (it shouldn't)
|
|
subdoc1.destroy()
|
|
const subdoc2 = yarray.get(0)
|
|
t.assert(subdoc1 !== subdoc2)
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
|
|
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
|
|
// load
|
|
subdoc2.load()
|
|
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
|
|
// apply from remote
|
|
const ydoc2 = new Y.Doc()
|
|
ydoc2.on('subdocs', event => {
|
|
lastEvent = event
|
|
})
|
|
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
|
|
const subdoc3 = ydoc2.getArray().get(0)
|
|
t.assert(subdoc1.shouldLoad)
|
|
t.assert(subdoc1.autoLoad)
|
|
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
|
|
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testSubdocsUndo = _tc => {
|
|
const ydoc = new Y.Doc()
|
|
const elems = ydoc.getXmlFragment()
|
|
const undoManager = new Y.UndoManager(elems)
|
|
const subdoc = new Y.Doc()
|
|
// @ts-ignore
|
|
elems.insert(0, [subdoc])
|
|
undoManager.undo()
|
|
undoManager.redo()
|
|
t.assert(elems.length === 1)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testLoadDocsEvent = async _tc => {
|
|
const ydoc = new Y.Doc()
|
|
t.assert(ydoc.isLoaded === false)
|
|
let loadedEvent = false
|
|
ydoc.on('load', () => {
|
|
loadedEvent = true
|
|
})
|
|
ydoc.emit('load', [ydoc])
|
|
await ydoc.whenLoaded
|
|
t.assert(loadedEvent)
|
|
t.assert(ydoc.isLoaded)
|
|
}
|
|
|
|
/**
|
|
* @param {t.TestCase} _tc
|
|
*/
|
|
export const testSyncDocsEvent = async _tc => {
|
|
const ydoc = new Y.Doc()
|
|
t.assert(ydoc.isLoaded === false)
|
|
t.assert(ydoc.isSynced === false)
|
|
let loadedEvent = false
|
|
ydoc.once('load', () => {
|
|
loadedEvent = true
|
|
})
|
|
let syncedEvent = false
|
|
ydoc.once('sync', /** @param {any} isSynced */ (isSynced) => {
|
|
syncedEvent = true
|
|
t.assert(isSynced)
|
|
})
|
|
ydoc.emit('sync', [true, ydoc])
|
|
await ydoc.whenLoaded
|
|
const oldWhenSynced = ydoc.whenSynced
|
|
await ydoc.whenSynced
|
|
t.assert(loadedEvent)
|
|
t.assert(syncedEvent)
|
|
t.assert(ydoc.isLoaded)
|
|
t.assert(ydoc.isSynced)
|
|
let loadedEvent2 = false
|
|
ydoc.on('load', () => {
|
|
loadedEvent2 = true
|
|
})
|
|
let syncedEvent2 = false
|
|
ydoc.on('sync', (isSynced) => {
|
|
syncedEvent2 = true
|
|
t.assert(isSynced === false)
|
|
})
|
|
ydoc.emit('sync', [false, ydoc])
|
|
t.assert(!loadedEvent2)
|
|
t.assert(syncedEvent2)
|
|
t.assert(ydoc.isLoaded)
|
|
t.assert(!ydoc.isSynced)
|
|
t.assert(ydoc.whenSynced !== oldWhenSynced)
|
|
}
|