Ctrl K
ComunidadGuías…

tests/notesReminders.test.mjs

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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);
});