- Toast queue: a burst of notifications appended unboundedly and could cover the viewport. Cap at 5 in the atom writer, dropping the OLDEST non-sticky toast (sticky = action toasts requiring a click, never dropped). The drop scan excludes the just-appended newest (`length - 1` bound) so a fresh toast is never the one eaten when the cap is full of stickies — it stretches instead. Container gains a maxHeight + overflowY safety net and scrolls the newest (bottom) toast into view if the stack ever overflows. +4 unit tests incl. the cap-full-of-stickies boundary. - "Unread First" room sort left the entire read tail (all counts tie at 0) in arbitrary Map order. factoryRoomIdByUnread now breaks ties by recent activity. Relocated from Home.tsx (module-private) to utils/sort.ts (exported, pure) and unit-tested (equal-count and read-tail cases fall back to activity). Bug-hunt findings from LOTUS_TODO. Three review passes: the second caught that the cap could silently drop the newest notification when full of stickies (real bug, untested boundary) — fixed and covered; a third traced the corrected loop. Gate-green (tsc, eslint, prettier, 920 tests, build). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.8 KiB
TypeScript
141 lines
4.8 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createStore } from 'jotai';
|
|
import { toastQueueAtom, dismissToastAtom, ToastNotif, createDownloadToast } from './toast';
|
|
|
|
// The queue lives in an unexported baseAtom; we drive the two write-only setters
|
|
// (toastQueueAtom append + null no-op guard, dismissToastAtom remove-by-id)
|
|
// through a jotai store and read back via toastQueueAtom's getter.
|
|
|
|
const makeToast = (id: string, sticky?: boolean): ToastNotif => ({
|
|
id,
|
|
displayName: `name-${id}`,
|
|
body: `body-${id}`,
|
|
roomName: `room-${id}`,
|
|
roomId: `!${id}:server`,
|
|
...(sticky ? { sticky: true } : {}),
|
|
});
|
|
|
|
test('starts empty', () => {
|
|
const store = createStore();
|
|
assert.deepEqual(store.get(toastQueueAtom), []);
|
|
});
|
|
|
|
test('toastQueueAtom appends in order', () => {
|
|
const store = createStore();
|
|
const a = makeToast('a');
|
|
const b = makeToast('b');
|
|
store.set(toastQueueAtom, a);
|
|
store.set(toastQueueAtom, b);
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['a', 'b'],
|
|
);
|
|
assert.equal(store.get(toastQueueAtom)[0], a);
|
|
});
|
|
|
|
test('toastQueueAtom ignores null (no-op guard)', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(toastQueueAtom, null);
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['a'],
|
|
);
|
|
});
|
|
|
|
test('toastQueueAtom allows duplicate ids (no dedupe)', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
assert.equal(store.get(toastQueueAtom).length, 2);
|
|
});
|
|
|
|
test('dismissToastAtom removes the matching id only', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(toastQueueAtom, makeToast('b'));
|
|
store.set(toastQueueAtom, makeToast('c'));
|
|
|
|
store.set(dismissToastAtom, 'b');
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['a', 'c'],
|
|
);
|
|
});
|
|
|
|
test('dismissToastAtom removes every entry sharing the id', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(toastQueueAtom, makeToast('b'));
|
|
|
|
store.set(dismissToastAtom, 'a');
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['b'],
|
|
);
|
|
});
|
|
|
|
test('dismissToastAtom for an unknown id is a no-op', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('a'));
|
|
store.set(dismissToastAtom, 'missing');
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['a'],
|
|
);
|
|
});
|
|
|
|
test('toastQueueAtom caps at 5, dropping the oldest non-sticky', () => {
|
|
const store = createStore();
|
|
// Append 7 transient toasts; the queue should keep only the newest 5.
|
|
for (let i = 0; i < 7; i += 1) store.set(toastQueueAtom, makeToast(`t${i}`));
|
|
assert.deepEqual(
|
|
store.get(toastQueueAtom).map((t) => t.id),
|
|
['t2', 't3', 't4', 't5', 't6'],
|
|
);
|
|
});
|
|
|
|
test('toastQueueAtom never drops a sticky toast, even over cap', () => {
|
|
const store = createStore();
|
|
store.set(toastQueueAtom, makeToast('sticky-old', true));
|
|
for (let i = 0; i < 7; i += 1) store.set(toastQueueAtom, makeToast(`t${i}`));
|
|
const ids = store.get(toastQueueAtom).map((t) => t.id);
|
|
// The sticky action toast survives; the oldest non-sticky ones are dropped.
|
|
assert.ok(ids.includes('sticky-old'));
|
|
assert.ok(ids.includes('t6')); // newest kept
|
|
assert.ok(!ids.includes('t0')); // oldest non-sticky dropped
|
|
assert.equal(ids.length, 5);
|
|
});
|
|
|
|
test('toastQueueAtom queue of all-sticky toasts is allowed to exceed the cap', () => {
|
|
const store = createStore();
|
|
for (let i = 0; i < 7; i += 1) store.set(toastQueueAtom, makeToast(`s${i}`, true));
|
|
// Nothing droppable → all 7 retained rather than silently losing action toasts.
|
|
assert.equal(store.get(toastQueueAtom).length, 7);
|
|
});
|
|
|
|
test('toastQueueAtom keeps a new transient toast even when the cap is full of stickies', () => {
|
|
const store = createStore();
|
|
// Fill the cap with sticky action toasts, then a normal message toast arrives.
|
|
for (let i = 0; i < 5; i += 1) store.set(toastQueueAtom, makeToast(`s${i}`, true));
|
|
store.set(toastQueueAtom, makeToast('fresh'));
|
|
const ids = store.get(toastQueueAtom).map((t) => t.id);
|
|
// The newest is never the one dropped — the queue stretches instead of eating it.
|
|
assert.ok(ids.includes('fresh'));
|
|
assert.equal(ids.length, 6);
|
|
});
|
|
|
|
test('createDownloadToast: filename in body, no room navigation, unique ids', () => {
|
|
const a = createDownloadToast('photo.jpg');
|
|
assert.equal(a.displayName, 'Downloaded');
|
|
assert.equal(a.body, 'photo.jpg');
|
|
// roomId empty + an onClick present → clicking dismisses without navigating to a room.
|
|
assert.equal(a.roomId, '');
|
|
assert.equal(a.roomName, '');
|
|
assert.equal(typeof a.onClick, 'function');
|
|
const b = createDownloadToast('photo.jpg');
|
|
assert.notEqual(a.id, b.id);
|
|
});
|