import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { WeekModeRenderer } from '../src/renderers/week'; import type { RenderContext } from '../src/renderers/types'; import type { HeatmapData } from '../src/types'; import { createInitialState } from '../src/state'; const DEFAULT_CONFIG = { cellSize: 13, cellGap: 2, marginTop: 20, marginLeft: 30, marginBottom: 4 }; function makeCtx(container: HTMLElement, data: HeatmapData, overrides?: Partial): RenderContext { return { container, data, state: createInitialState('monday'), config: DEFAULT_CONFIG, ...overrides, }; } // Known dates: // 2025-01-06 = Monday // 2025-01-07 = Tuesday // 2025-01-08 = Wednesday // 2025-01-13 = Monday (second Monday entry) function makeMockData(): HeatmapData { return { days: [ { date: '2025-01-06', hours: 2.5, count: 3 }, // Monday { date: '2025-01-07', hours: 5.0, count: 5 }, // Tuesday { date: '2025-01-08', hours: 1.0, count: 1 }, // Wednesday { date: '2025-01-13', hours: 3.0, count: 2 }, // Monday (second) ], range: { begin: '2025-01-01', end: '2025-01-31' }, }; } describe('WeekModeRenderer', () => { let container: HTMLDivElement; let renderer: WeekModeRenderer; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); renderer = new WeekModeRenderer(); }); afterEach(() => { renderer.destroy(); document.querySelectorAll('.heatmap-tooltip').forEach(el => el.remove()); container.remove(); }); it('has mode "week"', () => { expect(renderer.mode).toBe('week'); }); it('renders an SVG with exactly 7 rect elements', () => { renderer.render(makeCtx(container, makeMockData())); const svg = container.querySelector('svg.heatmap-svg'); expect(svg).not.toBeNull(); const rects = container.querySelectorAll('rect'); expect(rects.length).toBe(7); }); it('aggregates same-weekday entries (two Mondays summed)', () => { renderer.render(makeCtx(container, makeMockData())); // Monday is index 0 for monday-start. It should have data (2.5+3.0=5.5h) const rects = container.querySelectorAll('rect'); const mondayRect = rects[0]; // First rect = Monday expect(mondayRect.classList.contains('heatmap-empty')).toBe(false); expect(mondayRect.getAttribute('fill')).toBeTruthy(); }); it('marks weekdays with no data as heatmap-empty', () => { renderer.render(makeCtx(container, makeMockData())); // Data on Mon, Tue, Wed only. Thu, Fri, Sat, Sun should be empty = 4 empty const emptyRects = container.querySelectorAll('rect.heatmap-empty'); expect(emptyRects.length).toBe(4); }); it('data weekdays get a fill color (not empty)', () => { renderer.render(makeCtx(container, makeMockData())); const filledRects = container.querySelectorAll('rect:not(.heatmap-empty)'); expect(filledRects.length).toBe(3); // Mon, Tue, Wed filledRects.forEach(rect => { expect(rect.getAttribute('fill')).toBeTruthy(); }); }); it('renders all 7 weekday labels for monday start', () => { renderer.render(makeCtx(container, makeMockData())); const labels = container.querySelectorAll('text.heatmap-label'); expect(labels.length).toBe(7); const texts = Array.from(labels).map(el => el.textContent); expect(texts[0]).toBe('Mon'); expect(texts[6]).toBe('Sun'); }); it('renders labels in sunday-first order when weekStart is sunday', () => { const state = createInitialState('sunday'); renderer.render(makeCtx(container, makeMockData(), { state })); const labels = container.querySelectorAll('text.heatmap-label'); const texts = Array.from(labels).map(el => el.textContent); expect(texts[0]).toBe('Sun'); expect(texts[1]).toBe('Mon'); expect(texts[6]).toBe('Sat'); }); it('shows tooltip with full weekday name and hours on hover', () => { renderer.render(makeCtx(container, makeMockData())); // Hover over Monday rect (index 0) const rect = container.querySelectorAll('rect')[0]; rect.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement; expect(tooltip).not.toBeNull(); expect(tooltip.style.display).toBe('block'); expect(tooltip.innerHTML).toContain('Monday'); expect(tooltip.innerHTML).toContain('5.5h'); expect(tooltip.innerHTML).toContain('5 entries'); }); it('shows count-first tooltip when metric is count', () => { const state = createInitialState('monday'); state.metric = 'count'; renderer.render(makeCtx(container, makeMockData(), { state })); const rect = container.querySelectorAll('rect')[0]; // Monday rect.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement; expect(tooltip.innerHTML).toContain('5 entries'); expect(tooltip.innerHTML).toContain('5.5h'); // Count should come before hours in the text const html = tooltip.innerHTML; expect(html.indexOf('5 entries')).toBeLessThan(html.indexOf('5.5h')); }); it('shows "No tracked time" tooltip for empty weekday', () => { renderer.render(makeCtx(container, makeMockData())); // Thursday is index 3 (monday-start), has no data const rect = container.querySelectorAll('rect')[3]; rect.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); const tooltip = document.body.querySelector('.heatmap-tooltip') as HTMLDivElement; expect(tooltip.innerHTML).toContain('Thursday'); expect(tooltip.innerHTML).toContain('No tracked time'); }); it('destroy() removes tooltip element', () => { renderer.render(makeCtx(container, makeMockData())); expect(document.body.querySelector('.heatmap-tooltip')).not.toBeNull(); renderer.destroy(); expect(document.body.querySelector('.heatmap-tooltip')).toBeNull(); }); it('handles empty data gracefully', () => { const data: HeatmapData = { days: [], range: { begin: '2025-01-01', end: '2025-01-31' }, }; renderer.render(makeCtx(container, data)); const svg = container.querySelector('svg'); expect(svg).toBeNull(); expect(container.textContent).toContain('No tracking data available'); }); it('all rects have heatmap-week-cell class', () => { renderer.render(makeCtx(container, makeMockData())); const rects = container.querySelectorAll('rect.heatmap-week-cell'); expect(rects.length).toBe(7); }); it('rects have rounded corners', () => { renderer.render(makeCtx(container, makeMockData())); const rect = container.querySelector('rect'); expect(rect?.getAttribute('rx')).toBe('2'); expect(rect?.getAttribute('ry')).toBe('2'); }); });