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:
293
src/lib/utils/filterEntries.test.ts
Normal file
293
src/lib/utils/filterEntries.test.ts
Normal 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' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user