import { describe, it, expect, beforeEach } from 'vitest'; import { renderHeatmap } from '../src/heatmap'; import type { HeatmapData } from '../src/types'; function makeMockData(overrides: Partial = {}): HeatmapData { return { days: [ { date: '2025-01-06', hours: 2.5, count: 3 }, { date: '2025-01-07', hours: 5.0, count: 5 }, { date: '2025-01-08', hours: 1.0, count: 1 }, { date: '2025-01-13', hours: 8.0, count: 4 }, { date: '2025-01-20', hours: 3.5, count: 2 }, ], range: { begin: '2025-01-01', end: '2025-01-31', }, ...overrides, }; } describe('renderHeatmap', () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); it('renders an SVG element', () => { renderHeatmap(container, makeMockData()); const svg = container.querySelector('svg'); expect(svg).not.toBeNull(); }); it('renders correct number of rect elements for date range', () => { renderHeatmap(container, makeMockData()); const rects = container.querySelectorAll('rect.heatmap-cell'); // Jan 1 to Jan 31 = 31 days expect(rects.length).toBe(31); }); it('applies heatmap-empty class to cells with no data', () => { renderHeatmap(container, makeMockData()); const emptyRects = container.querySelectorAll('rect.heatmap-empty'); // 31 days total, 5 with data = 26 empty expect(emptyRects.length).toBe(26); }); it('applies fill attribute to cells with data', () => { renderHeatmap(container, makeMockData()); const allRects = container.querySelectorAll('rect.heatmap-cell:not(.heatmap-empty)'); expect(allRects.length).toBe(5); allRects.forEach((rect) => { expect(rect.getAttribute('fill')).toBeTruthy(); }); }); it('renders day labels (Mon, Wed, Fri)', () => { renderHeatmap(container, makeMockData()); const dayLabels = container.querySelectorAll('text.day-label'); expect(dayLabels.length).toBe(7); // all 7 slots rendered const texts = Array.from(dayLabels).map((el) => el.textContent); expect(texts).toContain('Mon'); expect(texts).toContain('Wed'); expect(texts).toContain('Fri'); }); it('renders month labels', () => { // Use a range spanning two months const data = makeMockData({ range: { begin: '2025-01-01', end: '2025-02-28' }, days: [ { date: '2025-01-15', hours: 3.0, count: 2 }, { date: '2025-02-10', hours: 4.0, count: 1 }, ], }); renderHeatmap(container, data); const monthLabels = container.querySelectorAll('text.month-label'); expect(monthLabels.length).toBeGreaterThan(0); const texts = Array.from(monthLabels).map((el) => el.textContent); expect(texts).toContain('Feb'); }); it('creates tooltip on mouseenter', () => { renderHeatmap(container, makeMockData()); const rect = container.querySelector( 'rect.heatmap-cell:not(.heatmap-empty)', ); expect(rect).not.toBeNull(); // Simulate mouseenter const event = new MouseEvent('mouseenter', { bubbles: true }); // jsdom getBoundingClientRect returns zeros, which is fine for structure test rect!.dispatchEvent(event); const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement; expect(tooltip).not.toBeNull(); expect(tooltip.style.display).toBe('block'); expect(tooltip.innerHTML).toContain('h'); expect(tooltip.innerHTML).toContain('entries'); }); it('hides tooltip on mouseleave', () => { renderHeatmap(container, makeMockData()); const rect = container.querySelector( 'rect.heatmap-cell:not(.heatmap-empty)', ); rect!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); rect!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement; expect(tooltip.style.display).toBe('none'); }); it('handles empty days array gracefully', () => { renderHeatmap(container, makeMockData({ days: [] })); const svg = container.querySelector('svg'); expect(svg).toBeNull(); expect(container.textContent).toContain('No tracking data available'); }); it('only renders cells within the begin/end range', () => { const data: HeatmapData = { days: [ { date: '2025-03-01', hours: 2.0, count: 1 }, { date: '2025-03-05', hours: 4.0, count: 2 }, ], range: { begin: '2025-03-01', end: '2025-03-07' }, }; renderHeatmap(container, data); const rects = container.querySelectorAll('rect.heatmap-cell'); expect(rects.length).toBe(7); }); });