From 7fba98f89d2785ecba420fc0b343b7ca1c2e30c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=BChl?= Date: Thu, 9 Apr 2026 16:30:42 +0200 Subject: [PATCH] test(07-02): add failing tests for week mode renderer --- assets/test/week.test.ts | 178 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 assets/test/week.test.ts diff --git a/assets/test/week.test.ts b/assets/test/week.test.ts new file mode 100644 index 0000000..efed66e --- /dev/null +++ b/assets/test/week.test.ts @@ -0,0 +1,178 @@ +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'); + }); +});