import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { compareIDs } from '../src/internals.js' import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' /** * @param {t.TestCase} _tc */ export const testIterators = _tc => { const ydoc = new Y.Doc() /** * @type {Y.Map} */ const ymap = ydoc.getMap() // we are only checking if the type assumptions are correct /** * @type {Array} */ const vals = Array.from(ymap.values()) /** * @type {Array<[string,number]>} */ const entries = Array.from(ymap.entries()) /** * @type {Array} */ const keys = Array.from(ymap.keys()) console.log(vals, entries, keys) } /** * Computing event changes after transaction should result in an error. See yjs#539 * * @param {t.TestCase} _tc */ export const testMapEventError = _tc => { const doc = new Y.Doc() const ymap = doc.getMap() /** * @type {any} */ let event = null ymap.observe((e) => { event = e }) t.fails(() => { t.info(event.keys) }) t.fails(() => { t.info(event.keys) }) } /** * @param {t.TestCase} tc */ export const testMapHavingIterableAsConstructorParamTests = tc => { const { map0 } = init(tc, { users: 1 }) const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' })) map0.set('m1', m1) t.assert(m1.get('number') === 1) t.assert(m1.get('string') === 'hello') const m2 = new Y.Map([ ['object', { x: 1 }], ['boolean', true] ]) map0.set('m2', m2) t.assert(m2.get('object').x === 1) t.assert(m2.get('boolean') === true) const m3 = new Y.Map([...m1, ...m2]) map0.set('m3', m3) t.assert(m3.get('number') === 1) t.assert(m3.get('string') === 'hello') t.assert(m3.get('object').x === 1) t.assert(m3.get('boolean') === true) } /** * @param {t.TestCase} tc */ export const testBasicMapTests = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) users[2].disconnect() map0.set('null', null) map0.set('number', 1) map0.set('string', 'hello Y') map0.set('object', { key: { key2: 'value' } }) map0.set('y-map', new Y.Map()) map0.set('boolean1', true) map0.set('boolean0', false) const map = map0.get('y-map') map.set('y-array', new Y.Array()) const array = map.get('y-array') array.insert(0, [0]) array.insert(0, [-1]) t.assert(map0.get('null') === null, 'client 0 computed the change (null)') t.assert(map0.get('number') === 1, 'client 0 computed the change (number)') t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)') t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)') t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)') t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') t.assert(map0.size === 7, 'client 0 map has correct size') users[2].connect() testConnector.flushAllMessages() t.assert(map1.get('null') === null, 'client 1 received the update (null)') t.assert(map1.get('number') === 1, 'client 1 received the update (number)') t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)') t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)') t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)') t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') t.assert(map1.size === 7, 'client 1 map has correct size') // compare disconnected user t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected') t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)') t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)') t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected') t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected') compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapProperty = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') map0.set('undefined', undefined) map0.set('null', null) t.compare(map0.get('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'stuffy') t.assert(u.get('undefined') === undefined, 'undefined') t.compare(u.get('null'), null, 'null') } compare(users) } /** * @param {t.TestCase} tc */ export const testYmapSetsYmap = tc => { const { users, map0 } = init(tc, { users: 2 }) const map = map0.set('Map', new Y.Map()) t.assert(map0.get('Map') === map) map.set('one', 1) t.compare(map.get('one'), 1) compare(users) } /** * @param {t.TestCase} tc */ export const testYmapSetsYarray = tc => { const { users, map0 } = init(tc, { users: 2 }) const array = map0.set('Array', new Y.Array()) t.assert(array === map0.get('Array')) array.insert(0, [1, 2, 3]) // @ts-ignore t.compare(map0.toJSON(), { Array: [1, 2, 3] }) compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertySyncs = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) map0.set('stuff', 'stuffy') t.compare(map0.get('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'stuffy') } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertyWithConflict = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'c1') } compare(users) } /** * @param {t.TestCase} tc */ export const testSizeAndDeleteOfMapProperty = tc => { const { map0 } = init(tc, { users: 1 }) map0.set('stuff', 'c0') map0.set('otherstuff', 'c1') t.assert(map0.size === 2, `map size is ${map0.size} expected 2`) map0.delete('stuff') t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`) map0.delete('otherstuff') t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`) } /** * @param {t.TestCase} tc */ export const testGetAndSetAndDeleteOfMapProperty = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.delete('stuff') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) } compare(users) } /** * @param {t.TestCase} tc */ export const testSetAndClearOfMapProperties = tc => { const { testConnector, users, map0 } = init(tc, { users: 1 }) map0.set('stuff', 'c0') map0.set('otherstuff', 'c1') map0.clear() testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) t.assert(u.get('otherstuff') === undefined) t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) } compare(users) } /** * @param {t.TestCase} tc */ export const testSetAndClearOfMapPropertiesWithConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() map0.set('otherstuff', 'c0') map1.set('otherstuff', 'c1') map2.set('otherstuff', 'c2') map3.set('otherstuff', 'c3') map3.clear() testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) t.assert(u.get('otherstuff') === undefined) t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.compare(u.get('stuff'), 'c3') } compare(users) } /** * @param {t.TestCase} tc */ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) map0.set('stuff', 'c0') map1.set('stuff', 'c1') map1.set('stuff', 'c2') map2.set('stuff', 'c3') testConnector.flushAllMessages() map0.set('stuff', 'deleteme') map1.set('stuff', 'c1') map2.set('stuff', 'c2') map3.set('stuff', 'c3') map3.delete('stuff') testConnector.flushAllMessages() for (const user of users) { const u = user.getMap('map') t.assert(u.get('stuff') === undefined) } compare(users) } /** * @param {t.TestCase} tc */ export const testObserveDeepProperties = tc => { const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 }) const _map1 = map1.set('map', new Y.Map()) let calls = 0 let dmapid map1.observeDeep(events => { events.forEach(event => { calls++ // @ts-ignore t.assert(event.keysChanged.has('deepmap')) t.assert(event.path.length === 1) t.assert(event.path[0] === 'map') // @ts-ignore dmapid = event.target.get('deepmap')._item.id }) }) testConnector.flushAllMessages() const _map3 = map3.get('map') _map3.set('deepmap', new Y.Map()) testConnector.flushAllMessages() const _map2 = map2.get('map') _map2.set('deepmap', new Y.Map()) testConnector.flushAllMessages() const dmap1 = _map1.get('deepmap') const dmap2 = _map2.get('deepmap') const dmap3 = _map3.get('deepmap') t.assert(calls > 0) t.assert(compareIDs(dmap1._item.id, dmap2._item.id)) t.assert(compareIDs(dmap1._item.id, dmap3._item.id)) // @ts-ignore we want the possibility of dmapid being undefined t.assert(compareIDs(dmap1._item.id, dmapid)) compare(users) } /** * @param {t.TestCase} tc */ export const testObserversUsingObservedeep = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Array>} */ const paths = [] let calls = 0 map0.observeDeep(events => { events.forEach(event => { paths.push(event.path) }) calls++ }) map0.set('map', new Y.Map()) map0.get('map').set('array', new Y.Array()) map0.get('map').get('array').insert(0, ['content']) t.assert(calls === 3) t.compare(paths, [[], ['map'], ['map', 'array']]) compare(users) } /** * @param {t.TestCase} tc */ export const testPathsOfSiblingEvents = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Array>} */ const paths = [] let calls = 0 const doc = users[0] map0.set('map', new Y.Map()) map0.get('map').set('text1', new Y.Text('initial')) map0.observeDeep(events => { events.forEach(event => { paths.push(event.path) }) calls++ }) doc.transact(() => { map0.get('map').get('text1').insert(0, 'post-') map0.get('map').set('text2', new Y.Text('new')) }) t.assert(calls === 1) t.compare(paths, [['map'], ['map', 'text1']]) compare(users) } // TODO: Test events in Y.Map /** * @param {Object} is * @param {Object} should */ const compareEvent = (is, should) => { for (const key in should) { t.compare(should[key], is[key]) } } /** * @param {t.TestCase} tc */ export const testThrowsAddAndUpdateAndDeleteEvents = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e // just put it on event, should be thrown synchronously anyway }) map0.set('stuff', 4) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in contents map0.set('stuff', new Y.Array()) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in opContents map0.set('stuff', 5) // delete map0.delete('stuff') compareEvent(event, { keysChanged: new Set(['stuff']), target: map0 }) compare(users) } /** * @param {t.TestCase} tc */ export const testThrowsDeleteEventsOnClear = tc => { const { users, map0 } = init(tc, { users: 2 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e // just put it on event, should be thrown synchronously anyway }) // set values map0.set('stuff', 4) map0.set('otherstuff', new Y.Array()) // clear map0.clear() compareEvent(event, { keysChanged: new Set(['stuff', 'otherstuff']), target: map0 }) compare(users) } /** * @param {t.TestCase} tc */ export const testChangeEvent = tc => { const { map0, users } = init(tc, { users: 2 }) /** * @type {any} */ let changes = null /** * @type {any} */ let keyChange = null map0.observe(e => { changes = e.changes }) map0.set('a', 1) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) map0.set('a', 2) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1) users[0].transact(() => { map0.set('a', 3) map0.set('a', 4) }) keyChange = changes.keys.get('a') t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2) users[0].transact(() => { map0.set('b', 1) map0.set('b', 2) }) keyChange = changes.keys.get('b') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) users[0].transact(() => { map0.set('c', 1) map0.delete('c') }) t.assert(changes !== null && changes.keys.size === 0) users[0].transact(() => { map0.set('d', 1) map0.set('d', 2) }) keyChange = changes.keys.get('d') t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined) compare(users) } /** * @param {t.TestCase} _tc */ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => { const doc = new Y.Doc() const map = doc.getMap('map') let updateCalled = false let throwingObserverCalled = false let throwingDeepObserverCalled = false doc.on('update', () => { updateCalled = true }) const throwingObserver = () => { throwingObserverCalled = true throw new Error('Failure') } const throwingDeepObserver = () => { throwingDeepObserverCalled = true throw new Error('Failure') } map.observe(throwingObserver) map.observeDeep(throwingDeepObserver) t.fails(() => { map.set('y', '2') }) t.assert(updateCalled) t.assert(throwingObserverCalled) t.assert(throwingDeepObserverCalled) // check if it works again updateCalled = false throwingObserverCalled = false throwingDeepObserverCalled = false t.fails(() => { map.set('z', '3') }) t.assert(updateCalled) t.assert(throwingObserverCalled) t.assert(throwingDeepObserverCalled) t.assert(map.get('z') === '3') } /** * @param {t.TestCase} tc */ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => { const { users, map0 } = init(tc, { users: 3 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e }) map0.set('stuff', 2) t.compare(event.value, event.target.get(event.name)) compare(users) } /** * @param {t.TestCase} tc */ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc => { const { users, map0, map1, testConnector } = init(tc, { users: 3 }) /** * @type {Object} */ let event = {} map0.observe(e => { event = e }) map1.set('stuff', 2) testConnector.flushAllMessages() t.compare(event.value, event.target.get(event.name)) compare(users) } /** * @type {Array} */ const mapTransactions = [ function set (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) const value = prng.utf16String(gen) user.getMap('map').set(key, value) }, function setType (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()]) user.getMap('map').set(key, type) if (type instanceof Y.Array) { type.insert(0, [1, 2, 3, 4]) } else { type.set('deepkey', 'deepvalue') } }, function _delete (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) user.getMap('map').delete(key) } ] /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests10 = tc => { applyRandomTests(tc, mapTransactions, 3) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests40 = tc => { applyRandomTests(tc, mapTransactions, 40) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests42 = tc => { applyRandomTests(tc, mapTransactions, 42) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests43 = tc => { applyRandomTests(tc, mapTransactions, 43) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests44 = tc => { applyRandomTests(tc, mapTransactions, 44) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests45 = tc => { applyRandomTests(tc, mapTransactions, 45) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests46 = tc => { applyRandomTests(tc, mapTransactions, 46) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests300 = tc => { applyRandomTests(tc, mapTransactions, 300) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests400 = tc => { applyRandomTests(tc, mapTransactions, 400) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests500 = tc => { applyRandomTests(tc, mapTransactions, 500) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests600 = tc => { applyRandomTests(tc, mapTransactions, 600) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests1000 = tc => { applyRandomTests(tc, mapTransactions, 1000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests1800 = tc => { applyRandomTests(tc, mapTransactions, 1800) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests5000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 5000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests10000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 10000) } /** * @param {t.TestCase} tc */ export const testRepeatGeneratingYmapTests100000 = tc => { t.skip(!t.production) applyRandomTests(tc, mapTransactions, 100000) }