test(04-02): filter dropdown tests for project selection, fetch, and empty state

This commit is contained in:
Christopher Mühl 2026-04-08 15:34:31 +02:00
parent 4b87dbf087
commit b50972ac59
No known key found for this signature in database
GPG key ID: 925AC7D69955293F

152
assets/test/filter.test.ts Normal file
View file

@ -0,0 +1,152 @@
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<typeof vi.fn>;
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');
});
});
});