mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 19:32:21 +02:00
* refactor(notes): decompose Notes into feature-owned modules * fix(notes): avoid HTML interpolation in reminder picker * docs(notes): drop checkpoint archives, session logs and publication drafts The N0-N6 tarballs and raw pytest/node logs are not read by any script or test, and the logs carry absolute home-directory paths. Once squashed into dev they would stay in history for good. notes-pr.md and notes-issue.md were the drafts used to open #6712 and #6713, not documentation. notes-validation.md keeps the counts, the checkpoint hashes and implementation.sha256 as the record; local interpreter paths and the local Docker image name are replaced with portable placeholders. * fix(notes): keep the state store raw and stop masonry re-observing after close - state: writes built from views (filter, spread, splice) stored nested read-only views in the store, so structuredClone on a stored note threw DataCloneError. Store writes now unwrap nested views. - layout: a save that resolves after closePanel re-renders the leaving pane and re-created the masonry ResizeObserver that teardown had disposed. Skip creating it once Notes is closed. - index.js: drop 30 imported or declared names that were never used and group the imports at the top. Import order is unchanged. - Revert unrelated punctuation edits in .gitattributes and keep the header comment and docstring first in compare/vote.js and test_notes_dom_xss_helpers.py. Both fixes have a node test that fails on the previous head. * test(notes): make script rejection assertion case-insensitive Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum. --------- Co-authored-by: Léo <leograndcontact@gmail.com>
91 lines
5.3 KiB
JavaScript
91 lines
5.3 KiB
JavaScript
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);
|
|
});
|