// The popup stack's lifetime: the user's choice decides it, and "never" really // means the card waits for the user rather than for a timer. import { test } from 'node:test' import assert from 'node:assert/strict' import { createStore } from '../../src/client/core/store.js' import { createToastStack } from '../../src/client/core/toasts.js' import { createCopy } from '../../src/client/core/copy.js' import { createTranslator } from '../../src/client/i18n/index.js' import { TOAST_DURATION_MS } from '../../src/client/constants.js' const tr = createTranslator({ getSnapshot: () => ({ active: 'zh' }) }) const candidate = { kind: 'completion', sessionId: 's1', title: '标题', detail: '', pendingKind: '' } /** A stack over a store whose only interesting field is the popup lifetime. */ function stack(dismissAfterMs) { const store = createStore({ dismissAfterMs, toasts: [] }) return { store, toasts: createToastStack({ store, copy: createCopy(tr) }) } } const ids = (store) => store.getSnapshot().toasts.map((toast) => toast.id) test('a card closes itself after the configured lifetime', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(6000) toasts.showToast(candidate) assert.equal(ids(store).length, 1) context.mock.timers.tick(5999) assert.equal(ids(store).length, 1, 'the card must outlive its countdown') context.mock.timers.tick(1) assert.deepEqual(ids(store), []) }) test('a lifetime of zero leaves the card until it is dismissed', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(0) toasts.showToast(candidate) context.mock.timers.tick(60 * 60 * 1000) assert.equal(ids(store).length, 1, 'nothing may close a "never" card') toasts.closeToast(ids(store)[0]) assert.deepEqual(ids(store), []) }) test('hovering holds the card, and the lifetime chosen while held applies afterwards', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(3000) toasts.showToast(candidate) const id = ids(store)[0] toasts.holdToast(id) context.mock.timers.tick(30 * 1000) assert.equal(ids(store).length, 1, 'a held card has no timer left') // The settings page switches to "never" while the pointer is still on the card. store.set({ ...store.getSnapshot(), dismissAfterMs: 0 }) toasts.armToast(id) context.mock.timers.tick(60 * 60 * 1000) assert.equal(ids(store).length, 1) }) test('a lifetime change applies to the cards already on screen, but never to a held one', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(30000) toasts.showToast(candidate) toasts.showToast({ ...candidate, sessionId: 's2' }) const [newest, oldest] = ids(store) toasts.holdToast(oldest) // The settings page shortens the lifetime while both cards are visible. store.set({ ...store.getSnapshot(), dismissAfterMs: 3000 }) toasts.rearmAll() context.mock.timers.tick(3000) assert.deepEqual(ids(store), [oldest], 'the visible card followed the new choice, the held one did not') toasts.armToast(oldest) context.mock.timers.tick(3000) assert.deepEqual(ids(store), [], 'once the pointer leaves, the held card counts down again') void newest }) test('a lifetime this version cannot use falls back to the shipped default', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(undefined) toasts.showToast(candidate) context.mock.timers.tick(TOAST_DURATION_MS) assert.deepEqual(ids(store), []) }) test('the stack never grows past its bound, however long the cards live', (context) => { context.mock.timers.enable({ apis: ['setTimeout'] }) const { store, toasts } = stack(0) for (let index = 0; index < 5; index += 1) toasts.showToast({ ...candidate, sessionId: `s${index}` }) assert.equal(ids(store).length, 3) assert.deepEqual(ids(store), ['dsn-5', 'dsn-4', 'dsn-3'], 'newest first') })