test: lotus decorations, call caps, crypto, featureCheck, typing, markdown (+34)
Subagent batch (no bugs found) + markdown: - lotus/avatarDecorations (8): decorationUrl, CDN shape, ALL_DECORATIONS flattening, data invariants (unique category ids + slugs, slug charset). - plugins/call/utils (7): getCallCapabilities — static caps + room/user/device scoped state-keys. - utils/matrix-crypto (3): verifiedDevice via a stubbed CryptoApi. - utils/featureCheck (3): checkIndexedDBSupport success/error/throw paths. - state/typingMembers (8): add/dedup-by-latest-ts/per-room-scope/delete reducer via a jotai store (enableMapSet, mirroring app startup). - plugins/markdown/utils (5): inline + block escape/unescape round-trips. Full suite now 231 tests, all passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { enableMapSet } from 'immer';
|
||||
import {
|
||||
TYPING_TIMEOUT_MS,
|
||||
roomIdToTypingMembersAtom,
|
||||
type IRoomIdToTypingMembers,
|
||||
} from './typingMembers';
|
||||
|
||||
// The add/remove/dedup/filter logic lives in the unexported putTypingMember /
|
||||
// deleteTypingMember reducers, reachable only through `roomIdToTypingMembersAtom`.
|
||||
// We exercise it via a jotai store (the pure reducer path). The PUT branch also
|
||||
// schedules a real setTimeout for auto-expiry; that timer-driven cleanup and the
|
||||
// React `useBindRoomIdToTypingMembersAtom` hook wiring are not covered here.
|
||||
// The reducers `produce` over an immer-managed Map. The app turns this on once
|
||||
// at startup (src/index.tsx calls enableMapSet()); we replicate that here.
|
||||
enableMapSet();
|
||||
|
||||
const ROOM = '!room:server';
|
||||
const A = '@alice:server';
|
||||
const B = '@bob:server';
|
||||
|
||||
const members = (store: ReturnType<typeof createStore>): IRoomIdToTypingMembers =>
|
||||
store.get(roomIdToTypingMembersAtom);
|
||||
|
||||
const userIds = (store: ReturnType<typeof createStore>, roomId: string): string[] =>
|
||||
(members(store).get(roomId) ?? []).map((r) => r.userId);
|
||||
|
||||
test('TYPING_TIMEOUT_MS is 5 seconds', () => {
|
||||
assert.equal(TYPING_TIMEOUT_MS, 5000);
|
||||
});
|
||||
|
||||
test('starts empty', () => {
|
||||
const store = createStore();
|
||||
assert.equal(members(store).size, 0);
|
||||
});
|
||||
|
||||
test('PUT adds a typing member to a room', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
|
||||
assert.deepEqual(userIds(store, ROOM), [A]);
|
||||
});
|
||||
|
||||
test('PUT dedups by userId, keeping the latest ts', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 99 });
|
||||
|
||||
const receipts = members(store).get(ROOM) ?? [];
|
||||
assert.equal(receipts.length, 1);
|
||||
assert.equal(receipts[0].userId, A);
|
||||
assert.equal(receipts[0].ts, 99);
|
||||
});
|
||||
|
||||
test('PUT keeps distinct users side by side', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });
|
||||
assert.deepEqual(userIds(store, ROOM).sort(), [A, B].sort());
|
||||
});
|
||||
|
||||
test('typing members are scoped per room', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r1:s', userId: A, ts: 1 });
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r2:s', userId: B, ts: 1 });
|
||||
assert.deepEqual(userIds(store, '!r1:s'), [A]);
|
||||
assert.deepEqual(userIds(store, '!r2:s'), [B]);
|
||||
});
|
||||
|
||||
test('DELETE removes a user and drops the room when it becomes empty', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });
|
||||
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
|
||||
assert.deepEqual(userIds(store, ROOM), [B]);
|
||||
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
|
||||
assert.equal(members(store).has(ROOM), false);
|
||||
});
|
||||
|
||||
test('DELETE of an absent user is a no-op (no empty room created)', () => {
|
||||
const store = createStore();
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
|
||||
assert.equal(members(store).has(ROOM), false);
|
||||
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
|
||||
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
|
||||
assert.deepEqual(userIds(store, ROOM), [A]);
|
||||
});
|
||||
Reference in New Issue
Block a user