193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { YearModeRenderer } from '../src/renderers/year';
|
|
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,
|
|
};
|
|
}
|
|
|
|
function makeMockData(overrides: Partial<HeatmapData> = {}): 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;
|
|
let renderer: YearModeRenderer;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
renderer = new YearModeRenderer();
|
|
});
|
|
|
|
afterEach(() => {
|
|
renderer.destroy();
|
|
document.querySelectorAll('.heatmap-tooltip').forEach(el => el.remove());
|
|
});
|
|
|
|
it('renders an SVG element', () => {
|
|
renderer.render(makeCtx(container, makeMockData()));
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).not.toBeNull();
|
|
});
|
|
|
|
it('renders correct number of rect elements for date range', () => {
|
|
renderer.render(makeCtx(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', () => {
|
|
renderer.render(makeCtx(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', () => {
|
|
renderer.render(makeCtx(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)', () => {
|
|
renderer.render(makeCtx(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 },
|
|
],
|
|
});
|
|
renderer.render(makeCtx(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', () => {
|
|
renderer.render(makeCtx(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', () => {
|
|
renderer.render(makeCtx(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', () => {
|
|
renderer.render(makeCtx(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' },
|
|
};
|
|
renderer.render(makeCtx(container, data));
|
|
const rects = container.querySelectorAll('rect.heatmap-cell');
|
|
expect(rects.length).toBe(7);
|
|
});
|
|
|
|
it('applies heatmap-weekend class to Saturday and Sunday cells', () => {
|
|
// 2025-01-04 is Saturday, 2025-01-05 is Sunday
|
|
const data: HeatmapData = {
|
|
days: [
|
|
{ date: '2025-01-04', hours: 2.0, count: 1 },
|
|
],
|
|
range: { begin: '2025-01-01', end: '2025-01-07' },
|
|
};
|
|
renderer.render(makeCtx(container, data));
|
|
const weekendRects = container.querySelectorAll('rect.heatmap-weekend');
|
|
// Jan 1 (Wed) through Jan 7 (Tue): Sat Jan 4 + Sun Jan 5 = 2 weekend cells
|
|
expect(weekendRects.length).toBe(2);
|
|
});
|
|
|
|
it('does not apply heatmap-weekend class to weekday cells', () => {
|
|
const data: HeatmapData = {
|
|
days: [
|
|
{ date: '2025-01-06', hours: 2.0, count: 1 },
|
|
],
|
|
range: { begin: '2025-01-06', end: '2025-01-10' },
|
|
};
|
|
renderer.render(makeCtx(container, data));
|
|
const weekendRects = container.querySelectorAll('rect.heatmap-weekend');
|
|
// Mon-Fri, no weekends
|
|
expect(weekendRects.length).toBe(0);
|
|
});
|
|
|
|
it('renders day labels for Sunday week start', () => {
|
|
renderer.render(makeCtx(container, makeMockData(), { state: createInitialState('sunday') }));
|
|
const dayLabels = container.querySelectorAll('text.day-label');
|
|
const texts = Array.from(dayLabels).map((el) => el.textContent);
|
|
expect(texts).toContain('Sun');
|
|
expect(texts).toContain('Tue');
|
|
expect(texts).toContain('Thu');
|
|
});
|
|
});
|