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>
71 lines
3.6 KiB
JavaScript
71 lines
3.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createNotesPersistence } from '../frontend/src/features/notes/infrastructure/storage.js';
|
|
import { createNotesState } from '../frontend/src/features/notes/application/state.js';
|
|
import { createNotesDrafts } from '../frontend/src/features/notes/application/drafts.js';
|
|
import { createDraftEditor } from '../frontend/src/features/notes/ui/draftEditor.js';
|
|
|
|
function harness(patch = async () => ({})) {
|
|
const data = new Map(), timers = new Map(), errors = [];
|
|
let nextTimer = 0;
|
|
const context = {
|
|
localStorage: { getItem: k => data.get(k) ?? null, setItem: (k,v) => data.set(k,v), removeItem: k => data.delete(k) },
|
|
setTimeout: f => { timers.set(++nextTimer, f); return nextTimer; },
|
|
clearTimeout: id => timers.delete(id),
|
|
_patchNote: patch, _notes: [{id:'note1'}],
|
|
_collectItems: form => form.items,
|
|
uiModule: {showError: message => errors.push(message)},
|
|
};
|
|
context.persistence = createNotesPersistence({ storage: context.localStorage });
|
|
context.app = createNotesState();
|
|
context.app.replaceNotes(context._notes);
|
|
context.state = context.app.state;
|
|
Object.assign(context, context.persistence);
|
|
const drafts = createNotesDrafts({ app: context.app, api: { patchNote: patch }, persistence: context.persistence, scheduler: context, effects: context.uiModule });
|
|
Object.assign(context, createDraftEditor({ ...context, ...context.persistence, bindDraft: drafts.bindDraft }));
|
|
const fields = {'.note-form-title':{value:'Title'}, '.note-form-content':{value:'Original'}};
|
|
const handlers = {};
|
|
const form = { dataset:{noteType:'note'}, items:[], querySelector: s => fields[s], addEventListener:(n,f) => handlers[n]=f };
|
|
context._wireDraftAutosave(form, 'note1');
|
|
return {context, data, form, fields, errors,
|
|
input: () => handlers.input(),
|
|
drain: async () => { for (const f of timers.values()) f(); timers.clear(); await new Promise(setImmediate); },
|
|
draft: () => JSON.parse(data.get('odysseus-note-draft-note1') || 'null'),
|
|
};
|
|
}
|
|
|
|
test('typing is backed up synchronously, even before the autosave timer', () => {
|
|
const h = harness();
|
|
h.fields['.note-form-content'].value = 'Just typed'; h.input();
|
|
assert.equal(h.draft().content, 'Just typed');
|
|
});
|
|
|
|
test('checklist type without an active pill preserves items', () => {
|
|
const h = harness(); h.form.dataset.noteType='checklist';
|
|
h.form.items=[{text:'Drop keys',done:false}]; h.input();
|
|
assert.equal(h.draft().note_type,'checklist');
|
|
assert.equal(h.draft().items[0].text,'Drop keys');
|
|
});
|
|
|
|
test('failed network save retains recoverable edits including empty text', async () => {
|
|
const h = harness(async () => { throw new Error('offline'); });
|
|
h.fields['.note-form-title'].value=''; h.fields['.note-form-content'].value=''; h.input();
|
|
await h.drain();
|
|
assert.equal(h.draft().content,'');
|
|
assert.equal(h.context._applyDraftToNote({content:'Old'},'note1').note.content,'');
|
|
assert.equal(h.errors.length,1);
|
|
});
|
|
|
|
test('older save cannot clear newer draft and writes are serialized', async () => {
|
|
const releases=[], writes=[];
|
|
const h=harness((id,payload) => { writes.push(payload.content); return new Promise(r=>releases.push(r)); });
|
|
h.fields['.note-form-content'].value='First'; h.input(); await h.drain();
|
|
h.fields['.note-form-content'].value='Second'; h.input(); await h.drain();
|
|
assert.deepEqual(writes,['First']);
|
|
releases.shift()({}); await new Promise(setImmediate);
|
|
assert.equal(h.draft().content,'Second');
|
|
assert.deepEqual(writes,['First','Second']);
|
|
releases.shift()({}); await new Promise(setImmediate);
|
|
assert.equal(h.draft(),null);
|
|
});
|