import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { init } from '../src/heatmap'; import type { HeatmapData } from '../src/types'; const MOCK_DATA: HeatmapData = { days: [ { date: '2025-01-06', hours: 2.5, count: 3 }, { date: '2025-01-07', hours: 5.0, count: 5 }, ], range: { begin: '2025-01-01', end: '2025-01-14' }, }; const EMPTY_DATA: HeatmapData = { days: [], range: { begin: '2025-01-01', end: '2025-01-14' }, }; const PROJECTS = [ { id: 1, name: 'Alpha' }, { id: 2, name: 'Beta' }, ]; describe('filter dropdown', () => { let container: HTMLDivElement; let fetchMock: ReturnType; beforeEach(() => { container = document.createElement('div'); container.setAttribute('data-url', '/heatmap/data'); container.setAttribute('data-timesheet-url', '/en/timesheet/'); container.setAttribute('data-projects', JSON.stringify(PROJECTS)); document.body.appendChild(container); fetchMock = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(MOCK_DATA), }); vi.stubGlobal('fetch', fetchMock); }); afterEach(() => { document.body.removeChild(container); vi.restoreAllMocks(); }); it('renders select with form-select class when projects exist', () => { init(container); const select = container.querySelector('select.form-select'); expect(select).not.toBeNull(); }); it('does not render select when no projects', () => { container.setAttribute('data-projects', '[]'); init(container); const select = container.querySelector('select.form-select'); expect(select).toBeNull(); }); it('has "All Projects" as first option', () => { init(container); const options = container.querySelectorAll('select.form-select option'); expect(options[0].textContent).toBe('All Projects'); expect((options[0] as HTMLOptionElement).value).toBe(''); }); it('has one option per project', () => { init(container); const options = container.querySelectorAll('select.form-select option'); expect(options.length).toBe(3); // All Projects + 2 projects expect((options[1] as HTMLOptionElement).value).toBe('1'); expect(options[1].textContent).toBe('Alpha'); expect((options[2] as HTMLOptionElement).value).toBe('2'); expect(options[2].textContent).toBe('Beta'); }); it('has aria-label for accessibility', () => { init(container); const select = container.querySelector('select.form-select'); expect(select?.getAttribute('aria-label')).toBe('Filter by project'); }); it('fetches with project param on selection', async () => { init(container); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledTimes(1); }); const select = container.querySelector('select.form-select') as HTMLSelectElement; select.value = '1'; select.dispatchEvent(new Event('change')); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledTimes(2); }); expect(fetchMock.mock.calls[1][0]).toBe('/heatmap/data?project=1'); }); it('fetches without project param for All Projects', async () => { init(container); await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); const select = container.querySelector('select.form-select') as HTMLSelectElement; select.value = '1'; select.dispatchEvent(new Event('change')); await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)); select.value = ''; select.dispatchEvent(new Event('change')); await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(3)); expect(fetchMock.mock.calls[2][0]).toBe('/heatmap/data'); }); it('re-renders heatmap after filter change', async () => { init(container); await vi.waitFor(() => { expect(container.querySelector('rect.heatmap-cell')).not.toBeNull(); }); const select = container.querySelector('select.form-select') as HTMLSelectElement; select.value = '1'; select.dispatchEvent(new Event('change')); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledTimes(2); }); await vi.waitFor(() => { expect(container.querySelector('rect.heatmap-cell')).not.toBeNull(); }); }); it('shows filtered empty message when no data for project', async () => { fetchMock .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_DATA) }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(EMPTY_DATA) }); init(container); await vi.waitFor(() => { expect(container.querySelector('rect.heatmap-cell')).not.toBeNull(); }); const select = container.querySelector('select.form-select') as HTMLSelectElement; select.value = '1'; select.dispatchEvent(new Event('change')); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledTimes(2); }); await vi.waitFor(() => { expect(container.textContent).toContain('No tracking data for this project'); }); }); });