import { describe, it, expect } from 'vitest'; import { toWildcardQuery, buildJMAPFilter, isFilterEmpty, activeFilterCount, DEFAULT_SEARCH_FILTERS, } from '../jmap/search-utils'; import type { SearchFilters } from '../jmap/search-utils'; // --------------------------------------------------------------------------- // toWildcardQuery // --------------------------------------------------------------------------- describe('toWildcardQuery', () => { it('appends * to a single word', () => { expect(toWildcardQuery('pri')).toBe('pri*'); }); it('appends * to every word in a multi-word query', () => { expect(toWildcardQuery('hello world')).toBe('hello* world*'); }); it('handles single-character queries', () => { expect(toWildcardQuery('a')).toBe('a*'); }); it('handles two-character queries', () => { expect(toWildcardQuery('pr')).toBe('pr*'); }); it('handles long words', () => { expect(toWildcardQuery('internationalization')).toBe('internationalization*'); }); it('does not double-append * if already present', () => { expect(toWildcardQuery('hello*')).toBe('hello*'); expect(toWildcardQuery('hello* world')).toBe('hello* world*'); }); it('preserves quoted phrases ending with "', () => { expect(toWildcardQuery('"hello world"')).toBe('"hello* world"'); }); it('trims leading/trailing whitespace', () => { expect(toWildcardQuery(' hello ')).toBe('hello*'); }); it('collapses multiple spaces between words', () => { expect(toWildcardQuery('hello world')).toBe('hello* world*'); }); it('returns empty string for empty input', () => { expect(toWildcardQuery('')).toBe(''); }); it('returns empty string for whitespace-only input', () => { expect(toWildcardQuery(' ')).toBe(''); }); it('handles mixed words with and without wildcards', () => { expect(toWildcardQuery('foo* bar baz*')).toBe('foo* bar* baz*'); }); }); // --------------------------------------------------------------------------- // buildJMAPFilter // --------------------------------------------------------------------------- describe('buildJMAPFilter', () => { const emptyFilters: SearchFilters = { ...DEFAULT_SEARCH_FILTERS }; // -- text query ---------------------------------------------------------- describe('text query', () => { it('builds a text filter with wildcard from textQuery alone', () => { const result = buildJMAPFilter('pri', emptyFilters); expect(result).toEqual({ text: 'pri*' }); }); it('wildcards each word in multi-word text query', () => { const result = buildJMAPFilter('hello world', emptyFilters); expect(result).toEqual({ text: 'hello* world*' }); }); it('returns empty object when no query and no filters', () => { const result = buildJMAPFilter('', emptyFilters); expect(result).toEqual({}); }); }); // -- individual field filters ------------------------------------------- describe('individual field filters', () => { it('builds a from filter (no wildcard)', () => { const filters: SearchFilters = { ...emptyFilters, from: 'alice' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ from: 'alice' }); }); it('builds a to filter (no wildcard)', () => { const filters: SearchFilters = { ...emptyFilters, to: 'bob@example.com' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ to: 'bob@example.com' }); }); it('builds a subject filter (no wildcard)', () => { const filters: SearchFilters = { ...emptyFilters, subject: 'meeting' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ subject: 'meeting' }); }); it('builds a body filter (no wildcard)', () => { const filters: SearchFilters = { ...emptyFilters, body: 'payment' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ body: 'payment' }); }); }); // -- boolean / keyword filters ------------------------------------------ describe('boolean and keyword filters', () => { it('adds hasAttachment: true', () => { const filters: SearchFilters = { ...emptyFilters, hasAttachment: true }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ hasAttachment: true }); }); it('adds hasAttachment: false', () => { const filters: SearchFilters = { ...emptyFilters, hasAttachment: false }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ hasAttachment: false }); }); it('adds notKeyword $seen for isUnread=true', () => { const filters: SearchFilters = { ...emptyFilters, isUnread: true }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ notKeyword: '$seen' }); }); it('adds hasKeyword $seen for isUnread=false', () => { const filters: SearchFilters = { ...emptyFilters, isUnread: false }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ hasKeyword: '$seen' }); }); it('adds hasKeyword $flagged for isStarred=true', () => { const filters: SearchFilters = { ...emptyFilters, isStarred: true }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ hasKeyword: '$flagged' }); }); it('adds notKeyword $flagged for isStarred=false', () => { const filters: SearchFilters = { ...emptyFilters, isStarred: false }; const result = buildJMAPFilter('', filters); expect(result).toEqual({ notKeyword: '$flagged' }); }); }); // -- date filters ------------------------------------------------------- describe('date filters', () => { it('adds after condition for dateAfter', () => { const filters: SearchFilters = { ...emptyFilters, dateAfter: '2024-06-01' }; const result = buildJMAPFilter('', filters); expect(result).toHaveProperty('after'); expect(new Date(result.after as string).toISOString()).toContain('2024-06-01'); }); it('adds before condition for dateBefore (end of day)', () => { const filters: SearchFilters = { ...emptyFilters, dateBefore: '2024-12-31' }; const result = buildJMAPFilter('', filters); expect(result).toHaveProperty('before'); const d = new Date(result.before as string); expect(d.getHours()).toBe(23); expect(d.getMinutes()).toBe(59); }); it('ignores invalid dateAfter', () => { const filters: SearchFilters = { ...emptyFilters, dateAfter: 'not-a-date' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({}); }); it('ignores invalid dateBefore', () => { const filters: SearchFilters = { ...emptyFilters, dateBefore: 'nope' }; const result = buildJMAPFilter('', filters); expect(result).toEqual({}); }); }); // -- mailbox filter ------------------------------------------------------ describe('mailbox filter', () => { it('returns inMailbox filter when only mailboxId provided', () => { const result = buildJMAPFilter('', emptyFilters, 'inbox-1'); expect(result).toEqual({ inMailbox: 'inbox-1' }); }); it('wraps in AND when mailboxId combined with text query', () => { const result = buildJMAPFilter('pri', emptyFilters, 'inbox-1'); expect(result).toEqual({ operator: 'AND', conditions: [ { text: 'pri*' }, { inMailbox: 'inbox-1' }, ], }); }); }); // -- combinations -------------------------------------------------------- describe('combined filters', () => { it('ANDs text query with from filter', () => { const filters: SearchFilters = { ...emptyFilters, from: 'alice' }; const result = buildJMAPFilter('urgent', filters); expect(result).toEqual({ operator: 'AND', conditions: [ { text: 'urgent*' }, { from: 'alice' }, ], }); }); it('ANDs text query, from, subject, hasAttachment, and mailbox', () => { const filters: SearchFilters = { ...emptyFilters, from: 'ceo@corp.com', subject: 'quarterly', hasAttachment: true, }; const result = buildJMAPFilter('report', filters, 'mb-42'); expect(result).toEqual({ operator: 'AND', conditions: [ { text: 'report*' }, { from: 'ceo@corp.com' }, { subject: 'quarterly' }, { hasAttachment: true }, { inMailbox: 'mb-42' }, ], }); }); it('ANDs all possible filters at once', () => { const filters: SearchFilters = { from: 'alice', to: 'bob', subject: 'invoice', body: 'payment', hasAttachment: true, dateAfter: '2024-01-01', dateBefore: '2024-12-31', isUnread: true, isStarred: true, }; const result = buildJMAPFilter('money', filters, 'mb-1') as { operator: string; conditions: Record[]; }; expect(result.operator).toBe('AND'); expect(result.conditions).toContainEqual({ text: 'money*' }); expect(result.conditions).toContainEqual({ from: 'alice' }); expect(result.conditions).toContainEqual({ to: 'bob' }); expect(result.conditions).toContainEqual({ subject: 'invoice' }); expect(result.conditions).toContainEqual({ body: 'payment' }); expect(result.conditions).toContainEqual({ hasAttachment: true }); expect(result.conditions).toContainEqual({ notKeyword: '$seen' }); expect(result.conditions).toContainEqual({ hasKeyword: '$flagged' }); expect(result.conditions).toContainEqual({ inMailbox: 'mb-1' }); // dateAfter and dateBefore produce after/before ISO strings expect(result.conditions.some(c => 'after' in c)).toBe(true); expect(result.conditions.some(c => 'before' in c)).toBe(true); }); }); }); // --------------------------------------------------------------------------- // isFilterEmpty // --------------------------------------------------------------------------- describe('isFilterEmpty', () => { it('returns true for default filters', () => { expect(isFilterEmpty(DEFAULT_SEARCH_FILTERS)).toBe(true); }); it('returns false when from is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, from: 'alice' })).toBe(false); }); it('returns false when to is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, to: 'bob' })).toBe(false); }); it('returns false when subject is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, subject: 'hello' })).toBe(false); }); it('returns false when body is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, body: 'world' })).toBe(false); }); it('returns false when hasAttachment is true', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, hasAttachment: true })).toBe(false); }); it('returns false when hasAttachment is false', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, hasAttachment: false })).toBe(false); }); it('returns false when dateAfter is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, dateAfter: '2024-01-01' })).toBe(false); }); it('returns false when dateBefore is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, dateBefore: '2024-12-31' })).toBe(false); }); it('returns false when isUnread is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, isUnread: true })).toBe(false); }); it('returns false when isStarred is set', () => { expect(isFilterEmpty({ ...DEFAULT_SEARCH_FILTERS, isStarred: false })).toBe(false); }); }); // --------------------------------------------------------------------------- // activeFilterCount // --------------------------------------------------------------------------- describe('activeFilterCount', () => { it('returns 0 for default filters', () => { expect(activeFilterCount(DEFAULT_SEARCH_FILTERS)).toBe(0); }); it('counts each active text field', () => { expect(activeFilterCount({ ...DEFAULT_SEARCH_FILTERS, from: 'a', to: 'b' })).toBe(2); }); it('counts boolean filters', () => { expect(activeFilterCount({ ...DEFAULT_SEARCH_FILTERS, hasAttachment: true, isUnread: false, isStarred: true, })).toBe(3); }); it('counts date filters', () => { expect(activeFilterCount({ ...DEFAULT_SEARCH_FILTERS, dateAfter: '2024-01-01', dateBefore: '2024-12-31', })).toBe(2); }); it('counts all 9 filters when all active', () => { expect(activeFilterCount({ from: 'a', to: 'b', subject: 'c', body: 'd', hasAttachment: true, dateAfter: '2024-01-01', dateBefore: '2024-12-31', isUnread: true, isStarred: true, })).toBe(9); }); });