large scale refactoring
This commit is contained in:
82
tests/DeleteStore.tests.js
Normal file
82
tests/DeleteStore.tests.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { test } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
import { DeleteStore } from '../utils/DeleteStore.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
|
||||
/**
|
||||
* Converts a DS to an array of length 10.
|
||||
*
|
||||
* @example
|
||||
* const ds = new DeleteStore()
|
||||
* ds.mark(ID.createID(0, 0), 1, false)
|
||||
* ds.mark(ID.createID(0, 1), 1, true)
|
||||
* ds.mark(ID.createID(0, 3), 1, false)
|
||||
* dsToArray(ds) // => [0, 1, undefined, 0]
|
||||
*
|
||||
* @return {Array<(undefined|number)>} Array of numbers indicating if array[i] is deleted (0), garbage collected (1), or undeleted (undefined).
|
||||
*/
|
||||
function dsToArray (ds) {
|
||||
const array = []
|
||||
let i = 0
|
||||
ds.iterate(null, null, n => {
|
||||
// fill with null
|
||||
while (i < n._id.clock) {
|
||||
array[i++] = null
|
||||
}
|
||||
while (i < n._id.clock + n.len) {
|
||||
array[i++] = n.gc ? 1 : 0
|
||||
}
|
||||
})
|
||||
return array
|
||||
}
|
||||
|
||||
test('DeleteStore', async function ds1 (t) {
|
||||
const ds = new DeleteStore()
|
||||
ds.mark(ID.createID(0, 1), 1, false)
|
||||
ds.mark(ID.createID(0, 2), 1, false)
|
||||
ds.mark(ID.createID(0, 3), 1, false)
|
||||
t.compare(dsToArray(ds), [null, 0, 0, 0])
|
||||
ds.mark(ID.createID(0, 2), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 0, 1, 0])
|
||||
ds.mark(ID.createID(0, 1), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 0])
|
||||
ds.mark(ID.createID(0, 3), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1])
|
||||
ds.mark(ID.createID(0, 5), 1, true)
|
||||
ds.mark(ID.createID(0, 4), 1, true)
|
||||
t.compare(dsToArray(ds), [null, 1, 1, 1, 1, 1])
|
||||
ds.mark(ID.createID(0, 0), 3, false)
|
||||
t.compare(dsToArray(ds), [0, 0, 0, 1, 1, 1])
|
||||
})
|
||||
|
||||
test('random DeleteStore tests', async function randomDS (t) {
|
||||
const prng = random.createPRNG(t.getSeed())
|
||||
const ds = new DeleteStore()
|
||||
const dsArray = []
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const pos = random.int32(prng, 0, 10)
|
||||
const len = random.int32(prng, 0, 4)
|
||||
const gc = random.bool(prng)
|
||||
ds.mark(ID.createID(0, pos), len, gc)
|
||||
for (let j = 0; j < len; j++) {
|
||||
dsArray[pos + j] = gc ? 1 : 0
|
||||
}
|
||||
}
|
||||
// fill empty fields
|
||||
for (let i = 0; i < dsArray.length; i++) {
|
||||
if (dsArray[i] !== 0 && dsArray[i] !== 1) {
|
||||
dsArray[i] = null
|
||||
}
|
||||
}
|
||||
t.compare(dsToArray(ds), dsArray, 'expected DS result')
|
||||
let size = 0
|
||||
let lastEl = null
|
||||
for (let i = 0; i < dsArray.length; i++) {
|
||||
let el = dsArray[i]
|
||||
if (lastEl !== el && el !== null) {
|
||||
size++
|
||||
}
|
||||
lastEl = el
|
||||
}
|
||||
t.compare(size, ds.length, 'expected ds size')
|
||||
})
|
||||
29
tests/diff.tests.js
Normal file
29
tests/diff.tests.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { test } from 'cutest'
|
||||
import { simpleDiff } from '../lib/diff.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
function runDiffTest (t, a, b, expected) {
|
||||
let result = simpleDiff(a, b)
|
||||
t.compare(result, expected, `Compare "${a}" with "${b}"`)
|
||||
}
|
||||
|
||||
test('diff tests', async function diff1 (t) {
|
||||
runDiffTest(t, 'abc', 'axc', { pos: 1, remove: 1, insert: 'x' })
|
||||
runDiffTest(t, 'bc', 'xc', { pos: 0, remove: 1, insert: 'x' })
|
||||
runDiffTest(t, 'ab', 'ax', { pos: 1, remove: 1, insert: 'x' })
|
||||
runDiffTest(t, 'b', 'x', { pos: 0, remove: 1, insert: 'x' })
|
||||
runDiffTest(t, '', 'abc', { pos: 0, remove: 0, insert: 'abc' })
|
||||
runDiffTest(t, 'abc', 'xyz', { pos: 0, remove: 3, insert: 'xyz' })
|
||||
runDiffTest(t, 'axz', 'au', { pos: 1, remove: 2, insert: 'u' })
|
||||
runDiffTest(t, 'ax', 'axy', { pos: 2, remove: 0, insert: 'y' })
|
||||
})
|
||||
|
||||
test('random diff tests', async function randomDiff (t) {
|
||||
const gen = random.createPRNG(t.getSeed() * 1000000000)
|
||||
let a = random.word(gen)
|
||||
let b = random.word(gen)
|
||||
let change = simpleDiff(a, b)
|
||||
let arr = Array.from(a)
|
||||
arr.splice(change.pos, change.remove, ...Array.from(change.insert))
|
||||
t.assert(arr.join('') === b, 'Applying change information is correct')
|
||||
})
|
||||
65
tests/encode-decode.tests.js
Normal file
65
tests/encode-decode.tests.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { test } from 'cutest'
|
||||
import { generateRandomUint32 } from '../utils/generateRandomUint32.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
function testEncoding (t, write, read, val) {
|
||||
let encoder = encoding.createEncoder()
|
||||
write(encoder, val)
|
||||
let reader = decoding.createDecoder(encoding.toBuffer(encoder))
|
||||
let result = read(reader)
|
||||
t.log(`string encode: ${JSON.stringify(val).length} bytes / binary encode: ${encoding.length(encoder)} bytes`)
|
||||
t.compare(val, result, 'Compare results')
|
||||
}
|
||||
|
||||
const writeVarUint = (encoder, val) => encoding.writeVarUint(encoder, val)
|
||||
const readVarUint = decoder => decoding.readVarUint(decoder)
|
||||
|
||||
test('varUint 1 byte', async function varUint1 (t) {
|
||||
testEncoding(t, writeVarUint, readVarUint, 42)
|
||||
})
|
||||
|
||||
test('varUint 2 bytes', async function varUint2 (t) {
|
||||
testEncoding(t, writeVarUint, readVarUint, 1 << 9 | 3)
|
||||
testEncoding(t, writeVarUint, readVarUint, 1 << 9 | 3)
|
||||
})
|
||||
test('varUint 3 bytes', async function varUint3 (t) {
|
||||
testEncoding(t, writeVarUint, readVarUint, 1 << 17 | 1 << 9 | 3)
|
||||
})
|
||||
|
||||
test('varUint 4 bytes', async function varUint4 (t) {
|
||||
testEncoding(t, writeVarUint, readVarUint, 1 << 25 | 1 << 17 | 1 << 9 | 3)
|
||||
})
|
||||
|
||||
test('varUint of 2839012934', async function varUint2839012934 (t) {
|
||||
testEncoding(t, writeVarUint, readVarUint, 2839012934)
|
||||
})
|
||||
|
||||
test('varUint random', async function varUintRandom (t) {
|
||||
const prng = random.createPRNG(t.getSeed() * Math.pow(Number.MAX_SAFE_INTEGER, 2))
|
||||
testEncoding(t, writeVarUint, readVarUint, random.int32(prng, 0, (1 << 28) - 1))
|
||||
})
|
||||
|
||||
test('varUint random user id', async function varUintRandomUserId (t) {
|
||||
t.getSeed() // enforces that this test is repeated
|
||||
testEncoding(t, writeVarUint, readVarUint, generateRandomUint32())
|
||||
})
|
||||
|
||||
const writeVarString = (encoder, val) => encoding.writeVarString(encoder, val)
|
||||
const readVarString = decoder => decoding.readVarString(decoder)
|
||||
|
||||
test('varString', async function varString (t) {
|
||||
testEncoding(t, writeVarString, readVarString, 'hello')
|
||||
testEncoding(t, writeVarString, readVarString, 'test!')
|
||||
testEncoding(t, writeVarString, readVarString, '☺☺☺')
|
||||
testEncoding(t, writeVarString, readVarString, '1234')
|
||||
testEncoding(t, writeVarString, readVarString, '쾟')
|
||||
testEncoding(t, writeVarString, readVarString, '龟') // surrogate length 3
|
||||
testEncoding(t, writeVarString, readVarString, '😝') // surrogate length 4
|
||||
})
|
||||
|
||||
test('varString random', async function varStringRandom (t) {
|
||||
const prng = random.createPRNG(t.getSeed() * 10000000)
|
||||
testEncoding(t, writeVarString, readVarString, random.utf16String(prng))
|
||||
})
|
||||
411
tests/helper.js
Normal file
411
tests/helper.js
Normal file
@@ -0,0 +1,411 @@
|
||||
|
||||
import * as Y from '../index.js'
|
||||
import { ItemJSON } from '../structs/ItemJSON.js'
|
||||
import { ItemString } from '../structs/ItemString.js'
|
||||
import { defragmentItemContent } from '../utils/defragmentItemContent.js'
|
||||
import Quill from 'quill'
|
||||
import { GC } from '../structs/GC.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
import * as syncProtocol from '../protocols/syncProtocol.js'
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import { QuillBinding } from '../bindings/quill.js'
|
||||
import { DomBinding } from '../bindings/dom/DomBinding.js'
|
||||
|
||||
export * from '../index.js'
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y
|
||||
* @param {Y.Transaction} transaction
|
||||
*/
|
||||
const afterTransaction = (y, transaction) => {
|
||||
y.mMux(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs)
|
||||
broadcastMessage(y, encoding.toBuffer(encoder))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class TestYInstance extends Y.Y {
|
||||
/**
|
||||
* @param {TestConnector} testConnector
|
||||
*/
|
||||
constructor (testConnector, clientID) {
|
||||
super()
|
||||
this.userID = clientID // overwriting clientID
|
||||
/**
|
||||
* @type {TestConnector}
|
||||
*/
|
||||
this.tc = testConnector
|
||||
/**
|
||||
* @type {Map<TestYInstance, Array<ArrayBuffer>>}
|
||||
*/
|
||||
this.receiving = new Map()
|
||||
/**
|
||||
* Message mutex
|
||||
* @type {Function}
|
||||
*/
|
||||
this.mMux = createMutex()
|
||||
testConnector.allConns.add(this)
|
||||
// set up observe on local model
|
||||
this.on('afterTransaction', afterTransaction)
|
||||
this.connect()
|
||||
}
|
||||
/**
|
||||
* Disconnect from TestConnector.
|
||||
*/
|
||||
disconnect () {
|
||||
this.receiving = new Map()
|
||||
this.tc.onlineConns.delete(this)
|
||||
}
|
||||
/**
|
||||
* Append yourself to the list of known Y instances in testconnector.
|
||||
* Also initiate sync with all clients.
|
||||
*/
|
||||
connect () {
|
||||
if (!this.tc.onlineConns.has(this)) {
|
||||
this.tc.onlineConns.add(this)
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, this)
|
||||
// publish SyncStep1
|
||||
broadcastMessage(this, encoding.toBuffer(encoder))
|
||||
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== this) {
|
||||
// remote instance sends instance to this instance
|
||||
const encoder = encoding.createEncoder()
|
||||
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
|
||||
this._receive(encoding.toBuffer(encoder), remoteYInstance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Receive a message from another client. This message is only appended to the list of receiving messages.
|
||||
* TestConnector decides when this client actually reads this message.
|
||||
*
|
||||
* @param {ArrayBuffer} message
|
||||
* @param {TestYInstance} remoteClient
|
||||
*/
|
||||
_receive (message, remoteClient) {
|
||||
let messages = this.receiving.get(remoteClient)
|
||||
if (messages === undefined) {
|
||||
messages = []
|
||||
this.receiving.set(remoteClient, messages)
|
||||
}
|
||||
messages.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of TestYInstances.
|
||||
*
|
||||
* The TestYInstances add/remove themselves from the list of connections maintained in this object.
|
||||
* I think it makes sense. Deal with it.
|
||||
*/
|
||||
export class TestConnector {
|
||||
constructor (prng) {
|
||||
/**
|
||||
* @type {Set<TestYInstance>}
|
||||
*/
|
||||
this.allConns = new Set()
|
||||
/**
|
||||
* @type {Set<TestYInstance>}
|
||||
*/
|
||||
this.onlineConns = new Set()
|
||||
/**
|
||||
* @type {random.PRNG}
|
||||
*/
|
||||
this.prng = prng
|
||||
}
|
||||
/**
|
||||
* Create a new Y instance and add it to the list of connections
|
||||
* @param {number} clientID
|
||||
*/
|
||||
createY (clientID) {
|
||||
return new TestYInstance(this, clientID)
|
||||
}
|
||||
/**
|
||||
* Choose random connection and flush a random message from a random sender.
|
||||
*
|
||||
* If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise.
|
||||
* @return {boolean}
|
||||
*/
|
||||
flushRandomMessage () {
|
||||
const prng = this.prng
|
||||
const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0)
|
||||
if (conns.length > 0) {
|
||||
const receiver = random.oneOf(prng, conns)
|
||||
const [sender, messages] = random.oneOf(prng, Array.from(receiver.receiving))
|
||||
const m = messages.shift()
|
||||
if (messages.length === 0) {
|
||||
receiver.receiving.delete(sender)
|
||||
}
|
||||
const encoder = encoding.createEncoder()
|
||||
receiver.mMux(() => {
|
||||
console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
|
||||
// do not publish data created when this function is executed (could be ss2 or update message)
|
||||
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver)
|
||||
})
|
||||
if (encoding.length(encoder) > 0) {
|
||||
// send reply message
|
||||
sender._receive(encoding.toBuffer(encoder), receiver)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* @return {boolean} True iff this function actually flushed something
|
||||
*/
|
||||
flushAllMessages () {
|
||||
let didSomething = false
|
||||
while (this.flushRandomMessage()) {
|
||||
didSomething = true
|
||||
}
|
||||
return didSomething
|
||||
}
|
||||
reconnectAll () {
|
||||
this.allConns.forEach(conn => conn.connect())
|
||||
}
|
||||
disconnectAll () {
|
||||
this.allConns.forEach(conn => conn.disconnect())
|
||||
}
|
||||
syncAll () {
|
||||
this.reconnectAll()
|
||||
this.flushAllMessages()
|
||||
}
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to disconnect a randon connection.
|
||||
*/
|
||||
disconnectRandom () {
|
||||
if (this.onlineConns.size === 0) {
|
||||
return false
|
||||
}
|
||||
random.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
|
||||
return true
|
||||
}
|
||||
/**
|
||||
* @return {boolean} Whether it was possible to reconnect a random connection.
|
||||
*/
|
||||
reconnectRandom () {
|
||||
const reconnectable = []
|
||||
this.allConns.forEach(conn => {
|
||||
if (!this.onlineConns.has(conn)) {
|
||||
reconnectable.push(conn)
|
||||
}
|
||||
})
|
||||
if (reconnectable.length === 0) {
|
||||
return false
|
||||
}
|
||||
random.oneOf(this.prng, reconnectable).connect()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TestYInstance} y // publish message created by `y` to all other online clients
|
||||
* @param {ArrayBuffer} m
|
||||
*/
|
||||
const broadcastMessage = (y, m) => {
|
||||
if (y.tc.onlineConns.has(y)) {
|
||||
y.tc.onlineConns.forEach(remoteYInstance => {
|
||||
if (remoteYInstance !== y) {
|
||||
remoteYInstance._receive(m, y)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DS to a proper DeleteSet of Map.
|
||||
*
|
||||
* @param {Y.Y} y
|
||||
* @return {Object<number, Array<[number, number, boolean]>>}
|
||||
*/
|
||||
const getDeleteSet = y => {
|
||||
/**
|
||||
* @type {Object<number, Array<[number, number, boolean]>}
|
||||
*/
|
||||
var ds = {}
|
||||
y.ds.iterate(null, null, n => {
|
||||
var user = n._id.user
|
||||
var counter = n._id.clock
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
var dv = ds[user]
|
||||
if (dv === void 0) {
|
||||
dv = []
|
||||
ds[user] = dv
|
||||
}
|
||||
dv.push([counter, len, gc])
|
||||
})
|
||||
return ds
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. reconnect and flush all
|
||||
* 2. user 0 gc
|
||||
* 3. get type content
|
||||
* 4. disconnect & reconnect all (so gc is propagated)
|
||||
* 5. compare os, ds, ss
|
||||
*
|
||||
* @param {any} t
|
||||
* @param {Array<TestYInstance>} users
|
||||
*/
|
||||
export const compareUsers = (t, users) => {
|
||||
users.forEach(u => u.connect())
|
||||
do {
|
||||
users.forEach(u => {
|
||||
// flush dom changes
|
||||
u.domBinding._beforeTransactionHandler(null, null, false)
|
||||
})
|
||||
} while (users[0].tc.flushAllMessages())
|
||||
|
||||
var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
||||
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
|
||||
var userXmlValues = users.map(u => u.define('xml', Y.XmlElement).toString())
|
||||
var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
|
||||
var userQuillValues = users.map(u => {
|
||||
u.quill.update('yjs') // get latest changes
|
||||
return u.quill.getContents().ops
|
||||
})
|
||||
|
||||
var data = users.map(u => {
|
||||
defragmentItemContent(u)
|
||||
var data = {}
|
||||
let ops = []
|
||||
u.os.iterate(null, null, op => {
|
||||
let json
|
||||
if (op.constructor === GC) {
|
||||
json = {
|
||||
type: 'GC',
|
||||
id: op._id,
|
||||
length: op._length,
|
||||
content: null
|
||||
}
|
||||
} else {
|
||||
json = {
|
||||
id: op._id,
|
||||
left: op._left === null ? null : op._left._lastId,
|
||||
right: op._right === null ? null : op._right._id,
|
||||
length: op._length,
|
||||
deleted: op._deleted,
|
||||
parent: op._parent._id,
|
||||
content: null
|
||||
}
|
||||
}
|
||||
if (op instanceof ItemJSON || op instanceof ItemString) {
|
||||
json.content = op._content
|
||||
}
|
||||
ops.push(json)
|
||||
})
|
||||
data.os = ops
|
||||
data.ds = getDeleteSet(u)
|
||||
const ss = {}
|
||||
u.ss.state.forEach((clock, user) => {
|
||||
ss[user] = clock
|
||||
})
|
||||
data.ss = ss
|
||||
return data
|
||||
})
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
t.group(() => {
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
||||
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
|
||||
t.compare(userTextValues[i], userTextValues[i + 1], 'text types')
|
||||
t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content')
|
||||
t.compare(data[i].os, data[i + 1].os, 'os')
|
||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||
}, `Compare user${i} with user${i + 1}`)
|
||||
}
|
||||
users.forEach(user => {
|
||||
if (user._missingStructs.size !== 0) {
|
||||
t.fail('missing structs should mes empty!')
|
||||
}
|
||||
})
|
||||
users.map(u => u.destroy())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} nodeName
|
||||
* @param {Map<string, string>} attrs
|
||||
* @return {null|Map<string, string>}
|
||||
*/
|
||||
const filter = (nodeName, attrs) => {
|
||||
if (nodeName === 'HIDDEN') {
|
||||
return null
|
||||
}
|
||||
attrs.delete('hidden')
|
||||
return attrs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} t
|
||||
* @param {any} opts
|
||||
* @return {any}
|
||||
*/
|
||||
export const initArrays = (t, opts) => {
|
||||
var result = {
|
||||
users: []
|
||||
}
|
||||
var prng = opts.prng || random.createPRNG(t.getSeed())
|
||||
const testConnector = new TestConnector(prng)
|
||||
result.testConnector = testConnector
|
||||
for (let i = 0; i < opts.users; i++) {
|
||||
let y = testConnector.createY(i)
|
||||
result.users.push(y)
|
||||
result['array' + i] = y.define('array', Y.Array)
|
||||
result['map' + i] = y.define('map', Y.Map)
|
||||
const yxml = y.define('xml', Y.XmlElement)
|
||||
result['xml' + i] = yxml
|
||||
const dom = document.createElement('my-dom')
|
||||
const domBinding = new DomBinding(yxml, dom, { filter })
|
||||
result['domBinding' + i] = domBinding
|
||||
result['dom' + i] = dom
|
||||
const textType = y.define('text', Y.Text)
|
||||
result['text' + i] = textType
|
||||
const quill = new Quill(document.createElement('div'))
|
||||
result['quillBinding' + i] = new QuillBinding(textType, quill)
|
||||
result['quill' + i] = quill
|
||||
y.quill = quill // put quill on the y object (so we can use it later)
|
||||
y.dom = dom
|
||||
y.domBinding = domBinding
|
||||
}
|
||||
testConnector.syncAll()
|
||||
return result
|
||||
}
|
||||
|
||||
export const applyRandomTests = (t, mods, iterations) => {
|
||||
const prng = random.createPRNG(t.getSeed())
|
||||
const result = initArrays(t, { users: 5, prng })
|
||||
const { testConnector, users } = result
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
if (random.int32(prng, 0, 100) <= 2) {
|
||||
// 2% chance to disconnect/reconnect a random user
|
||||
if (random.bool(prng)) {
|
||||
testConnector.disconnectRandom()
|
||||
} else {
|
||||
testConnector.reconnectRandom()
|
||||
}
|
||||
} else if (random.int32(prng, 0, 100) <= 1) {
|
||||
// 1% chance to flush all & garbagecollect
|
||||
// TODO: We do not gc all users as this does not work yet
|
||||
// await garbageCollectUsers(t, users)
|
||||
testConnector.flushAllMessages()
|
||||
// await users[0].db.emptyGarbageCollector() // TODO: reintroduce GC tests!
|
||||
} else if (random.int32(prng, 0, 100) <= 50) {
|
||||
// 50% chance to flush a random message
|
||||
testConnector.flushRandomMessage()
|
||||
}
|
||||
let user = random.oneOf(prng, users)
|
||||
var test = random.oneOf(prng, mods)
|
||||
test(t, user, prng)
|
||||
}
|
||||
compareUsers(t, users)
|
||||
return result
|
||||
}
|
||||
8
tests/index.html
Normal file
8
tests/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./diff.tests.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
tests/index.js
Normal file
9
tests/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// TODO: include all tests
|
||||
import './red-black-tree.js'
|
||||
import './y-array.tests.js'
|
||||
import './y-text.tests.js'
|
||||
import './y-map.tests.js'
|
||||
import './y-xml.tests.js'
|
||||
import './encode-decode.tests.js'
|
||||
import './diff.tests.js'
|
||||
import './prosemirror.test.js'
|
||||
37
tests/prosemirror.test.js
Normal file
37
tests/prosemirror.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
import * as Y from '../index.js'
|
||||
|
||||
import { prosemirrorPlugin } from '../bindings/prosemirror.js'
|
||||
import {EditorState} from 'prosemirror-state'
|
||||
import {EditorView} from 'prosemirror-view'
|
||||
import {schema} from 'prosemirror-schema-basic'
|
||||
import {exampleSetup} from 'prosemirror-example-setup'
|
||||
|
||||
const createNewProsemirrorView = y => {
|
||||
const view = new EditorView(document.createElement('div'), {
|
||||
state: EditorState.create({
|
||||
schema,
|
||||
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(y.define('prosemirror', Y.XmlFragment))])
|
||||
})
|
||||
})
|
||||
return view
|
||||
}
|
||||
|
||||
test('random prosemirror insertions', async t => {
|
||||
const gen = random.createPRNG(t.getSeed())
|
||||
const y = new Y.Y()
|
||||
const p1 = createNewProsemirrorView(y)
|
||||
const p2 = createNewProsemirrorView(y)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const p = random.oneOf(gen, [p1, p2])
|
||||
const insertPos = random.int32(gen, 0, p.state.doc.content.size)
|
||||
const overwrite = random.int32(gen, 0, p.state.doc.content.size - insertPos)
|
||||
p.dispatch(p.state.tr.insertText('' + i, insertPos, insertPos + overwrite))
|
||||
}
|
||||
t.compare(
|
||||
p1.state.doc.toJSON(),
|
||||
p2.state.doc.toJSON(),
|
||||
'compare prosemirror models'
|
||||
)
|
||||
})
|
||||
192
tests/red-black-tree.js
Normal file
192
tests/red-black-tree.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Tree as RedBlackTree } from '../lib/Tree.js'
|
||||
import * as ID from '../utils/ID.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
var numberOfRBTreeTests = 10000
|
||||
|
||||
const checkRedNodesDoNotHaveBlackChildren = (t, tree) => {
|
||||
let correct = true
|
||||
const traverse = n => {
|
||||
if (n == null) {
|
||||
return
|
||||
}
|
||||
if (n.isRed()) {
|
||||
if (n.left != null) {
|
||||
correct = correct && !n.left.isRed()
|
||||
}
|
||||
if (n.right != null) {
|
||||
correct = correct && !n.right.isRed()
|
||||
}
|
||||
}
|
||||
traverse(n.left)
|
||||
traverse(n.right)
|
||||
}
|
||||
traverse(tree.root)
|
||||
t.assert(correct, 'Red nodes do not have black children')
|
||||
}
|
||||
|
||||
const checkBlackHeightOfSubTreesAreEqual = (t, tree) => {
|
||||
let correct = true
|
||||
const traverse = n => {
|
||||
if (n == null) {
|
||||
return 0
|
||||
}
|
||||
var sub1 = traverse(n.left)
|
||||
var sub2 = traverse(n.right)
|
||||
if (sub1 !== sub2) {
|
||||
correct = false
|
||||
}
|
||||
if (n.isRed()) {
|
||||
return sub1
|
||||
} else {
|
||||
return sub1 + 1
|
||||
}
|
||||
}
|
||||
traverse(tree.root)
|
||||
t.assert(correct, 'Black-height of sub-trees are equal')
|
||||
}
|
||||
|
||||
const checkRootNodeIsBlack = (t, tree) => {
|
||||
t.assert(tree.root == null || tree.root.isBlack(), 'root node is black')
|
||||
}
|
||||
|
||||
test('RedBlack Tree', async function redBlackTree (t) {
|
||||
let tree = new RedBlackTree()
|
||||
tree.put({_id: ID.createID(8433, 0)})
|
||||
tree.put({_id: ID.createID(12844, 0)})
|
||||
tree.put({_id: ID.createID(1795, 0)})
|
||||
tree.put({_id: ID.createID(30302, 0)})
|
||||
tree.put({_id: ID.createID(64287)})
|
||||
tree.delete(ID.createID(8433, 0))
|
||||
tree.put({_id: ID.createID(28996)})
|
||||
tree.delete(ID.createID(64287))
|
||||
tree.put({_id: ID.createID(22721)})
|
||||
checkRootNodeIsBlack(t, tree)
|
||||
checkBlackHeightOfSubTreesAreEqual(t, tree)
|
||||
checkRedNodesDoNotHaveBlackChildren(t, tree)
|
||||
})
|
||||
|
||||
test(`random tests (${numberOfRBTreeTests})`, async function randomRBTree (t) {
|
||||
let prng = random.createPRNG(t.getSeed() * 1000000000)
|
||||
let tree = new RedBlackTree()
|
||||
let elements = []
|
||||
for (var i = 0; i < numberOfRBTreeTests; i++) {
|
||||
if (random.int32(prng, 0, 100) < 80) {
|
||||
// 80% chance to insert an element
|
||||
let obj = ID.createID(random.int32(prng, 0, numberOfRBTreeTests), random.int32(prng, 0, 1))
|
||||
let nodeExists = tree.find(obj)
|
||||
if (nodeExists === null) {
|
||||
if (elements.some(e => e.equals(obj))) {
|
||||
t.assert(false, 'tree and elements contain different results')
|
||||
}
|
||||
elements.push(obj)
|
||||
tree.put({_id: obj})
|
||||
}
|
||||
} else if (elements.length > 0) {
|
||||
// ~20% chance to delete an element
|
||||
var elem = random.oneOf(prng, elements)
|
||||
elements = elements.filter(e => {
|
||||
return !e.equals(elem)
|
||||
})
|
||||
tree.delete(elem)
|
||||
}
|
||||
}
|
||||
checkRootNodeIsBlack(t, tree)
|
||||
checkBlackHeightOfSubTreesAreEqual(t, tree)
|
||||
checkRedNodesDoNotHaveBlackChildren(t, tree)
|
||||
// TEST if all nodes exist
|
||||
let allNodesExist = true
|
||||
for (let id of elements) {
|
||||
let node = tree.find(id)
|
||||
if (!node._id.equals(id)) {
|
||||
allNodesExist = false
|
||||
}
|
||||
}
|
||||
t.assert(allNodesExist, 'All inserted nodes exist')
|
||||
// TEST lower bound search
|
||||
let findAllNodesWithLowerBoundSerach = true
|
||||
for (let id of elements) {
|
||||
let node = tree.findWithLowerBound(id)
|
||||
if (!node._id.equals(id)) {
|
||||
findAllNodesWithLowerBoundSerach = false
|
||||
}
|
||||
}
|
||||
t.assert(
|
||||
findAllNodesWithLowerBoundSerach,
|
||||
'Find every object with lower bound search'
|
||||
)
|
||||
// TEST iteration (with lower bound search)
|
||||
let lowerBound = random.oneOf(prng, elements)
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
(lowerBound.lessThan(e) || e.equals(lowerBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
let actualResults = 0
|
||||
tree.iterate(lowerBound, null, val => {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'Iterating over a tree with lower bound yields the right amount of results'
|
||||
)
|
||||
|
||||
expectedResults = elements.filter((e, pos) =>
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
actualResults = 0
|
||||
tree.iterate(null, null, val => {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree without bounds yields the right amount of results'
|
||||
)
|
||||
|
||||
let upperBound = random.oneOf(prng, elements)
|
||||
expectedResults = elements.filter((e, pos) =>
|
||||
(e.lessThan(upperBound) || e.equals(upperBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
actualResults = 0
|
||||
tree.iterate(null, upperBound, val => {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree with upper bound yields the right amount of results'
|
||||
)
|
||||
|
||||
upperBound = random.oneOf(prng, elements)
|
||||
lowerBound = random.oneOf(prng, elements)
|
||||
if (upperBound.lessThan(lowerBound)) {
|
||||
[lowerBound, upperBound] = [upperBound, lowerBound]
|
||||
}
|
||||
expectedResults = elements.filter((e, pos) =>
|
||||
(lowerBound.lessThan(e) || e.equals(lowerBound)) &&
|
||||
(e.lessThan(upperBound) || e.equals(upperBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
actualResults = 0
|
||||
tree.iterate(lowerBound, upperBound, val => {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree with upper bound yields the right amount of results'
|
||||
)
|
||||
})
|
||||
319
tests/y-array.tests.js
Normal file
319
tests/y-array.tests.js
Normal file
@@ -0,0 +1,319 @@
|
||||
import { initArrays, compareUsers, applyRandomTests } from './helper.js'
|
||||
import * as Y from '../index.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
proxyConsole()
|
||||
test('basic spec', async function array0 (t) {
|
||||
let { users, array0 } = await initArrays(t, { users: 2 })
|
||||
|
||||
array0.delete(0, 0)
|
||||
t.assert(true, 'Does not throw when deleting zero elements with position 0')
|
||||
|
||||
let throwInvalidPosition = false
|
||||
try {
|
||||
array0.delete(1, 1)
|
||||
} catch (e) {
|
||||
throwInvalidPosition = true
|
||||
}
|
||||
t.assert(throwInvalidPosition, 'Throws when deleting with an invalid position')
|
||||
|
||||
array0.insert(0, ['A'])
|
||||
array0.delete(1, 0)
|
||||
t.assert(true, 'Does not throw when deleting zero elements with valid position 1')
|
||||
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insert three elements, try re-get property', async function array1 (t) {
|
||||
var { testConnector, users, array0, array1 } = initArrays(t, { users: 2 })
|
||||
array0.insert(0, [1, 2, 3])
|
||||
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works')
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('concurrent insert (handle three conflicts)', async function array2 (t) {
|
||||
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, [0])
|
||||
array1.insert(0, [1])
|
||||
array2.insert(0, [2])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('concurrent insert&delete (handle three conflicts)', async function array3 (t) {
|
||||
var { testConnector, users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
testConnector.flushAllMessages()
|
||||
array0.insert(1, [0])
|
||||
array1.delete(0)
|
||||
array1.delete(1, 1)
|
||||
array2.insert(1, [2])
|
||||
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insertions work in late sync', async function array4 (t) {
|
||||
var { testConnector, users, array0, array1, array2 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
testConnector.flushAllMessages()
|
||||
users[1].disconnect()
|
||||
users[2].disconnect()
|
||||
array0.insert(1, ['user0'])
|
||||
array1.insert(1, ['user1'])
|
||||
array2.insert(1, ['user2'])
|
||||
await users[1].connect()
|
||||
await users[2].connect()
|
||||
testConnector.flushAllMessages()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('disconnect really prevents sending messages', async function array5 (t) {
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
testConnector.flushAllMessages()
|
||||
users[1].disconnect()
|
||||
users[2].disconnect()
|
||||
array0.insert(1, ['user0'])
|
||||
array1.insert(1, ['user1'])
|
||||
t.compare(array0.toJSON(), ['x', 'user0', 'y'])
|
||||
t.compare(array1.toJSON(), ['x', 'user1', 'y'])
|
||||
await users[1].connect()
|
||||
await users[2].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('deletions in late sync', async function array6 (t) {
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
array0.insert(0, ['x', 'y'])
|
||||
testConnector.flushAllMessages()
|
||||
await users[1].disconnect()
|
||||
array1.delete(1, 1)
|
||||
array0.delete(0, 2)
|
||||
await users[1].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insert, then marge delete on sync', async function array7 (t) {
|
||||
var { testConnector, users, array0, array1 } = await initArrays(t, { users: 2 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
testConnector.flushAllMessages()
|
||||
users[0].disconnect()
|
||||
array1.delete(0, 3)
|
||||
users[0].connect()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
function compareEvent (t, is, should) {
|
||||
for (var key in should) {
|
||||
t.assert(
|
||||
should[key] === is[key] ||
|
||||
JSON.stringify(should[key]) === JSON.stringify(is[key])
|
||||
, 'event works as expected'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test('insert & delete events', async function array8 (t) {
|
||||
var { array0, users } = await initArrays(t, { users: 2 })
|
||||
var event
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array0.insert(0, [0, 1, 2])
|
||||
compareEvent(t, event, {
|
||||
remote: false
|
||||
})
|
||||
array0.delete(0)
|
||||
compareEvent(t, event, {
|
||||
remote: false
|
||||
})
|
||||
array0.delete(0, 2)
|
||||
compareEvent(t, event, {
|
||||
remote: false
|
||||
})
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insert & delete events for types', async function array9 (t) {
|
||||
var { array0, users } = await initArrays(t, { users: 2 })
|
||||
var event
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array0.insert(0, [Y.Array])
|
||||
compareEvent(t, event, {
|
||||
remote: false
|
||||
})
|
||||
array0.delete(0)
|
||||
compareEvent(t, event, {
|
||||
remote: false
|
||||
})
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('insert & delete events for types (2)', async function array10 (t) {
|
||||
var { array0, users } = await initArrays(t, { users: 2 })
|
||||
var events = []
|
||||
array0.observe(e => {
|
||||
events.push(e)
|
||||
})
|
||||
array0.insert(0, ['hi', Y.Map])
|
||||
compareEvent(t, events[0], {
|
||||
remote: false
|
||||
})
|
||||
t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements')
|
||||
array0.delete(1)
|
||||
compareEvent(t, events[1], {
|
||||
remote: false
|
||||
})
|
||||
t.assert(events.length === 2, 'Event is triggered exactly once for deletion')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('garbage collector', async function gc1 (t) {
|
||||
var { testConnector, users, array0 } = await initArrays(t, { users: 3 })
|
||||
array0.insert(0, ['x', 'y', 'z'])
|
||||
testConnector.flushAllMessages()
|
||||
users[0].disconnect()
|
||||
array0.delete(0, 3)
|
||||
await users[0].connect()
|
||||
testConnector.flushAllMessages()
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('event target is set correctly (local)', async function array11 (t) {
|
||||
let { array0, users } = await initArrays(t, { users: 3 })
|
||||
var event
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array0.insert(0, ['stuff'])
|
||||
t.assert(event.target === array0, '"target" property is set correctly')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('event target is set correctly (remote user)', async function array12 (t) {
|
||||
let { testConnector, array0, array1, users } = await initArrays(t, { users: 3 })
|
||||
var event
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array1.insert(0, ['stuff'])
|
||||
testConnector.flushAllMessages()
|
||||
compareEvent(t, event, {
|
||||
remote: true
|
||||
})
|
||||
t.assert(event.target === array0, '"target" property is set correctly')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
var _uniqueNumber = 0
|
||||
function getUniqueNumber () {
|
||||
return _uniqueNumber++
|
||||
}
|
||||
|
||||
var arrayTransactions = [
|
||||
function insert (t, user, prng) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
var uniqueNumber = getUniqueNumber()
|
||||
var content = []
|
||||
var len = random.int32(prng, 1, 4)
|
||||
for (var i = 0; i < len; i++) {
|
||||
content.push(uniqueNumber)
|
||||
}
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, content)
|
||||
},
|
||||
function insertTypeArray (t, user, prng) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Array])
|
||||
var array2 = yarray.get(pos)
|
||||
array2.insert(0, [1, 2, 3, 4])
|
||||
},
|
||||
function insertTypeMap (t, user, prng) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
var pos = random.int32(prng, 0, yarray.length)
|
||||
yarray.insert(pos, [Y.Map])
|
||||
var map = yarray.get(pos)
|
||||
map.set('someprop', 42)
|
||||
map.set('someprop', 43)
|
||||
map.set('someprop', 44)
|
||||
},
|
||||
function _delete (t, user, prng) {
|
||||
const yarray = user.define('array', Y.Array)
|
||||
var length = yarray.length
|
||||
if (length > 0) {
|
||||
var somePos = random.int32(prng, 0, length - 1)
|
||||
var delLength = random.int32(prng, 1, Math.min(2, length - somePos))
|
||||
if (yarray instanceof Y.Array) {
|
||||
if (random.bool(prng)) {
|
||||
var type = yarray.get(somePos)
|
||||
if (type.length > 0) {
|
||||
somePos = random.int32(prng, 0, type.length - 1)
|
||||
delLength = random.int32(prng, 0, Math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
yarray.delete(somePos, delLength)
|
||||
}
|
||||
} else {
|
||||
yarray.delete(somePos, delLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
test('y-array: Random tests (20)', async function randomArray20 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 20)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (42)', async function randomArray42 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 42)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (43)', async function randomArray43 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 43)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (44)', async function randomArray44 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 44)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (45)', async function randomArray45 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 45)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (46)', async function randomArray46 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 46)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (47)', async function randomArray47 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 47)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (300)', async function randomArray300 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 200)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (500)', async function randomArray500 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 300)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (600)', async function randomArray600 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 400)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (700)', async function randomArray700 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 500)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (1000)', async function randomArray1000 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 1000)
|
||||
})
|
||||
|
||||
test('y-array: Random tests (1800)', async function randomArray1800 (t) {
|
||||
await applyRandomTests(t, arrayTransactions, 2000)
|
||||
})
|
||||
366
tests/y-map.tests.js
Normal file
366
tests/y-map.tests.js
Normal file
@@ -0,0 +1,366 @@
|
||||
import { initArrays, compareUsers, applyRandomTests } from './helper.js'
|
||||
import * as Y from '../index.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
test('basic map tests', async function map0 (t) {
|
||||
let { testConnector, users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
users[2].disconnect()
|
||||
|
||||
map0.set('number', 1)
|
||||
map0.set('string', 'hello Y')
|
||||
map0.set('object', { key: { key2: 'value' } })
|
||||
map0.set('y-map', new Y.Map())
|
||||
let map = map0.get('y-map')
|
||||
map.set('y-array', new Y.Array())
|
||||
let array = map.get('y-array')
|
||||
array.insert(0, [0])
|
||||
array.insert(0, [-1])
|
||||
|
||||
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.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)')
|
||||
|
||||
await users[2].connect()
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
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.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)')
|
||||
|
||||
// compare disconnected user
|
||||
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.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')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
|
||||
let { testConnector, users, map0 } = await initArrays(t, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
map0.set('undefined', undefined)
|
||||
map0.set('null', null)
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
t.assert(u.get('undefined') === undefined, 'undefined')
|
||||
t.compare(u.get('null'), null, 'null')
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Map can set custom types (Map)', async function map2 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
var map = map0.set('Map', new Y.Map())
|
||||
map.set('one', 1)
|
||||
map = map0.get('Map')
|
||||
t.compare(map.get('one'), 1)
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Map can set custom types (Map) - get also returns the type', async function map3 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
map0.set('Map', new Y.Map())
|
||||
var map = map0.get('Map')
|
||||
map.set('one', 1)
|
||||
map = map0.get('Map')
|
||||
t.compare(map.get('one'), 1)
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Map can set custom types (Array)', async function map4 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
var array = map0.set('Array', new Y.Array())
|
||||
array.insert(0, [1, 2, 3])
|
||||
array = map0.get('Array')
|
||||
t.compare(array.toArray(), [1, 2, 3])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (converge via update)', async function map5 (t) {
|
||||
let { testConnector, users, map0 } = await initArrays(t, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (handle conflict)', async function map6 (t) {
|
||||
let { testConnector, users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.compare(u.get('stuff'), 'c0')
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set&delete of Map property (handle conflict)', async function map7 (t) {
|
||||
let { testConnector, users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map0.delete('stuff')
|
||||
map1.set('stuff', 'c1')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set of Map property (handle three conflicts)', async function map8 (t) {
|
||||
let { testConnector, users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.compare(u.get('stuff'), 'c0')
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Basic get&set&delete of Map property (handle three conflicts)', async function map9 (t) {
|
||||
let { testConnector, users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
map0.set('stuff', 'deleteme')
|
||||
map0.delete('stuff')
|
||||
map1.set('stuff', 'c1')
|
||||
map2.set('stuff', 'c2')
|
||||
map3.set('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
for (let user of users) {
|
||||
var u = user.define('map', Y.Map)
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
/* TODO reimplement observePath
|
||||
test('observePath properties', async function map10 (t) {
|
||||
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
|
||||
let map
|
||||
map0.observePath(['map'], map => {
|
||||
if (map != null) {
|
||||
map.set('yay', 4)
|
||||
}
|
||||
})
|
||||
map1.set('map', new Y.Map())
|
||||
testConnector.flushAllMessages()
|
||||
map = map2.get('map')
|
||||
t.compare(map.get('yay'), 4)
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
*/
|
||||
|
||||
test('observe deep properties', async function map11 (t) {
|
||||
let { testConnector, users, map1, map2, map3 } = await initArrays(t, { users: 4 })
|
||||
var _map1 = map1.set('map', new Y.Map())
|
||||
var calls = 0
|
||||
var dmapid
|
||||
map1.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
calls++
|
||||
t.assert(event.keysChanged.has('deepmap'))
|
||||
t.assert(event.path.length === 1)
|
||||
t.assert(event.path[0] === 'map')
|
||||
dmapid = event.target.get('deepmap')._id
|
||||
})
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
var _map3 = map3.get('map')
|
||||
_map3.set('deepmap', new Y.Map())
|
||||
testConnector.flushAllMessages()
|
||||
var _map2 = map2.get('map')
|
||||
_map2.set('deepmap', new Y.Map())
|
||||
testConnector.flushAllMessages()
|
||||
var dmap1 = _map1.get('deepmap')
|
||||
var dmap2 = _map2.get('deepmap')
|
||||
var dmap3 = _map3.get('deepmap')
|
||||
t.assert(calls > 0)
|
||||
t.assert(dmap1._id.equals(dmap2._id))
|
||||
t.assert(dmap1._id.equals(dmap3._id))
|
||||
t.assert(dmap1._id.equals(dmapid))
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('observes using observeDeep', async function map12 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
var pathes = []
|
||||
var calls = 0
|
||||
map0.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
pathes.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(pathes, [[], ['map'], ['map', 'array']])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
/* TODO: Test events in Y.Map
|
||||
function compareEvent (t, is, should) {
|
||||
for (var key in should) {
|
||||
t.assert(should[key] === is[key])
|
||||
}
|
||||
}
|
||||
|
||||
test('throws add & update & delete events (with type and primitive content)', async function map13 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 2 })
|
||||
var event
|
||||
await flushAll(t, users)
|
||||
map0.observe(e => {
|
||||
event = e // just put it on event, should be thrown synchronously anyway
|
||||
})
|
||||
map0.set('stuff', 4)
|
||||
compareEvent(t, event, {
|
||||
type: 'add',
|
||||
object: map0,
|
||||
name: 'stuff'
|
||||
})
|
||||
// update, oldValue is in contents
|
||||
map0.set('stuff', new Y.Array())
|
||||
compareEvent(t, event, {
|
||||
type: 'update',
|
||||
object: map0,
|
||||
name: 'stuff',
|
||||
oldValue: 4
|
||||
})
|
||||
var replacedArray = map0.get('stuff')
|
||||
// update, oldValue is in opContents
|
||||
map0.set('stuff', 5)
|
||||
var array = event.oldValue
|
||||
t.compare(array._model, replacedArray._model)
|
||||
// delete
|
||||
map0.delete('stuff')
|
||||
compareEvent(t, event, {
|
||||
type: 'delete',
|
||||
name: 'stuff',
|
||||
object: map0,
|
||||
oldValue: 5
|
||||
})
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
*/
|
||||
|
||||
/* reimplement event.value somehow (probably with ss vector)
|
||||
test('event has correct value when setting a primitive on a YMap (same user)', async function map14 (t) {
|
||||
let { users, map0 } = await initArrays(t, { users: 3 })
|
||||
var event
|
||||
await flushAll(t, users)
|
||||
map0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
map0.set('stuff', 2)
|
||||
t.compare(event.value, event.target.get(event.name))
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('event has correct value when setting a primitive on a YMap (received from another user)', async function map15 (t) {
|
||||
let { users, map0, map1 } = await initArrays(t, { users: 3 })
|
||||
var event
|
||||
await flushAll(t, users)
|
||||
map0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
map1.set('stuff', 2)
|
||||
await flushAll(t, users)
|
||||
t.compare(event.value, event.target.get(event.name))
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
*/
|
||||
|
||||
var mapTransactions = [
|
||||
function set (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
var value = random.utf16String(prng)
|
||||
user.define('map', Y.Map).set(key, value)
|
||||
},
|
||||
function setType (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
var type = random.oneOf(prng, [new Y.Array(), new Y.Map()])
|
||||
user.define('map', Y.Map).set(key, type)
|
||||
if (type instanceof Y.Array) {
|
||||
type.insert(0, [1, 2, 3, 4])
|
||||
} else {
|
||||
type.set('deepkey', 'deepvalue')
|
||||
}
|
||||
},
|
||||
function _delete (t, user, prng) {
|
||||
let key = random.oneOf(prng, ['one', 'two'])
|
||||
user.define('map', Y.Map).delete(key)
|
||||
}
|
||||
]
|
||||
|
||||
test('y-map: Random tests (42)', async function randomMap42 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 42)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (43)', async function randomMap43 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 43)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (44)', async function randomMap44 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 44)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (45)', async function randomMap45 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 45)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (46)', async function randomMap46 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 46)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (47)', async function randomMap47 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 47)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (200)', async function randomMap200 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 200)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (300)', async function randomMap300 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 300)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (500)', async function randomMap500 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 500)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (1000)', async function randomMap1000 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 1000)
|
||||
})
|
||||
|
||||
test('y-map: Random tests (1800)', async function randomMap1800 (t) {
|
||||
await applyRandomTests(t, mapTransactions, 1800)
|
||||
})
|
||||
102
tests/y-text.tests.js
Normal file
102
tests/y-text.tests.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { initArrays, compareUsers } from './helper.js'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
test('basic insert delete', async function text0 (t) {
|
||||
let { users, text0 } = await initArrays(t, { 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 }])
|
||||
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('basic format', async function text1 (t) {
|
||||
let { users, text0 } = await initArrays(t, { 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 } }])
|
||||
t.assert(text0._start._right._right._right._content === '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 } }])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('quill issue 1', async function quill1 (t) {
|
||||
let { testConnector, users, quill0 } = await initArrays(t, { users: 2 })
|
||||
quill0.insertText(0, 'x')
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, '\n', 'list', 'ordered')
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, '\n', 'list', 'ordered')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('quill issue 2', async function quill2 (t) {
|
||||
let { testConnector, users, quill0, text0 } = await initArrays(t, { users: 2 })
|
||||
let delta
|
||||
text0.observe(event => {
|
||||
delta = event.delta
|
||||
})
|
||||
quill0.insertText(0, 'abc', 'bold', true)
|
||||
testConnector.flushAllMessages()
|
||||
quill0.insertText(1, 'x')
|
||||
quill0.update()
|
||||
t.compare(delta, [{ retain: 1 }, { insert: 'x', attributes: { bold: true } }])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('quill issue 3', async function quill3 (t) {
|
||||
let { users, quill0, text0 } = await initArrays(t, { users: 2 })
|
||||
quill0.insertText(0, 'a')
|
||||
quill0.insertText(1, '\n\n', 'list', 'ordered')
|
||||
quill0.insertText(2, 'b')
|
||||
t.compare(text0.toDelta(), [
|
||||
{ insert: 'a' },
|
||||
{ insert: '\n', attributes: { list: 'ordered' } },
|
||||
{ insert: 'b' },
|
||||
{ insert: '\n', attributes: { list: 'ordered' } }
|
||||
])
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
416
tests/y-xml.tests.js
Normal file
416
tests/y-xml.tests.js
Normal file
@@ -0,0 +1,416 @@
|
||||
import { initArrays, compareUsers, applyRandomTests } from './helper.js'
|
||||
import { test } from 'cutest'
|
||||
import * as Y from '../index.js'
|
||||
import * as random from '../lib/prng/prng.js'
|
||||
|
||||
test('set property', async function xml0 (t) {
|
||||
var { testConnector, users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
xml0.setAttribute('height', '10')
|
||||
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('events', async function xml1 (t) {
|
||||
var { testConnector, users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
var event = { attributesChanged: new Set() }
|
||||
var remoteEvent = { attributesChanged: new Set() }
|
||||
xml0.observe(e => {
|
||||
delete e._content
|
||||
delete e.nodes
|
||||
delete e.values
|
||||
event = e
|
||||
})
|
||||
xml1.observe(e => {
|
||||
delete e._content
|
||||
delete e.nodes
|
||||
delete e.values
|
||||
remoteEvent = e
|
||||
})
|
||||
xml0.setAttribute('key', 'value')
|
||||
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)')
|
||||
// check attributeRemoved
|
||||
xml0.removeAttribute('key')
|
||||
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)')
|
||||
xml0.insert(0, [new Y.XmlText('some text')])
|
||||
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)')
|
||||
// test childRemoved
|
||||
xml0.delete(0)
|
||||
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('attribute modifications (y -> dom)', async function xml2 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.setAttribute('height', '100px')
|
||||
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
|
||||
xml0.removeAttribute('height')
|
||||
t.assert(dom0.getAttribute('height') == null, 'removeAttribute')
|
||||
xml0.setAttribute('class', 'stuffy stuff')
|
||||
t.assert(dom0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('attribute modifications (dom -> y)', async function xml3 (t) {
|
||||
var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
dom0.setAttribute('height', '100px')
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
|
||||
dom0.removeAttribute('height')
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.getAttribute('height') == null, 'removeAttribute')
|
||||
dom0.setAttribute('class', 'stuffy stuff')
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('element insert (dom -> y)', async function xml4 (t) {
|
||||
var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
dom0.insertBefore(document.createTextNode('some text'), null)
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.get(0).toString() === 'some text', 'Retrieve Text Node')
|
||||
t.assert(xml0.get(1).nodeName === 'P', 'Retrieve Element node')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('element insert (y -> dom)', async function xml5 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlText('some text')])
|
||||
xml0.insert(1, [new Y.XmlElement('p')])
|
||||
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
|
||||
t.assert(dom0.childNodes[1].nodeName === 'P', 'Retrieve Element node')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
||||
var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.length === 1, 'one node present')
|
||||
dom0.childNodes[0].remove()
|
||||
domBinding0.flushDomChanges()
|
||||
t.assert(xml0.length === 0, 'no node present after delete')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlElement('p')])
|
||||
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
|
||||
xml0.delete(0, 1)
|
||||
t.assert(dom0.childNodes.length === 0, '#childNodes is empty after delete')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (1) (Text)', async function xml8 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||
xml0.delete(1, 2)
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '1', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (2) (Text)', async function xml9 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '2', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (1) (Element)', async function xml10 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
xml0.delete(1, 2)
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'A', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (2) (Element)', async function xml11 (t) {
|
||||
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'B', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
||||
var { testConnector, users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||
users[1].disconnect()
|
||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||
xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')])
|
||||
await users[1].connect()
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml0.length === 6, 'check length (y)')
|
||||
t.assert(xml1.length === 6, 'check length (y) (reconnected user)')
|
||||
t.assert(dom0.childNodes.length === 6, 'check length (dom)')
|
||||
t.assert(dom1.childNodes.length === 6, 'check length (dom) (reconnected user)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('move element to a different position', async function xml13 (t) {
|
||||
var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
dom1.insertBefore(dom1.childNodes[0], null)
|
||||
domBinding1.flushDomChanges()
|
||||
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 0)')
|
||||
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 0)')
|
||||
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 1)')
|
||||
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 1)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('filter node', async function xml14 (t) {
|
||||
var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
let domFilter = (nodeName, attrs) => {
|
||||
if (nodeName === 'H1') {
|
||||
return null
|
||||
} else {
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
domBinding0.setFilter(domFilter)
|
||||
domBinding1.setFilter(domFilter)
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted')
|
||||
t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('filter attribute', async function xml15 (t) {
|
||||
var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
let domFilter = (nodeName, attrs) => {
|
||||
attrs.delete('hidden')
|
||||
return attrs
|
||||
}
|
||||
domBinding0.setFilter(domFilter)
|
||||
domBinding1.setFilter(domFilter)
|
||||
dom0.setAttribute('hidden', 'true')
|
||||
dom0.setAttribute('style', 'height: 30px')
|
||||
dom0.setAttribute('data-me', '77')
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute')
|
||||
t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update')
|
||||
t.assert(dom1.getAttribute('style') === 'height: 30px', 'User 1 received style update')
|
||||
t.assert(dom1.getAttribute('data-me') === '77', 'User 1 received data update')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('deep element insert', async function xml16 (t) {
|
||||
var { testConnector, users, dom0, dom1, domBinding0 } = await initArrays(t, { users: 3 })
|
||||
let deepElement = document.createElement('p')
|
||||
let boldElement = document.createElement('b')
|
||||
let attrElement = document.createElement('img')
|
||||
attrElement.setAttribute('src', 'http:localhost:8080/nowhere')
|
||||
boldElement.append(document.createTextNode('hi'))
|
||||
deepElement.append(boldElement)
|
||||
deepElement.append(attrElement)
|
||||
dom0.append(deepElement)
|
||||
let str0 = dom0.outerHTML
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
let str1 = dom1.outerHTML
|
||||
t.compare(str0, str1, 'Dom string representation matches')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('treeWalker', async function xml17 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let paragraph1 = new Y.XmlElement('p')
|
||||
let paragraph2 = new Y.XmlElement('p')
|
||||
let text1 = new Y.XmlText('init')
|
||||
let text2 = new Y.XmlText('text')
|
||||
paragraph1.insert(0, [text1, text2])
|
||||
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
|
||||
let allParagraphs = xml0.querySelectorAll('p')
|
||||
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
|
||||
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
|
||||
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')
|
||||
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
/**
|
||||
* The expected behavior is that changes on your own dom (e.g. malicious attributes) persist.
|
||||
* Yjs should just ignore them, never propagate those attributes.
|
||||
* Incoming changes that contain malicious attributes should be deleted.
|
||||
*/
|
||||
test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
||||
var { testConnector, users, xml0, xml1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||
const filter = (nodeName, attributes) => {
|
||||
attributes.delete('malicious')
|
||||
if (nodeName === 'HIDEME') {
|
||||
return null
|
||||
} else if (attributes.has('isHidden')) {
|
||||
return null
|
||||
} else {
|
||||
return attributes
|
||||
}
|
||||
}
|
||||
domBinding0.setFilter(filter)
|
||||
domBinding1.setFilter(filter)
|
||||
let paragraph = new Y.XmlElement('p')
|
||||
let hideMe = new Y.XmlElement('hideMe')
|
||||
let span = new Y.XmlElement('span')
|
||||
span.setAttribute('malicious', 'alert("give me money")')
|
||||
let tag = new Y.XmlElement('tag')
|
||||
tag.setAttribute('isHidden', 'true')
|
||||
paragraph.insert(0, [hideMe, span, tag])
|
||||
xml0.insert(0, [paragraph])
|
||||
let tag2 = new Y.XmlElement('tag')
|
||||
tag2.setAttribute('isHidden', 'true')
|
||||
paragraph.insert(0, [tag2])
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
// check dom
|
||||
domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true')
|
||||
domBinding0.typeToDom.get(span).setAttribute('malicious', 'true')
|
||||
domBinding0.flushDomChanges()
|
||||
// check incoming attributes
|
||||
xml1.get(0).get(0).setAttribute('malicious', 'true')
|
||||
xml1.insert(0, [new Y.XmlElement('hideMe')])
|
||||
domBinding0.flushDomChanges()
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
// TODO: move elements
|
||||
var xmlTransactions = [
|
||||
function attributeChange (t, user, prng) {
|
||||
// random.word generates non-empty words. prepend something
|
||||
user.dom.setAttribute('_' + random.word(prng), random.word(prng))
|
||||
user.domBinding.flushDomChanges()
|
||||
},
|
||||
function attributeChangeHidden (t, user, prng) {
|
||||
user.dom.setAttribute('hidden', random.word(prng))
|
||||
user.domBinding.flushDomChanges()
|
||||
},
|
||||
function insertText (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createTextNode(random.word(prng)), succ)
|
||||
user.domBinding.flushDomChanges()
|
||||
},
|
||||
function insertHiddenDom (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createElement('hidden'), succ)
|
||||
user.domBinding.flushDomChanges()
|
||||
},
|
||||
function insertDom (t, user, prng) {
|
||||
let dom = user.dom
|
||||
var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null
|
||||
dom.insertBefore(document.createElement('my-' + random.word(prng)), succ)
|
||||
user.domBinding.flushDomChanges()
|
||||
},
|
||||
function deleteChild (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.childNodes.length > 0) {
|
||||
var d = random.oneOf(prng, dom.childNodes)
|
||||
d.remove()
|
||||
user.domBinding.flushDomChanges()
|
||||
}
|
||||
},
|
||||
function insertTextSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createTextNode(random.word(prng)), succ)
|
||||
user.domBinding.flushDomChanges()
|
||||
}
|
||||
},
|
||||
function insertDomSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createElement('my-' + random.word(prng)), succ)
|
||||
user.domBinding.flushDomChanges()
|
||||
}
|
||||
},
|
||||
function deleteChildSecondLayer (t, user, prng) {
|
||||
let dom = user.dom
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = random.oneOf(prng, dom.children)
|
||||
if (dom2.childNodes.length > 0) {
|
||||
let d = random.oneOf(prng, dom2.childNodes)
|
||||
d.remove()
|
||||
}
|
||||
user.domBinding.flushDomChanges()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
test('y-xml: Random tests (10)', async function xmlRandom10 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 10)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (42)', async function xmlRandom42 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 42)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (43)', async function xmlRandom43 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 43)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (44)', async function xmlRandom44 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 44)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (45)', async function xmlRandom45 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 45)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (46)', async function xmlRandom46 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 46)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (47)', async function xmlRandom47 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 47)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (100)', async function xmlRandom100 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 100)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (200)', async function xmlRandom200 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 200)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (500)', async function xmlRandom500 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 500)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (1000)', async function xmlRandom1000 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 1000)
|
||||
})
|
||||
Reference in New Issue
Block a user