import { describe, it, expect, vi } from 'vitest'; import { fetchDensity, buildDensityUrl } from './timeline'; describe('buildDensityUrl', () => { it('returns the bare endpoint when no filters provided', () => { expect(buildDensityUrl()).toBe('/api/documents/density'); }); it('forwards single-value filters as query params', () => { expect(buildDensityUrl({ q: 'Brief', senderId: 's-1' })).toBe( '/api/documents/density?q=Brief&senderId=s-1' ); }); it('repeats the tag param for multi-value tag filters', () => { const url = buildDensityUrl({ tags: ['Familie', 'Urlaub'], tagOp: 'OR' }); expect(url).toContain('tag=Familie'); expect(url).toContain('tag=Urlaub'); expect(url).toContain('tagOp=OR'); }); it('omits tagOp when it is AND (default on backend)', () => { const url = buildDensityUrl({ tags: ['Familie'], tagOp: 'AND' }); expect(url).not.toContain('tagOp='); }); it('does not forward from/to even if a caller mistakenly adds them', () => { // Intentional: density is the surface for picking from/to, so it must always // span the broader space the user is selecting within. // @ts-expect-error - from/to are explicitly absent from DensityFilters const url = buildDensityUrl({ q: 'Brief', from: '1915-01-01', to: '1916-12-31' }); expect(url).not.toContain('from='); expect(url).not.toContain('to='); }); }); describe('fetchDensity', () => { it('skips fetch and returns null density on mobile', async () => { const fetch = vi.fn(); const result = await fetchDensity(fetch, null, false); expect(fetch).not.toHaveBeenCalled(); expect(result).toEqual({ density: null, minDate: null, maxDate: null }); }); it('skips fetch when view is calendar', async () => { const fetch = vi.fn(); const result = await fetchDensity(fetch, 'calendar', true); expect(fetch).not.toHaveBeenCalled(); expect(result).toEqual({ density: null, minDate: null, maxDate: null }); }); it('calls /api/documents/density and returns body on desktop, list view', async () => { const fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ buckets: [{ month: '1915-08', count: 3 }], minDate: '1915-08-01', maxDate: '1916-12-31' }) }); const result = await fetchDensity(fetch, null, true); expect(fetch).toHaveBeenCalledWith('/api/documents/density'); expect(result.density).toEqual([{ month: '1915-08', count: 3 }]); expect(result.minDate).toBe('1915-08-01'); expect(result.maxDate).toBe('1916-12-31'); }); it('forwards active filters as query params', async () => { const fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ buckets: [], minDate: null, maxDate: null }) }); await fetchDensity(fetch, null, true, { senderId: 's-1', tags: ['Familie'] }); const calledWith = fetch.mock.calls[0][0] as string; expect(calledWith).toContain('senderId=s-1'); expect(calledWith).toContain('tag=Familie'); }); it('returns empty density and null bounds when the API responds non-ok', async () => { const fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 }); const result = await fetchDensity(fetch, null, true); expect(result).toEqual({ density: [], minDate: null, maxDate: null }); }); it('treats fetch rejection as a graceful degradation, not an error', async () => { const fetch = vi.fn().mockRejectedValue(new TypeError('Network down')); const result = await fetchDensity(fetch, null, true); expect(result).toEqual({ density: [], minDate: null, maxDate: null }); }); it('emits console.warn with the status when the response is non-ok', async () => { const fetch = vi.fn().mockResolvedValue({ ok: false, status: 503 }); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); await fetchDensity(fetch, null, true); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls[0][0]).toContain('503'); warn.mockRestore(); }); it('emits console.warn with the caught error when fetch rejects', async () => { const error = new TypeError('Network down'); const fetch = vi.fn().mockRejectedValue(error); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); await fetchDensity(fetch, null, true); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls[0]).toContain(error); warn.mockRestore(); }); });