mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 09:49:41 +02:00
feat(notifications): configurable toast and browser-notification display time (#564)
Squash-merged so the toast-history half, dropped during review, stays out of master's history.
This commit is contained in:
@@ -23,6 +23,10 @@ type Manager = {
|
||||
getStorageKey: () => string;
|
||||
normalizePreferences: (preferences: Record<string, unknown>) => NotificationPreferences;
|
||||
notify: (notification: Record<string, unknown>) => void;
|
||||
getToastDurationMs: () => number;
|
||||
unreadCount: number;
|
||||
markAllRead: () => void;
|
||||
clearAll: () => void;
|
||||
};
|
||||
|
||||
const openWindows: JSDOM[] = [];
|
||||
@@ -48,6 +52,10 @@ function loadManager(
|
||||
STUCK_THRESHOLD_DEFAULT_MS: number;
|
||||
GROUPING_TIMEOUT_MS: number;
|
||||
NOTIFICATION_LIST_CAP: number;
|
||||
AUTO_CLOSE_NOTIFICATION_MS: number;
|
||||
DEFAULT_TOAST_DURATION_MS: number;
|
||||
MIN_NOTIFICATION_DURATION_MS: number;
|
||||
MAX_NOTIFICATION_DURATION_MS: number;
|
||||
};
|
||||
win.MobileDetection = {
|
||||
getDeviceType: () => device.deviceType ?? 'desktop',
|
||||
@@ -56,6 +64,10 @@ function loadManager(
|
||||
win.STUCK_THRESHOLD_DEFAULT_MS = 600_000;
|
||||
win.GROUPING_TIMEOUT_MS = 5_000;
|
||||
win.NOTIFICATION_LIST_CAP = 100;
|
||||
win.AUTO_CLOSE_NOTIFICATION_MS = 8_000;
|
||||
win.DEFAULT_TOAST_DURATION_MS = 3_000;
|
||||
win.MIN_NOTIFICATION_DURATION_MS = 1_000;
|
||||
win.MAX_NOTIFICATION_DURATION_MS = 300_000;
|
||||
win.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
||||
callback(0);
|
||||
return 1;
|
||||
@@ -66,6 +78,7 @@ function loadManager(
|
||||
}
|
||||
|
||||
win.eval(`
|
||||
window.escapeHtml = (value) => String(value).replace(/[&<>"']/g, (c) => '&#' + c.charCodeAt(0) + ';');
|
||||
${SOURCE}
|
||||
window.__testNotificationManager = NotificationManager;
|
||||
`);
|
||||
@@ -148,3 +161,23 @@ describe('notification noise defaults', () => {
|
||||
expect(manager.getStorageKey()).toBe('codeman-notification-prefs-mobile');
|
||||
});
|
||||
});
|
||||
|
||||
describe('notification display time', () => {
|
||||
it('defaults to 3s toasts and 8s browser notifications', () => {
|
||||
const { manager } = loadManager();
|
||||
expect(manager.getToastDurationMs()).toBe(3000);
|
||||
expect((manager.preferences as unknown as Record<string, number>).browserAutoCloseMs).toBe(8000);
|
||||
});
|
||||
|
||||
it('honours a configured toast time and clamps unusable values', () => {
|
||||
const { manager } = loadManager({ toastDurationMs: 15_000 });
|
||||
expect(manager.getToastDurationMs()).toBe(15_000);
|
||||
|
||||
const clamp = (value: unknown) =>
|
||||
(manager.normalizePreferences({ toastDurationMs: value }) as unknown as Record<string, number>).toastDurationMs;
|
||||
expect(clamp(10)).toBe(1000);
|
||||
expect(clamp(9_999_999)).toBe(300_000);
|
||||
expect(clamp('soon')).toBe(3000);
|
||||
expect(clamp(undefined)).toBe(3000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @fileoverview showToast() display time and drawer logging: a toast with no explicit
|
||||
* `duration` uses the notification preference, an explicit `duration` (0 = sticky) still wins,
|
||||
* and every toast is recorded in the notification drawer.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const windows: JSDOM[] = [];
|
||||
|
||||
function loadApp(notificationManager?: unknown) {
|
||||
const dom = new JSDOM('<!doctype html><body></body>', { url: 'http://localhost/' });
|
||||
windows.push(dom);
|
||||
const CodemanApp = function CodemanApp() {} as unknown as { prototype: Record<string, unknown> };
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
document: dom.window.document,
|
||||
requestAnimationFrame: (cb: () => void) => cb(),
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
console,
|
||||
});
|
||||
const source = readFileSync(resolve(import.meta.dirname, '../src/web/public/panels-ui.js'), 'utf8');
|
||||
vm.runInContext(source, context, { filename: 'panels-ui.js' });
|
||||
const app = new (CodemanApp as unknown as new () => Record<string, any>)();
|
||||
app.notificationManager = notificationManager;
|
||||
return app;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
for (const dom of windows.splice(0)) dom.window.close();
|
||||
});
|
||||
|
||||
describe('showToast', () => {
|
||||
it('uses the configured display time when no duration is given', () => {
|
||||
vi.useFakeTimers();
|
||||
const app = loadApp({ getToastDurationMs: () => 10_000 });
|
||||
app.showToast('hello');
|
||||
expect(windows[0].window.document.querySelectorAll('.toast')).toHaveLength(1);
|
||||
vi.advanceTimersByTime(9_000);
|
||||
expect(windows[0].window.document.querySelector('.toast.show')).not.toBeNull();
|
||||
vi.advanceTimersByTime(1_500);
|
||||
expect(windows[0].window.document.querySelector('.toast.show')).toBeNull();
|
||||
});
|
||||
|
||||
it('falls back to 3s without a notification manager', () => {
|
||||
vi.useFakeTimers();
|
||||
const app = loadApp();
|
||||
app.showToast('hello');
|
||||
vi.advanceTimersByTime(3_100);
|
||||
expect(windows[0].window.document.querySelector('.toast.show')).toBeNull();
|
||||
});
|
||||
|
||||
it('lets an explicit duration of 0 stay until dismissed', () => {
|
||||
vi.useFakeTimers();
|
||||
const app = loadApp({ getToastDurationMs: () => 1_000 });
|
||||
app.showToast('sticky', 'error', { duration: 0 });
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(windows[0].window.document.querySelector('.toast.show')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user