178 lines
6.7 KiB
TypeScript
178 lines
6.7 KiB
TypeScript
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>): 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');
|
|
});
|
|
});
|