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

This commit is contained in:
upstream-mirror
2026-09-15 17:21:35 +05:00
commit e0c588ca1c
265 changed files with 55259 additions and 0 deletions

View 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))
})
})

View 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')
},
})
})
})

View 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
View 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)
})
})

View 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)
})
})

View 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')
},
})
})
})

View 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')
},
})
})
})
})

View 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);
});

View 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");
},
});
});
});

View 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
View 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')
})
})
})

View 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')
})
})
})

View 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')
})
})
})

View 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
View 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')
})
})
})

View 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
View 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')
},
})
})
})