kimai-plugin-heatmap/assets/test/week.test.ts

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');
});
});