snapshot of https://github.com/ueberdosis/hocuspocus.git @ cf3cfa355b4d3bb61c6b6c80071f71783e32470e (2026-09-15)
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s
This commit is contained in:
79
tests/provider/awarenessEcho.ts
Normal file
79
tests/provider/awarenessEcho.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import test from 'ava'
|
||||
import type { HocuspocusProvider } from '@hocuspocus/provider'
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts'
|
||||
import { retryableAssertion } from '../utils/retryableAssertion.ts'
|
||||
|
||||
const AWARENESS_MESSAGE = 'Awareness states update'
|
||||
|
||||
const hasAwarenessField = (provider: HocuspocusProvider) =>
|
||||
Array.from(provider.awareness?.getStates().values() ?? []).some(state => state.foo === 'bar')
|
||||
|
||||
test('does not echo awareness received from the server back to it', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const first = newHocuspocusProvider(t, server, { name: 'awareness-echo' })
|
||||
|
||||
let outgoingAwarenessMessages = 0
|
||||
const second = newHocuspocusProvider(t, server, {
|
||||
name: 'awareness-echo',
|
||||
onOutgoingMessage({ message }) {
|
||||
if (message.description === AWARENESS_MESSAGE) {
|
||||
outgoingAwarenessMessages += 1
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
await new Promise(resolve => second.on('synced', () => resolve('done')))
|
||||
|
||||
// Ignore whatever the handshake produced; only the reaction to a remote
|
||||
// change is under test here.
|
||||
await sleep(100)
|
||||
outgoingAwarenessMessages = 0
|
||||
|
||||
first.setAwarenessField('foo', 'bar')
|
||||
|
||||
// The remote state does arrive, so the assertion below is not vacuous.
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.true(hasAwarenessField(second))
|
||||
})
|
||||
|
||||
await sleep(100)
|
||||
|
||||
t.is(outgoingAwarenessMessages, 0)
|
||||
})
|
||||
|
||||
test('still sends awareness changes that originate locally', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const first = newHocuspocusProvider(t, server, { name: 'awareness-local' })
|
||||
const second = newHocuspocusProvider(t, server, { name: 'awareness-local' })
|
||||
|
||||
await new Promise(resolve => second.on('synced', () => resolve('done')))
|
||||
|
||||
second.setAwarenessField('foo', 'bar')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.true(hasAwarenessField(first))
|
||||
})
|
||||
})
|
||||
|
||||
test('still tells the server when a client goes away', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const first = newHocuspocusProvider(t, server, { name: 'awareness-destroy' })
|
||||
const second = newHocuspocusProvider(t, server, { name: 'awareness-destroy' })
|
||||
|
||||
await new Promise(resolve => second.on('synced', () => resolve('done')))
|
||||
|
||||
second.setAwarenessField('foo', 'bar')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.true(hasAwarenessField(first))
|
||||
})
|
||||
|
||||
second.destroy()
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.false(hasAwarenessField(first))
|
||||
})
|
||||
})
|
||||
138
tests/provider/flushDelay.ts
Normal file
138
tests/provider/flushDelay.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts'
|
||||
import { retryableAssertion } from '../utils/retryableAssertion.ts'
|
||||
|
||||
test('batches rapid document updates into a single outgoing message', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
let updateMessages = 0
|
||||
|
||||
await new Promise(resolve => {
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
flushDelay: 100,
|
||||
onOutgoingMessage({ message }) {
|
||||
if (message.description === 'A document update') {
|
||||
updateMessages += 1
|
||||
}
|
||||
},
|
||||
async onSynced() {
|
||||
provider.document.getMap('test').set('a', 1)
|
||||
provider.document.getMap('test').set('b', 2)
|
||||
provider.document.getMap('test').set('c', 3)
|
||||
|
||||
// Still inside the flush window: nothing has been sent yet.
|
||||
t.is(updateMessages, 0)
|
||||
|
||||
await sleep(300)
|
||||
|
||||
// The three updates were merged into a single message.
|
||||
t.is(updateMessages, 1)
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('a merged batch still converges on another client', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
flushDelay: 100,
|
||||
})
|
||||
|
||||
await new Promise(resolve => {
|
||||
provider.on('synced', () => resolve('done'))
|
||||
})
|
||||
|
||||
provider.document.getMap('test').set('a', 1)
|
||||
provider.document.getMap('test').set('b', 2)
|
||||
provider.document.getMap('test').set('c', 3)
|
||||
|
||||
const reader = newHocuspocusProvider(t, server, { awareness: null })
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
const map = reader.document.getMap('test')
|
||||
tt.is(map.get('a'), 1)
|
||||
tt.is(map.get('b'), 2)
|
||||
tt.is(map.get('c'), 3)
|
||||
})
|
||||
})
|
||||
|
||||
test('sends one message per update when flushDelay is disabled', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
let updateMessages = 0
|
||||
|
||||
await new Promise(resolve => {
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
onOutgoingMessage({ message }) {
|
||||
if (message.description === 'A document update') {
|
||||
updateMessages += 1
|
||||
}
|
||||
},
|
||||
async onSynced() {
|
||||
provider.document.getMap('test').set('a', 1)
|
||||
provider.document.getMap('test').set('b', 2)
|
||||
provider.document.getMap('test').set('c', 3)
|
||||
|
||||
await sleep(100)
|
||||
|
||||
t.is(updateMessages, 3)
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('reports unsynced changes during the batch window and clears after sync', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
flushDelay: 100,
|
||||
})
|
||||
|
||||
await new Promise(resolve => {
|
||||
provider.on('synced', () => resolve('done'))
|
||||
})
|
||||
|
||||
provider.document.getMap('test').set('a', 1)
|
||||
provider.document.getMap('test').set('b', 2)
|
||||
|
||||
// Buffered but not yet sent — still counts as unsynced.
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, false)
|
||||
})
|
||||
})
|
||||
|
||||
test('collapses rapid awareness changes into a single outgoing message', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
let awarenessMessages = 0
|
||||
|
||||
await new Promise(resolve => {
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
flushDelay: 100,
|
||||
onOutgoingMessage({ message }) {
|
||||
if (message.description === 'Awareness states update') {
|
||||
awarenessMessages += 1
|
||||
}
|
||||
},
|
||||
async onSynced() {
|
||||
// Ignore any awareness sent as part of the initial sync.
|
||||
awarenessMessages = 0
|
||||
|
||||
provider.setAwarenessField('cursor', 1)
|
||||
provider.setAwarenessField('cursor', 2)
|
||||
provider.setAwarenessField('cursor', 3)
|
||||
|
||||
await sleep(300)
|
||||
|
||||
t.is(awarenessMessages, 1)
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
187
tests/provider/hasUnsyncedChanges.ts
Normal file
187
tests/provider/hasUnsyncedChanges.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import test from 'ava'
|
||||
import * as Y from 'yjs'
|
||||
import { retryableAssertion } from '../utils/retryableAssertion.ts'
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts'
|
||||
|
||||
test("initially doesn't have unsynced changes", async t => {
|
||||
return new Promise(async resolve => {
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, false)
|
||||
t.is(provider.synced, false)
|
||||
|
||||
setTimeout(() => {
|
||||
t.is(provider.hasUnsyncedChanges, false)
|
||||
t.is(provider.synced, true)
|
||||
|
||||
resolve()
|
||||
}, 200)
|
||||
})
|
||||
})
|
||||
|
||||
test('has unsynced changes when updating', async t => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: undefined,
|
||||
})
|
||||
|
||||
provider.document.getMap('test').set('foo', 'bar')
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
|
||||
// changes are synced
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, false)
|
||||
})
|
||||
})
|
||||
|
||||
test('has unsynced changes when in readonly mode', async t => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ connectionConfig }) {
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, { token: 'readonly' })
|
||||
|
||||
provider.document.getMap('test').set('foo', 'bar')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
await sleep(100)
|
||||
|
||||
// confirm that the changes are not synced later either
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
test('has no unsynced changes when in readonly mode and no changes', async t => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ connectionConfig }) {
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, { token: 'readonly' })
|
||||
|
||||
// first, unsyncedChanges is briefly set to true when we're waiting for the ack of the initial sync
|
||||
await new Promise((resolve, reject) => {
|
||||
provider.on('unsyncedChanges', () => {
|
||||
provider.off('unsyncedChanges')
|
||||
if (provider.hasUnsyncedChanges) {
|
||||
resolve('done')
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// then, it should be set to false when the sync message is confirmed
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, false)
|
||||
})
|
||||
})
|
||||
|
||||
test('has unsynced changes when in readonly mode and receiving external update', async t => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ connectionConfig, token }) {
|
||||
if (token === 'readonly') {
|
||||
connectionConfig.readOnly = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
token: 'readonly',
|
||||
})
|
||||
|
||||
provider.document.getMap('test').set('foo', 'bar')
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
|
||||
await sleep(100)
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
|
||||
const provider2 = newHocuspocusProvider(t, server, {
|
||||
token: 'full-access',
|
||||
})
|
||||
|
||||
provider2.document.getMap('test2').set('foo', 'bar')
|
||||
|
||||
t.is(provider2.hasUnsyncedChanges, true)
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider2.hasUnsyncedChanges, false)
|
||||
})
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
test('has unsynced changes when in readonly mode and initial document has changed', async t => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ connectionConfig }) {
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
const document = new Y.Doc()
|
||||
document.getMap('test').set('foo', 'bar')
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' })
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
await sleep(100)
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
test('has unsynced changes when in readonly mode and initial document has changed (deletion)', async t => {
|
||||
const document = new Y.Doc()
|
||||
document.getMap('test').set('foo', 'bar')
|
||||
const initialState = Y.encodeStateAsUpdate(document)
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onLoadDocument() {
|
||||
return initialState
|
||||
},
|
||||
async onAuthenticate({ connectionConfig }) {
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
document.getMap('test').delete('foo')
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' })
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
await sleep(100)
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, true)
|
||||
})
|
||||
|
||||
test('has no unsynced changes when in readonly mode and initial document has not changed', async t => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ connectionConfig }) {
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
const document = new Y.Doc()
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, { document, token: 'readonly' })
|
||||
|
||||
await sleep(200)
|
||||
|
||||
t.is(provider.hasUnsyncedChanges, false)
|
||||
})
|
||||
83
tests/provider/observe.ts
Normal file
83
tests/provider/observe.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import test from 'ava'
|
||||
import * as Y from 'yjs'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
import { retryableAssertion } from '../utils/retryableAssertion.ts'
|
||||
|
||||
test('observe is called just once', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observe is called …
|
||||
type.observe((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
type.insert(1, 'a')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 1)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
test('observe is called for every single change', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observe is called …
|
||||
type.observe((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
type.insert(1, 'a')
|
||||
type.insert(2, 'b')
|
||||
type.insert(3, 'c')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 3)
|
||||
})
|
||||
})
|
||||
|
||||
test('observe is called once for a single transaction', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observe is called …
|
||||
type.observe((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
Y.transact(provider.document, () => {
|
||||
type.insert(1, 'a')
|
||||
type.insert(2, 'b')
|
||||
type.insert(3, 'c')
|
||||
})
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 1)
|
||||
})
|
||||
})
|
||||
82
tests/provider/observeDeep.ts
Normal file
82
tests/provider/observeDeep.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import test from 'ava'
|
||||
import * as Y from 'yjs'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
import { retryableAssertion } from '../utils/retryableAssertion.ts'
|
||||
|
||||
test('observeDeep is called just once', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observeDeep is called …
|
||||
type.observeDeep((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
type.insert(1, 'a')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 1)
|
||||
})
|
||||
})
|
||||
|
||||
test('observeDeep is called for every single change', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observeDeep is called …
|
||||
type.observeDeep((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
type.insert(1, 'a')
|
||||
type.insert(2, 'b')
|
||||
type.insert(3, 'c')
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 3)
|
||||
})
|
||||
})
|
||||
|
||||
test('observeDeep is called once for a single transaction', async t => {
|
||||
let count = 0
|
||||
|
||||
const server = await newHocuspocus(t)
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
const type = provider.document.get(
|
||||
'xmlText',
|
||||
Y.XmlText,
|
||||
) as unknown as Y.XmlText
|
||||
|
||||
// Count how often observeDeep is called …
|
||||
type.observeDeep((events, transaction) => {
|
||||
count += 1
|
||||
})
|
||||
|
||||
// Insert something …
|
||||
Y.transact(provider.document, () => {
|
||||
type.insert(1, 'a')
|
||||
type.insert(2, 'b')
|
||||
type.insert(3, 'c')
|
||||
})
|
||||
|
||||
await retryableAssertion(t, tt => {
|
||||
tt.is(count, 1)
|
||||
})
|
||||
})
|
||||
91
tests/provider/onAuthenticated.ts
Normal file
91
tests/provider/onAuthenticated.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('executes the onAuthenticated callback', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }) {
|
||||
if (token !== 'SUPER-SECRET-TOKEN') {
|
||||
throw new Error()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
token: 'SUPER-SECRET-TOKEN',
|
||||
onAuthenticated() {
|
||||
t.is(provider.isAuthenticated, true)
|
||||
t.is(provider.authorizedScope, 'read-write')
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('executes the onAuthenticated callback when token is provided as a function that returns a promise', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }) {
|
||||
if (token !== 'SUPER-SECRET-TOKEN') {
|
||||
throw new Error()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
token: async () => Promise.resolve('SUPER-SECRET-TOKEN'),
|
||||
onAuthenticated() {
|
||||
t.is(provider.isAuthenticated, true)
|
||||
t.is(provider.authorizedScope, 'read-write')
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('executes the onAuthenticated callback when token is provided as a function that returns a string', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token }) {
|
||||
if (token !== 'SUPER-SECRET-TOKEN') {
|
||||
throw new Error()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
token: () => 'SUPER-SECRET-TOKEN',
|
||||
onAuthenticated() {
|
||||
t.is(provider.isAuthenticated, true)
|
||||
t.is(provider.authorizedScope, 'read-write')
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('sets correct scope for readonly', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token, connectionConfig }) {
|
||||
if (token !== 'SUPER-SECRET-TOKEN') {
|
||||
throw new Error()
|
||||
}
|
||||
connectionConfig.readOnly = true
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
token: 'SUPER-SECRET-TOKEN',
|
||||
onAuthenticated() {
|
||||
t.is(provider.isAuthenticated, true)
|
||||
t.is(provider.authorizedScope, 'readonly')
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
20
tests/provider/onAuthenticationFailed.ts
Normal file
20
tests/provider/onAuthenticationFailed.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('executes the onAuthenticationFailed callback', async t => {
|
||||
await new Promise(async resolve => {
|
||||
newHocuspocus(t, {
|
||||
async onAuthenticate({ token }) {
|
||||
throw new Error()
|
||||
},
|
||||
}).then(server => {
|
||||
newHocuspocusProvider(t, server, {
|
||||
token: 'SUPER-SECRET-TOKEN',
|
||||
onAuthenticationFailed() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
90
tests/provider/onAuthenticationFailedRetry.ts
Normal file
90
tests/provider/onAuthenticationFailedRetry.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import test from "ava";
|
||||
import {
|
||||
newHocuspocus,
|
||||
newHocuspocusProvider,
|
||||
newHocuspocusProviderWebsocket,
|
||||
sleep,
|
||||
} from "../utils/index.ts";
|
||||
|
||||
test("provider retries auth with token function after initial failure", async (t) => {
|
||||
const docName = "superSecretDoc";
|
||||
const requiredToken = "SUPER-SECRET-TOKEN";
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token, documentName }) {
|
||||
if (documentName !== docName) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (token !== requiredToken) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
let tokenCallCount = 0;
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
name: docName,
|
||||
token: () => {
|
||||
tokenCallCount++;
|
||||
return tokenCallCount === 1 ? "wrongToken" : requiredToken;
|
||||
},
|
||||
onAuthenticationFailed() {
|
||||
provider.sendToken();
|
||||
provider.startSync();
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
t.is(tokenCallCount, 2);
|
||||
t.is(provider.isAuthenticated, true);
|
||||
});
|
||||
|
||||
test("second provider with same doc name succeeds after first fails auth", async (t) => {
|
||||
const docName = "superSecretDoc";
|
||||
const requiredToken = "SUPER-SECRET-TOKEN";
|
||||
|
||||
const server = await newHocuspocus(t, {
|
||||
async onAuthenticate({ token, documentName }) {
|
||||
if (documentName !== docName) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (token !== requiredToken) {
|
||||
throw new Error();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const socket = newHocuspocusProviderWebsocket(t, server);
|
||||
|
||||
const providerFail = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: "wrongToken",
|
||||
name: docName,
|
||||
onAuthenticated() {
|
||||
t.fail("providerFail should not authenticate");
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
const providerOK = newHocuspocusProvider(t, server, {
|
||||
websocketProvider: socket,
|
||||
token: requiredToken,
|
||||
name: docName,
|
||||
onAuthenticationFailed() {
|
||||
t.fail("providerOK should not fail auth");
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
|
||||
t.is(providerFail.isAuthenticated, false);
|
||||
t.is(providerOK.isAuthenticated, true);
|
||||
});
|
||||
169
tests/provider/onAwarenessChange.ts
Normal file
169
tests/provider/onAwarenessChange.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import test from "ava";
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from "../utils/index.ts";
|
||||
|
||||
test("onAwarenessChange callback is executed", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let resolved = false;
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.setAwarenessField("foo", "bar");
|
||||
},
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
|
||||
t.is(states.length, 1);
|
||||
t.is(states[0].foo, "bar");
|
||||
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("onAwarenessChange callback is executed, even when no awareness fields are set", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let resolved = false;
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
|
||||
t.is(states.length, 2);
|
||||
|
||||
resolve("done");
|
||||
},
|
||||
});
|
||||
|
||||
const anotherProvider = newHocuspocusProvider(t, server, {
|
||||
async onConnect() {
|
||||
anotherProvider.setAwarenessField("foo", "bar");
|
||||
provider.configuration.websocketProvider.connect();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test("onAwarenessChange callback is executed on provider destroy", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let resolved = false;
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
const provider = newHocuspocusProvider(t,
|
||||
server,
|
||||
{
|
||||
onConnect() {
|
||||
provider.destroy();
|
||||
},
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
|
||||
t.is(states.length, 0);
|
||||
resolve("done");
|
||||
},
|
||||
},
|
||||
{
|
||||
maxAttempts: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("gets the current awareness states from the server", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let resolved = false;
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server);
|
||||
const provider2 = newHocuspocusProvider(t, server, {
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
const state = states.find((state) => state.foo === "bar");
|
||||
|
||||
if (state && state.foo === "bar") {
|
||||
resolved = true;
|
||||
t.pass();
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
provider.setAwarenessField("foo", "bar");
|
||||
});
|
||||
});
|
||||
|
||||
test("shares awareness state with other users", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
let resolved = false;
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.setAwarenessField("name", "player1");
|
||||
},
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
const player2 = !!states.filter((state) => state.name === "player2")
|
||||
.length;
|
||||
|
||||
if (player2) {
|
||||
resolved = true;
|
||||
t.is(player2, true);
|
||||
resolve("done");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const anotherProvider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
anotherProvider.setAwarenessField("name", "player2");
|
||||
},
|
||||
onAwarenessChange: ({ states }) => {
|
||||
if (resolved) return;
|
||||
const player1 = !!states.filter((state) => state.name === "player1")
|
||||
.length;
|
||||
|
||||
if (player1) {
|
||||
t.is(player1, true);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("does not share awareness state with users in other documents", async (t) => {
|
||||
await new Promise(async (resolve) => {
|
||||
const server = await newHocuspocus(t);
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
async onConnect() {
|
||||
await sleep(100);
|
||||
|
||||
t.pass();
|
||||
resolve("done");
|
||||
},
|
||||
onAwarenessChange: ({ states }) => {
|
||||
const player2 = !!states.filter((state) => state.name === "player2")
|
||||
.length;
|
||||
|
||||
if (player2) {
|
||||
t.fail("Awareness state leaked!");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const anotherProvider = newHocuspocusProvider(t, server, {
|
||||
name: "completely-different-and-unrelated-document",
|
||||
onConnect() {
|
||||
anotherProvider.setAwarenessField("name", "player2");
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
131
tests/provider/onAwarenessUpdate.ts
Normal file
131
tests/provider/onAwarenessUpdate.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import test from 'ava'
|
||||
import { AwarenessError } from '@hocuspocus/provider'
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts'
|
||||
|
||||
test('onAwarenessUpdate callback is executed', async t => {
|
||||
await new Promise(async resolve => {
|
||||
let resolved = false
|
||||
const server = await newHocuspocus(t, { })
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.setAwarenessField('foo', 'bar')
|
||||
},
|
||||
onAwarenessUpdate: ({ states }) => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
|
||||
t.is(states.length, 1)
|
||||
t.is(states[0].foo, 'bar')
|
||||
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('shares awareness state with other users', async t => {
|
||||
await new Promise(async resolve => {
|
||||
let resolved = false
|
||||
const server = await newHocuspocus(t, { })
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.setAwarenessField('name', 'player1')
|
||||
},
|
||||
onAwarenessUpdate: ({ states }) => {
|
||||
if (resolved) return
|
||||
const player2 = !!states.filter(state => state.name === 'player2').length
|
||||
|
||||
if (player2) {
|
||||
resolved = true
|
||||
t.is(player2, true)
|
||||
resolve('done')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const anotherProvider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
anotherProvider.setAwarenessField('name', 'player2')
|
||||
},
|
||||
onAwarenessUpdate: ({ states }) => {
|
||||
if (resolved) return
|
||||
const player1 = !!states.filter(state => state.name === 'player1').length
|
||||
|
||||
if (player1) {
|
||||
t.is(player1, true)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('does not share awareness state with users in other documents', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, { })
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
async onConnect() {
|
||||
await sleep(100)
|
||||
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
onAwarenessUpdate: ({ states }) => {
|
||||
const player2 = !!states.filter(state => state.name === 'player2').length
|
||||
|
||||
if (player2) {
|
||||
throw new Error('Awareness state leaked!')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const anotherProvider = newHocuspocusProvider(t, server, {
|
||||
name: 'hocuspocus-completely-different-and-unrelated-document',
|
||||
onConnect() {
|
||||
anotherProvider.setAwarenessField('name', 'player2')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('allows awareness to be null', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, { })
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
async onConnect() {
|
||||
await sleep(100)
|
||||
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('throws an error in setAwarenessFields if awareness is null', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
awareness: null,
|
||||
onConnect() {
|
||||
try {
|
||||
provider.setAwarenessField('foo', 'bar')
|
||||
t.fail()
|
||||
} catch (err: any) {
|
||||
if (err instanceof AwarenessError) {
|
||||
t.pass()
|
||||
} else {
|
||||
t.fail()
|
||||
}
|
||||
} finally {
|
||||
resolve('done')
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
35
tests/provider/onClose.ts
Normal file
35
tests/provider/onClose.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('onClose callback is executed', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.configuration.websocketProvider.disconnect()
|
||||
},
|
||||
onClose() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("on('close') callback is executed", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('connect', () => {
|
||||
provider.configuration.websocketProvider.disconnect()
|
||||
})
|
||||
|
||||
provider.on('close', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
28
tests/provider/onConnect.ts
Normal file
28
tests/provider/onConnect.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('executes the onConnect callback', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("executes the on('connect') callback", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('connect', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
36
tests/provider/onDisconnect.ts
Normal file
36
tests/provider/onDisconnect.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('onDisconnect callback is executed', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onConnect() {
|
||||
provider.configuration.websocketProvider.disconnect()
|
||||
provider.disconnect()
|
||||
},
|
||||
onDisconnect() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("on('disconnect') callback is executed", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('connect', () => {
|
||||
provider.configuration.websocketProvider.disconnect()
|
||||
provider.disconnect()
|
||||
})
|
||||
provider.on('disconnect', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
28
tests/provider/onMessage.ts
Normal file
28
tests/provider/onMessage.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('executes the onMessage callback', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, { })
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
onMessage() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("executes the on('message') callback", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('message', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
28
tests/provider/onOpen.ts
Normal file
28
tests/provider/onOpen.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('onOpen callback is executed', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
onOpen() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("on('open') callback is executed", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('open', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
21
tests/provider/onStateless.ts
Normal file
21
tests/provider/onStateless.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider } from '../utils/index.ts'
|
||||
|
||||
test('executes the onStateless callback', async t => {
|
||||
const payloadToSend = 'STATELESS-MESSAGE'
|
||||
await new Promise(async resolve => {
|
||||
newHocuspocus(t, {
|
||||
async onStateless({ payload }) {
|
||||
t.is(payload, payloadToSend)
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
}).then(server => {
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onSynced: () => {
|
||||
provider.sendStateless(payloadToSend)
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
109
tests/provider/onSynced.ts
Normal file
109
tests/provider/onSynced.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import test from 'ava'
|
||||
import { newHocuspocus, newHocuspocusProvider, sleep } from '../utils/index.ts'
|
||||
|
||||
test('onSynced callback is executed', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
onSynced() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("on('synced') callback is executed", async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t)
|
||||
|
||||
const provider = newHocuspocusProvider(t, server)
|
||||
|
||||
provider.on('synced', () => {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('onSynced callback is executed, even when the onConnect takes longer', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onConnect(data) {
|
||||
await sleep(100)
|
||||
},
|
||||
})
|
||||
|
||||
newHocuspocusProvider(t, server, {
|
||||
onSynced() {
|
||||
t.pass()
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('onSynced callback is executed when the document is actually synced', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onLoadDocument({ document }) {
|
||||
document.getArray('foo').insert(0, ['bar'])
|
||||
|
||||
return document
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onSynced() {
|
||||
const value = provider.document.getArray('foo').get(0)
|
||||
t.is(value, 'bar')
|
||||
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('send all messages according to the protocol', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onLoadDocument({ document }) {
|
||||
document.getArray('foo').insert(0, ['bar'])
|
||||
|
||||
return document
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
async onSynced() {
|
||||
t.deepEqual(provider.document.getArray('foo').get(0), 'bar')
|
||||
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('onSynced callback is executed when the document is actually synced, even if it takes longer', async t => {
|
||||
await new Promise(async resolve => {
|
||||
const server = await newHocuspocus(t, {
|
||||
async onLoadDocument({ document }) {
|
||||
await sleep(100)
|
||||
|
||||
document.getArray('foo').insert(0, ['bar'])
|
||||
|
||||
return document
|
||||
},
|
||||
})
|
||||
|
||||
const provider = newHocuspocusProvider(t, server, {
|
||||
onSynced() {
|
||||
const value = provider.document.getArray('foo').get(0)
|
||||
t.is(value, 'bar')
|
||||
|
||||
resolve('done')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user