feat: F5.4 presence: who else is in the project
Over Yjs awareness each editor tells who it is (F names the person in the access message), the device it has chosen and the windows it has open, and nothing else: no pointer, nothing that changes without a cause, sent only when it changed. Others' states are read with care. A frame in their colour round a device they chose, a dot on a window they have open too, a circle with initials each beside the mode buttons; the bridge tells the dashboard who is there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -319,11 +319,15 @@ describe("the dashboard's service", () => {
|
||||
it('freezes a project: whoever writes in it reads from then on and is told, and writes again when it is thawed', async () => {
|
||||
const server = await start();
|
||||
const project = await newProject();
|
||||
const writer = await enter(server, project, { level: 'write' });
|
||||
const reader = await enter(server, project, { level: 'read' });
|
||||
// Each is told its access as it comes in.
|
||||
const [writerId, readerId] = [randomUUID(), randomUUID()];
|
||||
const writer = await enter(server, project, { level: 'write', user: writerId, name: 'Mia' });
|
||||
const reader = await enter(server, project, { level: 'read', user: readerId, name: 'Rita' });
|
||||
// Each is told its access as it comes in, and who it is (with the colour of the ticket).
|
||||
await until(() => writer.messages.length === 1 && reader.messages.length === 1);
|
||||
expect([writer.messages, reader.messages]).toEqual([[{ v: 1, kind: 'access', readOnly: false, frozen: false }], [{ v: 1, kind: 'access', readOnly: true, frozen: false }]]);
|
||||
expect([writer.messages, reader.messages]).toEqual([
|
||||
[{ v: 1, kind: 'access', readOnly: false, frozen: false, you: { user: writerId, name: 'Mia', color: '#3366cc' } }],
|
||||
[{ v: 1, kind: 'access', readOnly: true, frozen: false, you: { user: readerId, name: 'Rita', color: '#3366cc' } }],
|
||||
]);
|
||||
expect(await answer(await service(server, 'POST', `/svc/projects/${project}/freeze`, { frozen: true }))).toEqual([200, { frozen: true }]);
|
||||
await until(() => writer.messages.length === 2);
|
||||
expect(writer.messages[1]).toEqual({ v: 1, kind: 'access', readOnly: true, frozen: true });
|
||||
@@ -331,7 +335,7 @@ describe("the dashboard's service", () => {
|
||||
const late = await enter(server, project, { level: 'write' });
|
||||
expect(late.provider.authorizedScope).toBe('readonly');
|
||||
await until(() => late.messages.length === 1);
|
||||
expect(late.messages).toEqual([{ v: 1, kind: 'access', readOnly: true, frozen: true }]);
|
||||
expect(late.messages).toEqual([{ v: 1, kind: 'access', readOnly: true, frozen: true, you: expect.objectContaining({ name: 'Ada Beispiel' }) }]);
|
||||
place(late.doc, 'a');
|
||||
await sleep(300);
|
||||
expect(await store.pending(project)).toBe(0);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import WebSocket from 'ws';
|
||||
import { addDevice } from '@mdg-filius/doc';
|
||||
import { peersOf } from '../../editor/src/remote/presence.js';
|
||||
import { connectSharedDoc, type SharedDocState } from '../../editor/src/remote/sharedDoc.js';
|
||||
import { PostgresSessions, PostgresStore } from '../src/index.js';
|
||||
import { testDatabase, type TestDatabase } from './database.js';
|
||||
@@ -68,6 +69,29 @@ describe("the editor's shared document", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('knows who it is, from F, and sees the others through the awareness', async () => {
|
||||
const server = await startTestServer(store, sessions);
|
||||
const project = await newProject(store);
|
||||
const [miaId, benId] = [randomUUID(), randomUUID()];
|
||||
const mia = connectEditor(server, project, await signIn(server, project, { user: miaId, name: 'Mia' }));
|
||||
const ben = connectEditor(server, project, await signIn(server, project, { user: benId, name: 'Ben', level: 'read' }));
|
||||
await mia.ready();
|
||||
await ben.ready();
|
||||
const miaReady = mia.last() as Extract<SharedDocState, { kind: 'ready' }>;
|
||||
const benReady = ben.last() as Extract<SharedDocState, { kind: 'ready' }>;
|
||||
expect(miaReady.you).toEqual({ id: miaId, name: 'Mia', color: '#3366cc' });
|
||||
expect(benReady.you).toMatchObject({ id: benId, name: 'Ben' });
|
||||
// A reader is seen too: the awareness is not the document.
|
||||
benReady.awareness.setLocalState({ v: 1, user: benReady.you, selection: 'router', windows: [] });
|
||||
await until(() => peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID).length === 1);
|
||||
expect(peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID)[0]!.state).toEqual({ v: 1, user: benReady.you, selection: 'router', windows: [] });
|
||||
// Gone, he is gone for her too.
|
||||
ben.close();
|
||||
await until(() => peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID).length === 0);
|
||||
mia.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('reads while the project is frozen and gets the document anew when it is thawed', async () => {
|
||||
const server = await startTestServer(store, sessions);
|
||||
const project = await newProject(store);
|
||||
|
||||
Reference in New Issue
Block a user