test(04-02): click navigation tests for cell callback and init URL construction
This commit is contained in:
parent
fbe64d1e0b
commit
4b87dbf087
1 changed files with 144 additions and 0 deletions
144
assets/test/interaction.test.ts
Normal file
144
assets/test/interaction.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHeatmap, 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' },
|
||||
};
|
||||
|
||||
describe('click navigation', () => {
|
||||
let container: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('calls onCellClick with dateStr when a data cell is clicked', () => {
|
||||
const onClick = vi.fn();
|
||||
renderHeatmap(container, MOCK_DATA, undefined, onClick);
|
||||
const cell = container.querySelector('rect.heatmap-cell:not(.heatmap-empty)') as SVGRectElement;
|
||||
cell.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClick).toHaveBeenCalledOnce();
|
||||
expect(onClick.mock.calls[0][0]).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it('calls onCellClick when an empty cell is clicked', () => {
|
||||
const onClick = vi.fn();
|
||||
renderHeatmap(container, MOCK_DATA, undefined, onClick);
|
||||
const emptyCell = container.querySelector('rect.heatmap-empty') as SVGRectElement;
|
||||
emptyCell.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClick).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not throw when no onCellClick provided', () => {
|
||||
renderHeatmap(container, MOCK_DATA);
|
||||
const cell = container.querySelector('rect.heatmap-cell') as SVGRectElement;
|
||||
expect(() => cell.dispatchEvent(new MouseEvent('click', { bubbles: true }))).not.toThrow();
|
||||
});
|
||||
|
||||
it('all cells have heatmap-cell class for cursor styling', () => {
|
||||
renderHeatmap(container, MOCK_DATA);
|
||||
const allRects = container.querySelectorAll('rect.heatmap-cell');
|
||||
expect(allRects.length).toBeGreaterThan(0);
|
||||
allRects.forEach((rect) => {
|
||||
expect(rect.classList.contains('heatmap-cell')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('init click navigation', () => {
|
||||
let container: HTMLDivElement;
|
||||
let locationHref: string;
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.setAttribute('data-url', '/heatmap/data');
|
||||
container.setAttribute('data-timesheet-url', '/en/timesheet/');
|
||||
container.setAttribute('data-projects', '[]');
|
||||
document.body.appendChild(container);
|
||||
|
||||
locationHref = '';
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...originalLocation,
|
||||
get href() { return locationHref; },
|
||||
set href(v: string) { locationHref = v; },
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(MOCK_DATA),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('navigates to timesheet URL with daterange on cell click', async () => {
|
||||
init(container);
|
||||
await vi.waitFor(() => {
|
||||
expect(container.querySelector('rect.heatmap-cell')).not.toBeNull();
|
||||
});
|
||||
const cell = container.querySelector('rect.heatmap-cell:not(.heatmap-empty)') as SVGRectElement;
|
||||
cell.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(locationHref).toContain('/en/timesheet/?daterange=');
|
||||
expect(locationHref).toMatch(/daterange=.*\d{4}-\d{2}-\d{2}.*-.*\d{4}-\d{2}-\d{2}/);
|
||||
});
|
||||
|
||||
it('includes project filter in navigation URL when active', async () => {
|
||||
container.setAttribute('data-projects', JSON.stringify([{ id: 5, name: 'Test Project' }]));
|
||||
init(container);
|
||||
await vi.waitFor(() => {
|
||||
expect(container.querySelector('rect.heatmap-cell')).not.toBeNull();
|
||||
});
|
||||
|
||||
// Select the project in the dropdown
|
||||
const select = container.querySelector('select.form-select') as HTMLSelectElement;
|
||||
select.value = '5';
|
||||
select.dispatchEvent(new Event('change'));
|
||||
|
||||
// Wait for re-render after filter fetch
|
||||
await vi.waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(container.querySelector('rect.heatmap-cell')).not.toBeNull();
|
||||
});
|
||||
|
||||
const cell = container.querySelector('rect.heatmap-cell:not(.heatmap-empty)') as SVGRectElement;
|
||||
cell.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(locationHref).toContain('projects[]=5');
|
||||
});
|
||||
|
||||
it('falls back to /en/timesheet/ without data-timesheet-url', async () => {
|
||||
container.removeAttribute('data-timesheet-url');
|
||||
init(container);
|
||||
await vi.waitFor(() => {
|
||||
expect(container.querySelector('rect.heatmap-cell')).not.toBeNull();
|
||||
});
|
||||
const cell = container.querySelector('rect.heatmap-cell:not(.heatmap-empty)') as SVGRectElement;
|
||||
cell.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(locationHref).toContain('/en/timesheet/');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue