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>
127 lines
5.5 KiB
TypeScript
127 lines
5.5 KiB
TypeScript
// The others in the project (plan §7.4): another editor's state read with
|
|
// care, the others by name without this editor, each person once however
|
|
// many tabs they have, their choices as frames and their windows as dots,
|
|
// a colour for whoever the dashboard gave none, initials; and this editor's
|
|
// state told only when it changed — nothing that changes without a cause.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { initialsOf } from '../src/remote/PresenceMarks.js';
|
|
import { colorOf, peersOf, peopleOf, PRESENCE_COLORS, readPresence, selectionsOf, watchersOf, windowKey, type PresenceState } from '../src/remote/presence.js';
|
|
import { presencePublisher } from '../src/remote/usePresence.js';
|
|
|
|
const state = (id: string, name: string, selection: string | null = null, windows: string[] = [], color = '#112233'): PresenceState => ({ v: 1, user: { id, name, color }, selection, windows });
|
|
|
|
describe('the others in the project', () => {
|
|
it('reads a state of the shape it writes, and leaves out any other', () => {
|
|
expect(readPresence(state('u', 'Mia', 'd', ['desktop:d']))).toEqual(state('u', 'Mia', 'd', ['desktop:d']));
|
|
for (const bad of [
|
|
null,
|
|
'Mia',
|
|
{ ...state('u', 'Mia'), v: 2 },
|
|
{ ...state('u', 'Mia'), user: null },
|
|
{ ...state('', 'Mia') },
|
|
{ ...state('u', 7 as unknown as string) },
|
|
{ ...state('u', 'x'.repeat(201)) },
|
|
{ ...state('u', 'Mia'), user: { id: 'u', name: 'Mia', color: 'c'.repeat(33) } },
|
|
{ ...state('u', 'Mia'), selection: 5 },
|
|
{ ...state('u', 'Mia'), windows: 'desktop:d' },
|
|
{ ...state('u', 'Mia'), windows: [7] },
|
|
{ ...state('u', 'Mia'), windows: Array.from({ length: 101 }, () => 'w') },
|
|
])
|
|
expect(readPresence(bad)).toBeUndefined();
|
|
});
|
|
|
|
it('lists the other editors by name, without this one and without what it cannot read', () => {
|
|
const states = new Map<number, unknown>([
|
|
[1, state('me', 'Zoe')],
|
|
[2, state('b', 'Ben')],
|
|
[3, { cursor: { x: 1 } }],
|
|
[4, state('a', 'Ada')],
|
|
[5, state('b', 'Ben')],
|
|
]);
|
|
expect(peersOf(states, 1).map((peer) => [peer.clientId, peer.state.user.name])).toEqual([
|
|
[4, 'Ada'],
|
|
[2, 'Ben'],
|
|
[5, 'Ben'],
|
|
]);
|
|
});
|
|
|
|
it('counts a person with two tabs once, and leaves out this person in another tab', () => {
|
|
const peers = peersOf(
|
|
new Map<number, unknown>([
|
|
[2, state('b', 'Ben')],
|
|
[3, state('b', 'Ben')],
|
|
[4, state('me', 'Zoe')],
|
|
]),
|
|
1,
|
|
);
|
|
expect(peopleOf(peers, 'me')).toEqual([{ id: 'b', name: 'Ben', color: '#112233' }]);
|
|
expect(peopleOf(peers).map((user) => user.id)).toEqual(['b', 'me']);
|
|
});
|
|
|
|
it('gives each chosen device the colours of who chose it, each person once', () => {
|
|
const peers = peersOf(
|
|
new Map<number, unknown>([
|
|
[2, state('b', 'Ben', 'router', [], '#aa0000')],
|
|
[3, state('b', 'Ben', 'router', [], '#aa0000')],
|
|
[4, state('a', 'Ada', 'router', [], '#00aa00')],
|
|
[5, state('c', 'Cem', 'pc', [], '#0000aa')],
|
|
[6, state('d', 'Dan', null)],
|
|
]),
|
|
1,
|
|
);
|
|
expect(selectionsOf(peers)).toEqual(
|
|
new Map([
|
|
['router', ['#00aa00', '#aa0000']],
|
|
['pc', ['#0000aa']],
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('gives each open window who has it open, each person once', () => {
|
|
const desktop = windowKey('desktop', 'pc');
|
|
const peers = peersOf(
|
|
new Map<number, unknown>([
|
|
[2, state('b', 'Ben', null, [desktop, 'capture'])],
|
|
[3, state('b', 'Ben', null, [desktop])],
|
|
[4, state('a', 'Ada', null, [windowKey('sat', 's')])],
|
|
]),
|
|
1,
|
|
);
|
|
const watchers = watchersOf(peers);
|
|
expect([...watchers.keys()].sort()).toEqual(['capture', 'desktop:pc', 'sat:s']);
|
|
expect(watchers.get(desktop)!.map((user) => user.id)).toEqual(['b']);
|
|
expect([windowKey('nat', 'g'), windowKey('capture', 'ignored')]).toEqual(['nat:g', 'capture']);
|
|
});
|
|
|
|
it("takes the dashboard's colour, and gives one by the id to whoever has none", () => {
|
|
expect(colorOf({ id: 'x', name: 'X', color: '#ABCDEF' })).toBe('#ABCDEF');
|
|
const given = colorOf({ id: 'person-1', name: 'X', color: '' });
|
|
expect(PRESENCE_COLORS).toContain(given);
|
|
expect(colorOf({ id: 'person-1', name: 'Y', color: 'blue' })).toBe(given);
|
|
// Every editor gives the same colour, whatever its version: pinned.
|
|
expect([colorOf({ id: '0b8f5f2e-1a3c-4d5e-8f90-123456789abc', name: '', color: '' }), colorOf({ id: 'person-2', name: '', color: '' })]).toEqual(['#e67700', '#1971c2']);
|
|
const all = new Set(Array.from({ length: 40 }, (_, index) => colorOf({ id: `p${index}`, name: '', color: '' })));
|
|
expect(all.size).toBeGreaterThan(4);
|
|
});
|
|
|
|
it('writes the initials of the first and the last word', () => {
|
|
expect([initialsOf('Ada Beispiel'), initialsOf('mia'), initialsOf('Jörg Ömer Weiß'), initialsOf(' '), initialsOf('🐧 Pinguin')]).toEqual(['AB', 'M', 'JW', '?', '🐧P']);
|
|
});
|
|
|
|
it("tells this editor's state only when it changed", () => {
|
|
const set: unknown[] = [];
|
|
const publish = presencePublisher({ setLocalState: (value) => void set.push(value) }, { id: 'me', name: 'Zoe', color: '' });
|
|
publish(null, []);
|
|
publish(null, []);
|
|
publish('pc', []);
|
|
publish('pc', []);
|
|
publish('pc', ['desktop:pc']);
|
|
expect(set).toEqual([
|
|
{ v: 1, user: { id: 'me', name: 'Zoe', color: '' }, selection: null, windows: [] },
|
|
{ v: 1, user: { id: 'me', name: 'Zoe', color: '' }, selection: 'pc', windows: [] },
|
|
{ v: 1, user: { id: 'me', name: 'Zoe', color: '' }, selection: 'pc', windows: ['desktop:pc'] },
|
|
]);
|
|
});
|
|
});
|