feat(documents): add fetchDensity helper and /documents/+page.ts (#385)

The density data is fetched only on tablet/desktop (sm:+ breakpoint) and
when ?view=calendar is not set — mobile users and the future calendar view
(#386) skip the request entirely. Lives in +page.ts (client-side) so the
matchMedia gate can run in the browser; +page.server.ts continues to handle
the document search.

Non-ok responses and network failures degrade to an empty bucket list
rather than throwing, so the document list keeps rendering.

5 unit tests cover the gating + graceful degradation paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-07 22:06:57 +02:00
parent 5fdcc95c3d
commit ad82f2e1e2
3 changed files with 103 additions and 2 deletions

View File

@@ -1,9 +1,10 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import {
monthBoundaryFrom,
monthBoundaryTo,
buildMonthSequence,
fillDensityGaps
fillDensityGaps,
fetchDensity
} from './timeline';
describe('monthBoundaryFrom', () => {
@@ -106,3 +107,57 @@ describe('fillDensityGaps', () => {
expect(result.map((b) => b.month)).toEqual(['1915-08', '1915-09', '1915-10']);
});
});
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('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 });
});
});