tests/notesReminders.test.mjs
Rama dev · réplica de lectura del repositorio original.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createNotesState } from '../frontend/src/features/notes/application/state.js';
import { createNotesReminders } from '../frontend/src/features/notes/application/reminders.js';
import { createNotesNotifications } from '../frontend/src/features/notes/infrastructure/notifications.js';
import { createNotesPersistence, REMINDER_DISMISSED_AT_KEY } from '../frontend/src/features/notes/infrastructure/storage.js';
function harness(notes = []) {
const now = Date.parse('2024-03-10T09:00'), app = createNotesState();
app.replaceNotes(notes);
const values = new Map(), timeouts = new Map(), intervals = new Map(), fired = [], shown = [], highlighted = [], patches = [];
let nextId = 0, response = Promise.resolve({ ok: true, json: async () => ({ synthesis: 'Synthesis' }) });
const scheduler = {
setTimeout: (fn, ms) => { const id = ++nextId; timeouts.set(id, { fn, ms }); return id; }, clearTimeout: id => timeouts.delete(id),
setInterval: (fn, ms) => { const id = ++nextId; intervals.set(id, { fn, ms }); return id; }, clearInterval: id => intervals.delete(id),
};
const persistence = createNotesPersistence({ storage: { getItem: key => values.get(key), setItem: (key, value) => values.set(key, value) } });
const reminders = createNotesReminders({
app, clock: { now: () => now }, scheduler, persistence, dismissedKey: REMINDER_DISMISSED_AT_KEY,
notifications: { show: (...args) => shown.push(args) }, fetchNotes: async () => {},
hasItems: note => ['todo', 'goal', 'checklist'].includes(note.note_type), isNoteFullyDone: () => false,
api: { listNotes: async () => ({ ok: true, json: async () => ({ notes }) }), patchNote: async (...args) => patches.push(args), fireReminder: body => { fired.push(body); return response; } },
effects: { badge: () => {}, highlight: id => highlighted.push(id) },
});
return { now, app, reminders, timeouts, intervals, fired, shown, highlighted, patches, persistence, setResponse: value => { response = value; } };
}
test('startup and loop are idempotent, and panel closure leaves background scheduling alive', async () => {
const h = harness();
h.reminders.start(); h.reminders.start();
assert.deepEqual([...h.timeouts.values()].map(t => t.ms), [3000]);
await [...h.timeouts.values()][0].fn();
h.reminders._startReminderLoop();
assert.deepEqual([...h.intervals.values()].map(t => t.ms), [30000]);
h.app.setOpen(false);
assert.equal(h.intervals.size, 1);
h.reminders.dispose();
assert.equal(h.intervals.size, 0);
h.reminders._startReminderLoop();
assert.equal(h.intervals.size, 0);
});
test('fresh reminders dispatch once, stale reminders mark silently, recurring reminders advance', async () => {
const now = Date.parse('2024-03-10T09:00');
const local = value => { const d = new Date(value); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:30`; };
const h = harness([
{ id: 'fresh', due_date: local(now - 30000), content: 'Body' },
{ id: 'old', due_date: local(now - 180000) },
{ id: 'recurring', due_date: local(now - 180000), repeat: 'daily' },
{ id: 'date', due_date: '2024-03-10' },
{ id: 'archived', archived: true, due_date: local(now - 30000) },
]);
h.reminders._checkReminders(); h.reminders._checkReminders();
await new Promise(setImmediate);
assert.deepEqual(h.fired.map(n => n.note_id), ['fresh']);
assert.deepEqual([...h.persistence._loadFiredReminders()], ['fresh', 'old']);
assert.equal(h.patches.length, 1);
assert.equal(h.app.state.notes[2].due_date, '2024-03-11T08:57');
assert.deepEqual(h.highlighted, ['fresh']);
assert.equal(h.shown[0][1], 'Synthesis');
});
test('slow synthesis and rejected dispatch use the local fallback exactly once', async () => {
const h = harness();
let resolve;
h.setResponse(new Promise(r => { resolve = r; }));
h.reminders._fireReminder({ id: 'a', title: 'Title', content: 'Raw' });
const timer = [...h.timeouts.values()].find(t => t.ms === 1500);
timer.fn();
resolve({ ok: true, json: async () => ({ synthesis: 'Too late' }) });
await new Promise(setImmediate);
assert.deepEqual(h.shown, [['Title', 'Raw', 'a']]);
h.setResponse(Promise.reject(Error('offline')));
h.reminders._fireReminder({ id: 'b', note_type: 'todo', items: [{ text: 'Task', done: false }] });
await new Promise(setImmediate);
assert.equal(h.shown.length, 2);
assert.equal(h.fired[1].body, 'Pending (1):\n- Task');
});
test('notification denial, unsupported browsers and permission errors remain fail-safe', async () => {
const toast = [];
const absent = createNotesNotifications({ browser: {}, toast: title => toast.push(title) });
assert.equal(await absent.ensurePermission(), false);
absent.show('Title', 'Body', 'a');
assert.deepEqual(toast, ['Title']);
const denied = createNotesNotifications({ browser: { Notification: true }, notify: { permission: 'denied' } });
assert.equal(await denied.ensurePermission(), false);
const failed = createNotesNotifications({ browser: { Notification: true }, notify: { permission: 'default', requestPermission: async () => { throw Error('blocked'); } } });
assert.equal(await failed.ensurePermission(), false);
});