import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createStore } from 'jotai'; import { enableMapSet } from 'immer'; import { roomToUnreadAtom, unreadInfoToUnread, unreadEqual } from './roomToUnread'; import { roomToParentsAtom } from './roomToParents'; import { RoomToParents, Unread, UnreadInfo } from '../../../types/matrix/room'; // `roomToUnreadAtom` maps roomId -> Unread { total, highlight, from }. The // reducer handles RESET (rebuild from a list of UnreadInfo), PUT (set one room's // unread + roll the delta up to all ancestor parents, accumulating their `from` // sets) and DELETE (remove a room's unread + roll the removal back up to // parents, pruning a parent whose `from` set becomes empty). Parent aggregation // reads `roomToParentsAtom`, which we seed directly in the store. // // The reducers `produce` over immer-managed Map/Set, so we enable that the same // way the app does at startup (src/index.tsx). The React // `useBindRoomToUnreadAtom` hook wiring (timeline/receipt/membership listeners, // getUnreadInfos) is not covered here. enableMapSet(); const get = (store: ReturnType) => store.get(roomToUnreadAtom); const info = (roomId: string, total: number, highlight: number): UnreadInfo => ({ roomId, total, highlight, }); const seedParents = (store: ReturnType, map: RoomToParents) => store.set(roomToParentsAtom, { type: 'INITIALIZE', roomToParents: map }); // --------------------------------------------------------------------------- // unreadInfoToUnread // --------------------------------------------------------------------------- test('unreadInfoToUnread copies counts and nulls `from`', () => { assert.deepEqual(unreadInfoToUnread(info('!r:s', 5, 2)), { total: 5, highlight: 2, from: null, }); }); // --------------------------------------------------------------------------- // unreadEqual // --------------------------------------------------------------------------- const u = (total: number, highlight: number, from: Set | null): Unread => ({ total, highlight, from, }); test('unreadEqual: equal counts with both `from` null are equal', () => { assert.equal(unreadEqual(u(1, 0, null), u(1, 0, null)), true); }); test('unreadEqual: differing total is not equal', () => { assert.equal(unreadEqual(u(1, 0, null), u(2, 0, null)), false); }); test('unreadEqual: differing highlight is not equal', () => { assert.equal(unreadEqual(u(1, 0, null), u(1, 1, null)), false); }); test('unreadEqual: one `from` null and the other a set is not equal', () => { assert.equal(unreadEqual(u(1, 0, null), u(1, 0, new Set(['!a:s']))), false); assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, null)), false); }); test('unreadEqual: `from` sets of different size are not equal', () => { assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, new Set(['!a:s', '!b:s']))), false); }); test('unreadEqual: same-size `from` sets with same members are equal', () => { assert.equal( unreadEqual(u(2, 1, new Set(['!a:s', '!b:s'])), u(2, 1, new Set(['!b:s', '!a:s']))), true, ); }); test('unreadEqual: same-size `from` sets with different members are not equal', () => { assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, new Set(['!b:s']))), false); }); // --------------------------------------------------------------------------- // roomToUnreadAtom: PUT (no parents) // --------------------------------------------------------------------------- test('starts empty', () => { const store = createStore(); assert.equal(get(store).size, 0); }); test('PUT sets a room unread with null `from`', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); assert.deepEqual(get(store).get('!r:s'), { total: 3, highlight: 1, from: null }); }); test('PUT overwrites a room unread with the latest counts', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 7, 2) }); assert.deepEqual(get(store).get('!r:s'), { total: 7, highlight: 2, from: null }); }); test('PUT with unchanged counts is skipped (same map reference)', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); const before = get(store); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); const after = get(store); // The "skip update if equal" guard returns without producing a new map. assert.equal(before, after); }); // --------------------------------------------------------------------------- // roomToUnreadAtom: PUT with parent aggregation // --------------------------------------------------------------------------- test('PUT rolls counts up to a single parent and records `from`', () => { const store = createStore(); seedParents(store, new Map([['!c:s', new Set(['!space:s'])]])); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) }); assert.deepEqual(get(store).get('!c:s'), { total: 4, highlight: 2, from: null }); const parent = get(store).get('!space:s'); assert.equal(parent?.total, 4); assert.equal(parent?.highlight, 2); assert.deepEqual(Array.from(parent?.from ?? []), ['!c:s']); }); test('PUT aggregates two children into the same parent', () => { const store = createStore(); seedParents( store, new Map([ ['!c1:s', new Set(['!space:s'])], ['!c2:s', new Set(['!space:s'])], ]), ); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c1:s', 4, 2) }); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c2:s', 1, 1) }); const parent = get(store).get('!space:s'); assert.equal(parent?.total, 5); assert.equal(parent?.highlight, 3); assert.deepEqual(Array.from(parent?.from ?? []).sort(), ['!c1:s', '!c2:s']); }); test('PUT applies only the delta to the parent on re-PUT of the same child', () => { const store = createStore(); seedParents(store, new Map([['!c:s', new Set(['!space:s'])]])); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) }); // Increase the child's counts; the parent should reflect the delta, not 4+6. store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 6, 3) }); const parent = get(store).get('!space:s'); assert.equal(parent?.total, 6); assert.equal(parent?.highlight, 3); assert.deepEqual(Array.from(parent?.from ?? []), ['!c:s']); }); test('PUT rolls up through a grandparent chain', () => { const store = createStore(); seedParents( store, new Map([ ['!c:s', new Set(['!mid:s'])], ['!mid:s', new Set(['!top:s'])], ]), ); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 2, 1) }); // Both !mid and !top are ancestors of !c. for (const ancestor of ['!mid:s', '!top:s']) { const a = get(store).get(ancestor); assert.equal(a?.total, 2, ancestor); assert.equal(a?.highlight, 1, ancestor); assert.deepEqual(Array.from(a?.from ?? []), ['!c:s'], ancestor); } }); // --------------------------------------------------------------------------- // roomToUnreadAtom: RESET // --------------------------------------------------------------------------- test('RESET rebuilds the map from scratch', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!old:s', 9, 9) }); store.set(roomToUnreadAtom, { type: 'RESET', unreadInfos: [info('!a:s', 2, 0), info('!b:s', 3, 1)], }); assert.equal(get(store).has('!old:s'), false); assert.deepEqual(get(store).get('!a:s'), { total: 2, highlight: 0, from: null }); assert.deepEqual(get(store).get('!b:s'), { total: 3, highlight: 1, from: null }); }); test('RESET aggregates parents from the seeded roomToParents', () => { const store = createStore(); seedParents( store, new Map([ ['!a:s', new Set(['!space:s'])], ['!b:s', new Set(['!space:s'])], ]), ); store.set(roomToUnreadAtom, { type: 'RESET', unreadInfos: [info('!a:s', 2, 1), info('!b:s', 3, 0)], }); const parent = get(store).get('!space:s'); assert.equal(parent?.total, 5); assert.equal(parent?.highlight, 1); assert.deepEqual(Array.from(parent?.from ?? []).sort(), ['!a:s', '!b:s']); }); // --------------------------------------------------------------------------- // roomToUnreadAtom: DELETE // --------------------------------------------------------------------------- test('DELETE removes a leaf room with no parents', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!r:s' }); assert.equal(get(store).has('!r:s'), false); }); test('DELETE of an absent room is a no-op (same map reference)', () => { const store = createStore(); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) }); const before = get(store); store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!absent:s' }); assert.equal(get(store), before); }); test('DELETE subtracts the child counts from a parent that keeps other children', () => { const store = createStore(); seedParents( store, new Map([ ['!c1:s', new Set(['!space:s'])], ['!c2:s', new Set(['!space:s'])], ]), ); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c1:s', 4, 2) }); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c2:s', 1, 1) }); store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!c1:s' }); assert.equal(get(store).has('!c1:s'), false); const parent = get(store).get('!space:s'); assert.equal(parent?.total, 1); assert.equal(parent?.highlight, 1); assert.deepEqual(Array.from(parent?.from ?? []), ['!c2:s']); }); test('DELETE prunes a parent whose `from` set becomes empty', () => { const store = createStore(); seedParents(store, new Map([['!c:s', new Set(['!space:s'])]])); store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) }); store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!c:s' }); assert.equal(get(store).has('!c:s'), false); // The parent had only !c contributing, so it is removed entirely. assert.equal(get(store).has('!space:s'), false); assert.equal(get(store).size, 0); });