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); }); // --- #80: eviction prefers toasts that have already been visible a while --- test('toastQueueAtom prefers evicting an old-enough toast over a fresh one when both exist', (t) => { t.mock.timers.enable({ apis: ['Date'] }); const store = createStore(); store.set(toastQueueAtom, makeToast('old')); // t = 0 t.mock.timers.tick(2000); // 'old' is now well past the 1.5s min-visible mark store.set(toastQueueAtom, makeToast('mid')); // t = 2000, too young to prefer for (let i = 0; i < 4; i += 1) store.set(toastQueueAtom, makeToast(`n${i}`)); // fills to cap + 1 const ids = store.get(toastQueueAtom).map((toast) => toast.id); // 'old' has been visible long enough to be preferred for eviction over the // still-young 'mid', even though 'old' isn't the only non-sticky candidate. assert.ok(!ids.includes('old')); assert.ok(ids.includes('mid')); assert.equal(ids.length, 5); }); test('toastQueueAtom falls back to oldest-first when no toast is old enough yet', () => { const store = createStore(); // All appended back-to-back within the same tick — none has reached the // 1.5s min-visible mark, so eviction falls back to plain oldest-first. for (let i = 0; i < 7; i += 1) store.set(toastQueueAtom, makeToast(`t${i}`)); assert.deepEqual( store.get(toastQueueAtom).map((toast) => toast.id), ['t2', 't3', 't4', 't5', 't6'], ); }); 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); });