test: add suites for list, roomToParents, roomToUnread reducers (+43)
Via subagent, verified against real behavior (all use jotai store + enableMapSet): - state/list (11): createListAtom PUT/DELETE/REPLACE (single + array, identity). - state/room/roomToParents (10): INITIALIZE/PUT/DELETE incl. cycle-skip and orphan-cleanup pruning of zero-parent children. - state/room/roomToUnread (22): unreadInfoToUnread, unreadEqual, and the roomToUnreadAtom reducer — leaf/overwrite/equal-guard, multi-level parent roll-up with `from` recording, RESET rebuild, DELETE subtract/prune. No bugs (noted a latent never-hit string-spread in deleteUnreadInfo's `from ?? roomId` fallback; left as-is). Suite growing toward full pure-logic coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { createListAtom } from './list';
|
||||
|
||||
// `createListAtom` produces a write atom whose reducer handles PUT (append a
|
||||
// single item or an array of items), DELETE (identity-based removal of a single
|
||||
// item or array of items) and REPLACE (identity match -> replacement). We drive
|
||||
// the pure reducer through a real jotai store. The exported `TListAtom` type is
|
||||
// a type alias only, nothing to test at runtime.
|
||||
|
||||
test('starts as an empty array', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
assert.deepEqual(store.get(listAtom), []);
|
||||
});
|
||||
|
||||
test('PUT appends a single item', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: 'a' });
|
||||
store.set(listAtom, { type: 'PUT', item: 'b' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('PUT appends an array of items in order', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: 'a' });
|
||||
store.set(listAtom, { type: 'PUT', item: ['b', 'c'] });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
test('PUT does not deduplicate', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: 'a' });
|
||||
store.set(listAtom, { type: 'PUT', item: 'a' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'a']);
|
||||
});
|
||||
|
||||
test('DELETE removes a single item by identity', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c'] });
|
||||
store.set(listAtom, { type: 'DELETE', item: 'b' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'c']);
|
||||
});
|
||||
|
||||
test('DELETE removes an array of items', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c', 'd'] });
|
||||
store.set(listAtom, { type: 'DELETE', item: ['a', 'c'] });
|
||||
assert.deepEqual(store.get(listAtom), ['b', 'd']);
|
||||
});
|
||||
|
||||
test('DELETE of an absent item is a no-op', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b'] });
|
||||
store.set(listAtom, { type: 'DELETE', item: 'z' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('DELETE uses reference identity for object items', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<{ id: number }>();
|
||||
const a = { id: 1 };
|
||||
const b = { id: 2 };
|
||||
store.set(listAtom, { type: 'PUT', item: [a, b] });
|
||||
|
||||
// A structurally-equal but distinct object is not removed.
|
||||
store.set(listAtom, { type: 'DELETE', item: { id: 1 } });
|
||||
assert.deepEqual(store.get(listAtom), [a, b]);
|
||||
|
||||
// The same reference is removed.
|
||||
store.set(listAtom, { type: 'DELETE', item: a });
|
||||
assert.deepEqual(store.get(listAtom), [b]);
|
||||
});
|
||||
|
||||
test('REPLACE swaps the matching item, preserving position', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c'] });
|
||||
store.set(listAtom, { type: 'REPLACE', item: 'b', replacement: 'B' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'B', 'c']);
|
||||
});
|
||||
|
||||
test('REPLACE matches by identity and replaces every match', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'a'] });
|
||||
store.set(listAtom, { type: 'REPLACE', item: 'a', replacement: 'X' });
|
||||
assert.deepEqual(store.get(listAtom), ['X', 'b', 'X']);
|
||||
});
|
||||
|
||||
test('REPLACE with no match leaves the list unchanged', () => {
|
||||
const store = createStore();
|
||||
const listAtom = createListAtom<string>();
|
||||
store.set(listAtom, { type: 'PUT', item: ['a', 'b'] });
|
||||
store.set(listAtom, { type: 'REPLACE', item: 'z', replacement: 'Z' });
|
||||
assert.deepEqual(store.get(listAtom), ['a', 'b']);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { enableMapSet } from 'immer';
|
||||
import { roomToParentsAtom } from './roomToParents';
|
||||
import { RoomToParents } from '../../../types/matrix/room';
|
||||
|
||||
// `roomToParentsAtom` maps childRoomId -> Set<parentRoomId>. The reducer handles
|
||||
// INITIALIZE (replace the whole map), PUT (mapParentWithChildren: register a
|
||||
// parent for each given child, skipping cycles) and DELETE (drop the room as a
|
||||
// parent map entry, strip it from every child's parent-set, then prune children
|
||||
// left with zero parents). The reducers `produce` over an immer-managed Map/Set,
|
||||
// so we enable that the same way the app does at startup (src/index.tsx).
|
||||
// The React `useBindRoomToParentsAtom` hook wiring is not covered here.
|
||||
enableMapSet();
|
||||
|
||||
const get = (store: ReturnType<typeof createStore>): RoomToParents => store.get(roomToParentsAtom);
|
||||
|
||||
const parentsOf = (store: ReturnType<typeof createStore>, child: string): string[] =>
|
||||
Array.from(get(store).get(child) ?? []).sort();
|
||||
|
||||
test('starts as an empty map', () => {
|
||||
const store = createStore();
|
||||
assert.equal(get(store).size, 0);
|
||||
});
|
||||
|
||||
test('INITIALIZE replaces the whole map', () => {
|
||||
const store = createStore();
|
||||
const seed: RoomToParents = new Map([['!child:s', new Set(['!space:s'])]]);
|
||||
store.set(roomToParentsAtom, { type: 'INITIALIZE', roomToParents: seed });
|
||||
assert.equal(get(store), seed);
|
||||
assert.deepEqual(parentsOf(store, '!child:s'), ['!space:s']);
|
||||
});
|
||||
|
||||
test('PUT registers a parent for each child', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, {
|
||||
type: 'PUT',
|
||||
parent: '!space:s',
|
||||
children: ['!c1:s', '!c2:s'],
|
||||
});
|
||||
assert.deepEqual(parentsOf(store, '!c1:s'), ['!space:s']);
|
||||
assert.deepEqual(parentsOf(store, '!c2:s'), ['!space:s']);
|
||||
});
|
||||
|
||||
test('PUT accumulates multiple parents for the same child', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p1:s', children: ['!c:s'] });
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p2:s', children: ['!c:s'] });
|
||||
assert.deepEqual(parentsOf(store, '!c:s'), ['!p1:s', '!p2:s']);
|
||||
});
|
||||
|
||||
test('PUT skips a child that would create a cycle', () => {
|
||||
const store = createStore();
|
||||
// !b is a child of !a.
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!a:s', children: ['!b:s'] });
|
||||
// Now make !a a child of !b -> !a's parents include !b, but !b already has !a
|
||||
// as an ancestor, so the cycle branch skips re-registering.
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!b:s', children: ['!a:s'] });
|
||||
|
||||
assert.deepEqual(parentsOf(store, '!b:s'), ['!a:s']);
|
||||
// !a was not registered as a child of !b because that closes a cycle.
|
||||
assert.equal(get(store).has('!a:s'), false);
|
||||
});
|
||||
|
||||
test('DELETE removes the room as a parent entry', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!space:s', children: ['!c:s'] });
|
||||
// Register !space itself as a child of a grandparent so it has its own entry.
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!grand:s', children: ['!space:s'] });
|
||||
assert.equal(get(store).has('!space:s'), true);
|
||||
|
||||
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!space:s' });
|
||||
// !space's own entry (as a child of !grand) is gone.
|
||||
assert.equal(get(store).has('!space:s'), false);
|
||||
});
|
||||
|
||||
test('DELETE strips the room from every child parent-set', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p1:s', children: ['!c:s'] });
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p2:s', children: ['!c:s'] });
|
||||
|
||||
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!p1:s' });
|
||||
// !c keeps !p2 as a parent.
|
||||
assert.deepEqual(parentsOf(store, '!c:s'), ['!p2:s']);
|
||||
});
|
||||
|
||||
test('DELETE prunes a child left with zero parents (orphan cleanup)', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!space:s', children: ['!c:s'] });
|
||||
assert.equal(get(store).has('!c:s'), true);
|
||||
|
||||
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!space:s' });
|
||||
// !c had only !space as a parent; with that gone it is pruned entirely.
|
||||
assert.equal(get(store).has('!c:s'), false);
|
||||
assert.equal(get(store).size, 0);
|
||||
});
|
||||
|
||||
test('DELETE removes the room as both a parent and a child', () => {
|
||||
const store = createStore();
|
||||
// !mid is a child of !top and a parent of !leaf.
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!top:s', children: ['!mid:s'] });
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!mid:s', children: ['!leaf:s'] });
|
||||
|
||||
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!mid:s' });
|
||||
// !mid's own entry is gone (was a child of !top).
|
||||
assert.equal(get(store).has('!mid:s'), false);
|
||||
// !leaf lost its only parent !mid, so it is pruned.
|
||||
assert.equal(get(store).has('!leaf:s'), false);
|
||||
});
|
||||
|
||||
test('DELETE of an unknown room only prunes nothing', () => {
|
||||
const store = createStore();
|
||||
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p:s', children: ['!c:s'] });
|
||||
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!unknown:s' });
|
||||
assert.deepEqual(parentsOf(store, '!c:s'), ['!p:s']);
|
||||
assert.equal(get(store).size, 1);
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
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<typeof createStore>) => store.get(roomToUnreadAtom);
|
||||
|
||||
const info = (roomId: string, total: number, highlight: number): UnreadInfo => ({
|
||||
roomId,
|
||||
total,
|
||||
highlight,
|
||||
});
|
||||
|
||||
const seedParents = (store: ReturnType<typeof createStore>, 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<string> | 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);
|
||||
});
|
||||
Reference in New Issue
Block a user