Files
crumbs-original/src/lib/stores/notes.test.ts
T
Frederic CabassutandClaude Opus 4.8 460f39298b fix(search): preserve unsynced hits and avoid empty-state flash
Address review feedback on the search-results preservation change:

- reconcileSearchResults now keeps a result absent from the canonical
  notes store (a server hit not yet loaded locally, e.g. from another
  device) instead of dropping it, so valid matches no longer vanish
  mid-session. Locally deleted/left notes are still pruned at their call
  sites; trashed notes are still dropped.
- SearchBar commits searchQuery only once its results arrive, so the view
  no longer flashes an empty state over the current notes while a request
  is in flight, and searchQuery stays consistent with searchResults on a
  failed request.
- Expand reconcile unit tests (keep-absent, trashed-drop, mixed order,
  empty) and add an e2e test asserting notes stay visible during an
  in-flight search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:57:00 +02:00

146 lines
4.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { mergeNotesByVersion, reconcileSearchResults } from './notes.js';
import type { Note } from '$lib/types/index.js';
function makeNote(overrides: Partial<Note> = {}): Note {
return {
id: 'note-1',
title: 'Test',
content: '- [ ] Item',
color: 'default',
pinned: false,
archived: false,
trashed: false,
trashedAt: null,
checklistMode: true,
sortOrder: 0,
createdAt: new Date(),
updatedAt: new Date(),
version: 1,
tags: [],
attachments: [],
collaborators: [],
isOwner: true,
isShared: false,
...overrides
};
}
describe('mergeNotesByVersion', () => {
it('keeps the local note when it has a higher version than the incoming note', () => {
const local = makeNote({ id: 'n1', version: 3, content: '- [x] Checked' });
const incoming = makeNote({ id: 'n1', version: 2, content: '- [ ] Unchecked' });
const result = mergeNotesByVersion([local], [incoming]);
expect(result).toHaveLength(1);
expect(result[0].content).toBe('- [x] Checked');
expect(result[0].version).toBe(3);
});
it('uses the incoming note when it has a higher or equal version', () => {
const local = makeNote({ id: 'n1', version: 2, content: '- [ ] Old' });
const incoming = makeNote({ id: 'n1', version: 3, content: '- [x] New' });
const result = mergeNotesByVersion([local], [incoming]);
expect(result).toHaveLength(1);
expect(result[0].content).toBe('- [x] New');
expect(result[0].version).toBe(3);
});
it('uses the incoming note when versions are equal', () => {
const local = makeNote({ id: 'n1', version: 2, content: 'local' });
const incoming = makeNote({ id: 'n1', version: 2, content: 'incoming' });
const result = mergeNotesByVersion([local], [incoming]);
expect(result[0].content).toBe('incoming');
});
it('handles notes not present in the local store', () => {
const incoming = makeNote({ id: 'n2', version: 1 });
const result = mergeNotesByVersion([], [incoming]);
expect(result).toHaveLength(1);
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' }),
makeNote({ id: 'n2', version: 1, content: 'unchanged' })
];
const incomingNotes = [
makeNote({ id: 'n1', version: 4, content: '- [ ] Stale from server' }),
makeNote({ id: 'n2', version: 2, content: 'updated on server' })
];
const result = mergeNotesByVersion(localNotes, incomingNotes);
expect(result).toHaveLength(2);
// n1: local version 5 > incoming version 4 → keep local
expect(result[0].content).toBe('- [x] Just saved');
// n2: local version 1 < incoming version 2 → use incoming
expect(result[1].content).toBe('updated on server');
});
});
describe('reconcileSearchResults', () => {
it('replaces a stale result with the canonical note', () => {
const canonical = makeNote({ id: 'n1', title: 'Updated', version: 2 });
const result = reconcileSearchResults(
[canonical],
[makeNote({ id: 'n1', title: 'Stale', version: 1 })]
);
expect(result).toEqual([canonical]);
});
it('keeps a result absent from the canonical store (hit not yet loaded locally)', () => {
const serverHit = makeNote({ id: 'n2', title: 'From another device' });
const result = reconcileSearchResults([makeNote({ id: 'n1' })], [serverHit]);
expect(result).toEqual([serverHit]);
});
it('drops a result that is trashed in the canonical store', () => {
const trashed = makeNote({ id: 'n1', trashed: true });
const result = reconcileSearchResults([trashed], [makeNote({ id: 'n1', trashed: false })]);
expect(result).toEqual([]);
});
it('preserves result order while reconciling a mix of cases', () => {
const canonical = makeNote({ id: 'n1', title: 'Canonical', version: 2 });
const trashed = makeNote({ id: 'n2', trashed: true });
const absent = makeNote({ id: 'n3', title: 'Absent' });
const result = reconcileSearchResults(
[canonical, trashed],
[makeNote({ id: 'n1', title: 'Stale', version: 1 }), trashed, absent]
);
expect(result).toEqual([canonical, absent]);
});
it('returns an empty array when there are no results', () => {
expect(reconcileSearchResults([makeNote({ id: 'n1' })], [])).toEqual([]);
});
});