From 6278f11184e08bad0d0afc0fac6d86b0aa49ac26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 17:50:21 +0000 Subject: [PATCH] fix: preserve local-only notes during loadNotes merge mergeNotesByVersion only iterated incoming notes, dropping any local-only notes (e.g. just created but not yet in the server/IDB response). This caused note cards to disappear briefly after creation when loadNotes raced with createNote. Also rename E2E test note title to avoid name collision with existing "Groceries" test. https://claude.ai/code/session_01TdPpATUkmdY7DhBkmqrvWs --- src/lib/stores/notes.test.ts | 11 +++++++++++ src/lib/stores/notes.ts | 8 ++++++++ tests/e2e/checklist.spec.ts | 6 +++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/lib/stores/notes.test.ts b/src/lib/stores/notes.test.ts index 637fe0f..22d9e92 100644 --- a/src/lib/stores/notes.test.ts +++ b/src/lib/stores/notes.test.ts @@ -67,6 +67,17 @@ describe('mergeNotesByVersion', () => { expect(result[0].id).toBe('n2'); }); + it('preserves local-only notes not present in the incoming data', () => { + const localOnly = makeNote({ id: 'n1', version: 1, content: 'just created' }); + const serverNote = makeNote({ id: 'n2', version: 1, content: 'from server' }); + + const result = mergeNotesByVersion([localOnly], [serverNote]); + + expect(result).toHaveLength(2); + expect(result.find((n) => n.id === 'n1')?.content).toBe('just created'); + expect(result.find((n) => n.id === 'n2')?.content).toBe('from server'); + }); + it('preserves multiple notes correctly during a race condition', () => { const localNotes = [ makeNote({ id: 'n1', version: 5, content: '- [x] Just saved' }), diff --git a/src/lib/stores/notes.ts b/src/lib/stores/notes.ts index e45b008..31617b3 100644 --- a/src/lib/stores/notes.ts +++ b/src/lib/stores/notes.ts @@ -60,11 +60,19 @@ function isNetworkError(err: unknown): boolean { */ export function mergeNotesByVersion(current: Note[], incoming: Note[]): Note[] { const currentMap = new Map(current.map((n) => [n.id, n])); + const incomingIds = new Set(); const merged = incoming.map((incomingNote) => { + incomingIds.add(incomingNote.id); const local = currentMap.get(incomingNote.id); if (local && local.version > incomingNote.version) return local; return incomingNote; }); + // Preserve local-only notes (e.g. just created, not yet in server/IDB response) + for (const local of current) { + if (!incomingIds.has(local.id)) { + merged.push(local); + } + } return merged; } diff --git a/tests/e2e/checklist.spec.ts b/tests/e2e/checklist.spec.ts index a402d5e..0127218 100644 --- a/tests/e2e/checklist.spec.ts +++ b/tests/e2e/checklist.spec.ts @@ -62,10 +62,10 @@ test.describe('Checklist', () => { test('Scenario: Multiple checked items persist after closing and reopening', async ({ authenticatedPage: page }) => { // Given a checklist note with three items exists - await createChecklistNote(page, 'Groceries', ['Milk', 'Eggs', 'Bread']); + await createChecklistNote(page, 'Weekly Shop', ['Milk', 'Eggs', 'Bread']); // When the user reopens and checks two items - await noteCard(page, 'Groceries').click(); + await noteCard(page, 'Weekly Shop').click(); await page.getByTestId('checklist-checkbox').nth(0).click(); // Milk → done await page.getByTestId('checklist-checkbox').nth(0).click(); // Eggs (now first) → done @@ -74,7 +74,7 @@ test.describe('Checklist', () => { await page.getByTestId('close-editor-btn').click(); // And the checked state persists after reopening - await noteCard(page, 'Groceries').click(); + await noteCard(page, 'Weekly Shop').click(); await expect(page.getByTestId('checklist-toggle-done')).toContainText('2 done'); await expect(page.getByTestId('checklist-input')).toHaveCount(1); // Only Bread remains active await expect(page.getByTestId('checklist-input').first()).toHaveValue('Bread');