test(09-01): add filterEntries unit tests proving infrastructure

- Test empty input handling
- Test query filter (min 2 chars, case insensitive, title OR content)
- Test tag filter (AND logic, case insensitive)
- Test type filter (task/thought/all)
- Test date range filter (start, end, both)
- Test combined filters
- Test generic type preservation

17 tests covering filterEntries.ts with 100% coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Richter
2026-02-03 23:29:44 +01:00
parent b0e8e4c0b9
commit b930f1842c

View File

@@ -0,0 +1,293 @@
import { describe, it, expect } from 'vitest';
import { filterEntries } from './filterEntries';
import type { SearchFilters } from '$lib/types/search';
// Test data factory
function createEntry(
overrides: Partial<{
id: string;
type: 'task' | 'thought';
title: string | null;
content: string;
createdAt: string;
tags: Array<{ id: string; name: string; entryId: string }>;
}> = {}
) {
return {
id: overrides.id ?? 'entry-1',
type: overrides.type ?? 'task',
title: overrides.title ?? null,
content: overrides.content ?? 'Default content',
createdAt: overrides.createdAt ?? '2026-01-15T10:00:00Z',
updatedAt: '2026-01-15T10:00:00Z',
tags: overrides.tags ?? []
};
}
function createFilters(overrides: Partial<SearchFilters> = {}): SearchFilters {
return {
query: overrides.query ?? '',
tags: overrides.tags ?? [],
type: overrides.type ?? 'all',
dateRange: overrides.dateRange ?? { start: null, end: null }
};
}
describe('filterEntries', () => {
describe('empty input', () => {
it('returns empty array when given empty entries', () => {
const result = filterEntries([], createFilters());
expect(result).toEqual([]);
});
});
describe('query filter', () => {
it('ignores query shorter than 2 characters', () => {
const entries = [createEntry({ content: 'Hello world' })];
const result = filterEntries(entries, createFilters({ query: 'H' }));
expect(result).toHaveLength(1);
});
it('filters by content match (case insensitive)', () => {
const entries = [
createEntry({ id: '1', content: 'Buy groceries' }),
createEntry({ id: '2', content: 'Write code' }),
createEntry({ id: '3', content: 'Buy books' })
];
const result = filterEntries(entries, createFilters({ query: 'buy' }));
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['1', '3']);
});
it('filters by title match (case insensitive)', () => {
const entries = [
createEntry({ id: '1', title: 'Shopping List', content: 'items' }),
createEntry({ id: '2', title: 'Work Notes', content: 'stuff' }),
createEntry({ id: '3', title: null, content: 'shopping reminder' })
];
const result = filterEntries(entries, createFilters({ query: 'shopping' }));
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['1', '3']);
});
it('matches title OR content', () => {
const entries = [
createEntry({ id: '1', title: 'Meeting', content: 'discuss project' }),
createEntry({ id: '2', title: 'Note', content: 'meeting notes' })
];
const result = filterEntries(entries, createFilters({ query: 'meeting' }));
expect(result).toHaveLength(2);
});
});
describe('tag filter', () => {
it('filters entries with matching tag', () => {
const entries = [
createEntry({
id: '1',
tags: [{ id: 't1', name: 'work', entryId: '1' }]
}),
createEntry({
id: '2',
tags: [{ id: 't2', name: 'personal', entryId: '2' }]
}),
createEntry({
id: '3',
tags: [
{ id: 't3', name: 'work', entryId: '3' },
{ id: 't4', name: 'urgent', entryId: '3' }
]
})
];
const result = filterEntries(entries, createFilters({ tags: ['work'] }));
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['1', '3']);
});
it('requires ALL tags (AND logic)', () => {
const entries = [
createEntry({
id: '1',
tags: [{ id: 't1', name: 'work', entryId: '1' }]
}),
createEntry({
id: '2',
tags: [
{ id: 't2', name: 'work', entryId: '2' },
{ id: 't3', name: 'urgent', entryId: '2' }
]
}),
createEntry({
id: '3',
tags: [
{ id: 't4', name: 'work', entryId: '3' },
{ id: 't5', name: 'urgent', entryId: '3' },
{ id: 't6', name: 'meeting', entryId: '3' }
]
})
];
const result = filterEntries(entries, createFilters({ tags: ['work', 'urgent'] }));
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['2', '3']);
});
it('matches tags case-insensitively', () => {
const entries = [
createEntry({
id: '1',
tags: [{ id: 't1', name: 'Work', entryId: '1' }]
})
];
const result = filterEntries(entries, createFilters({ tags: ['work'] }));
expect(result).toHaveLength(1);
});
it('returns empty for entries without any tags when tag filter active', () => {
const entries = [createEntry({ id: '1', tags: [] })];
const result = filterEntries(entries, createFilters({ tags: ['work'] }));
expect(result).toHaveLength(0);
});
});
describe('type filter', () => {
it('returns all types when filter is "all"', () => {
const entries = [
createEntry({ id: '1', type: 'task' }),
createEntry({ id: '2', type: 'thought' })
];
const result = filterEntries(entries, createFilters({ type: 'all' }));
expect(result).toHaveLength(2);
});
it('filters by task type', () => {
const entries = [
createEntry({ id: '1', type: 'task' }),
createEntry({ id: '2', type: 'thought' }),
createEntry({ id: '3', type: 'task' })
];
const result = filterEntries(entries, createFilters({ type: 'task' }));
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['1', '3']);
});
it('filters by thought type', () => {
const entries = [
createEntry({ id: '1', type: 'task' }),
createEntry({ id: '2', type: 'thought' })
];
const result = filterEntries(entries, createFilters({ type: 'thought' }));
expect(result).toHaveLength(1);
expect(result[0].id).toBe('2');
});
});
describe('date range filter', () => {
const entries = [
createEntry({ id: '1', createdAt: '2026-01-10T10:00:00Z' }),
createEntry({ id: '2', createdAt: '2026-01-15T10:00:00Z' }),
createEntry({ id: '3', createdAt: '2026-01-20T10:00:00Z' })
];
it('filters by start date', () => {
const result = filterEntries(
entries,
createFilters({ dateRange: { start: '2026-01-15', end: null } })
);
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['2', '3']);
});
it('filters by end date (inclusive)', () => {
const result = filterEntries(
entries,
createFilters({ dateRange: { start: null, end: '2026-01-15' } })
);
expect(result).toHaveLength(2);
expect(result.map((e) => e.id)).toEqual(['1', '2']);
});
it('filters by both start and end date', () => {
const result = filterEntries(
entries,
createFilters({ dateRange: { start: '2026-01-12', end: '2026-01-18' } })
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('2');
});
});
describe('combined filters', () => {
it('applies all filters together', () => {
const entries = [
createEntry({
id: '1',
type: 'task',
content: 'Buy groceries',
tags: [{ id: 't1', name: 'shopping', entryId: '1' }],
createdAt: '2026-01-15T10:00:00Z'
}),
createEntry({
id: '2',
type: 'task',
content: 'Buy office supplies',
tags: [{ id: 't2', name: 'work', entryId: '2' }],
createdAt: '2026-01-15T10:00:00Z'
}),
createEntry({
id: '3',
type: 'thought',
content: 'Buy a car someday',
tags: [{ id: 't3', name: 'shopping', entryId: '3' }],
createdAt: '2026-01-15T10:00:00Z'
}),
createEntry({
id: '4',
type: 'task',
content: 'Buy groceries',
tags: [{ id: 't4', name: 'shopping', entryId: '4' }],
createdAt: '2026-01-01T10:00:00Z' // Too early
})
];
const result = filterEntries(
entries,
createFilters({
query: 'buy',
tags: ['shopping'],
type: 'task',
dateRange: { start: '2026-01-10', end: null }
})
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('1');
});
});
describe('preserves entry type', () => {
it('preserves additional properties on entries', () => {
interface ExtendedEntry {
id: string;
type: 'task' | 'thought';
title: string | null;
content: string;
createdAt: string;
updatedAt: string;
tags: Array<{ id: string; name: string; entryId: string }>;
images: Array<{ id: string; path: string }>;
}
const entries: ExtendedEntry[] = [
{
...createEntry({ id: '1', content: 'Has image' }),
images: [{ id: 'img1', path: '/uploads/photo.jpg' }]
}
];
const result = filterEntries(entries, createFilters({ query: 'image' }));
expect(result).toHaveLength(1);
expect(result[0].images).toEqual([{ id: 'img1', path: '/uploads/photo.jpg' }]);
});
});
});