327 lines
9.0 KiB
TypeScript
327 lines
9.0 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
// Types based on data-model.md
|
|
interface ContainerSize {
|
|
width: number;
|
|
height: number;
|
|
devicePixelRatio: number;
|
|
}
|
|
|
|
interface Mood {
|
|
hue: number; // 0-360
|
|
intensity: number; // 0-1
|
|
note?: string;
|
|
}
|
|
|
|
interface HabitEntry {
|
|
id: string;
|
|
type: 'positive' | 'negative';
|
|
habitId: string;
|
|
label: string;
|
|
weight: number;
|
|
timestamp: string; // ISO datetime
|
|
}
|
|
|
|
interface DayTile {
|
|
date: string; // ISO date (YYYY-MM-DD)
|
|
mood: Mood;
|
|
entries: HabitEntry[];
|
|
netScore: number;
|
|
}
|
|
|
|
interface Theme {
|
|
palette: Record<string, string>;
|
|
cssVariables: Record<string, string>;
|
|
}
|
|
|
|
interface RenderOptions {
|
|
showLegend: boolean;
|
|
pngScale: number;
|
|
}
|
|
|
|
// Expected renderer API interface
|
|
interface Renderer {
|
|
renderGrid(
|
|
container: HTMLElement,
|
|
days: DayTile[],
|
|
theme: Theme,
|
|
options: RenderOptions
|
|
): Promise<void> | void;
|
|
}
|
|
|
|
describe('Renderer Contract', () => {
|
|
let container: HTMLElement;
|
|
let mockDays: DayTile[];
|
|
let mockTheme: Theme;
|
|
let mockOptions: RenderOptions;
|
|
|
|
beforeEach(() => {
|
|
// Create a test container
|
|
container = document.createElement('div');
|
|
container.style.width = '800px';
|
|
container.style.height = '600px';
|
|
document.body.appendChild(container);
|
|
|
|
// Mock data following the data model
|
|
mockDays = [
|
|
{
|
|
date: '2025-09-18',
|
|
mood: {
|
|
hue: 120, // Green
|
|
intensity: 0.7,
|
|
note: 'Good day'
|
|
},
|
|
entries: [
|
|
{
|
|
id: 'entry-1',
|
|
type: 'positive',
|
|
habitId: 'habit-1',
|
|
label: 'Exercise',
|
|
weight: 1,
|
|
timestamp: '2025-09-18T08:00:00Z'
|
|
},
|
|
{
|
|
id: 'entry-2',
|
|
type: 'negative',
|
|
habitId: 'habit-2',
|
|
label: 'Junk food',
|
|
weight: 1,
|
|
timestamp: '2025-09-18T14:00:00Z'
|
|
}
|
|
],
|
|
netScore: 0 // 1 positive - 1 negative
|
|
},
|
|
{
|
|
date: '2025-09-17',
|
|
mood: {
|
|
hue: 240, // Blue
|
|
intensity: 0.5
|
|
},
|
|
entries: [
|
|
{
|
|
id: 'entry-3',
|
|
type: 'positive',
|
|
habitId: 'habit-3',
|
|
label: 'Meditation',
|
|
weight: 1,
|
|
timestamp: '2025-09-17T07:00:00Z'
|
|
}
|
|
],
|
|
netScore: 1
|
|
}
|
|
];
|
|
|
|
mockTheme = {
|
|
palette: {
|
|
primary: '#3b82f6',
|
|
secondary: '#8b5cf6',
|
|
background: '#ffffff',
|
|
text: '#1f2937'
|
|
},
|
|
cssVariables: {
|
|
'--color-mood-base': '#ffffff',
|
|
'--color-glow-intensity': '0.8',
|
|
'--color-negative-overlay': '#ff000020'
|
|
}
|
|
};
|
|
|
|
mockOptions = {
|
|
showLegend: true,
|
|
pngScale: 1.0
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.removeChild(container);
|
|
});
|
|
|
|
it('should have renderGrid function available', async () => {
|
|
// This test should fail until the renderer is implemented
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
// Try to import the renderer module
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
} catch (error) {
|
|
expect.fail('Renderer module should exist at packages/viz/src/renderer.ts');
|
|
}
|
|
|
|
expect(renderer.renderGrid).toBeDefined();
|
|
expect(typeof renderer.renderGrid).toBe('function');
|
|
});
|
|
|
|
it('should accept correct parameters for renderGrid', async () => {
|
|
// This test should fail until the renderer is implemented
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
} catch (error) {
|
|
expect.fail('Renderer module should exist');
|
|
}
|
|
|
|
// Should not throw when called with correct parameters
|
|
expect(() => {
|
|
renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should render Canvas element for tiles', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
await renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
|
|
// Should create a Canvas element for tile rendering
|
|
const canvas = container.querySelector('canvas');
|
|
expect(canvas).toBeTruthy();
|
|
expect(canvas?.tagName).toBe('CANVAS');
|
|
} catch (error) {
|
|
expect.fail('Should render Canvas element for tiles');
|
|
}
|
|
});
|
|
|
|
it('should render SVG element for glyphs and overlays', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
await renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
|
|
// Should create an SVG element for glyph overlays
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeTruthy();
|
|
expect(svg?.tagName).toBe('SVG');
|
|
} catch (error) {
|
|
expect.fail('Should render SVG element for glyphs and overlays');
|
|
}
|
|
});
|
|
|
|
it('should apply mood hue to tiles', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
await renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
|
|
// Canvas should be configured to use mood hues
|
|
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
|
|
expect(canvas).toBeTruthy();
|
|
|
|
// The canvas context should have been used for drawing
|
|
// This is a basic check - actual hue application would be tested in integration tests
|
|
const ctx = canvas.getContext('2d');
|
|
expect(ctx).toBeTruthy();
|
|
} catch (error) {
|
|
expect.fail('Should apply mood hue to tiles');
|
|
}
|
|
});
|
|
|
|
it('should render glyph counts for habit entries', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
await renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
|
|
// SVG should contain glyph elements
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeTruthy();
|
|
|
|
// Should have glyph elements for positive (ticks) and negative (dots) entries
|
|
// This is a structural test - actual glyph rendering would be tested visually
|
|
} catch (error) {
|
|
expect.fail('Should render glyph counts for habit entries');
|
|
}
|
|
});
|
|
|
|
it('should support keyboard accessibility', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
await renderer.renderGrid(container, mockDays, mockTheme, mockOptions);
|
|
|
|
// Should have focusable elements for keyboard navigation
|
|
const focusableElements = container.querySelectorAll('[tabindex]');
|
|
expect(focusableElements.length).toBeGreaterThan(0);
|
|
|
|
// Should have ARIA labels for screen readers
|
|
const ariaElements = container.querySelectorAll('[aria-label]');
|
|
expect(ariaElements.length).toBeGreaterThan(0);
|
|
} catch (error) {
|
|
expect.fail('Should support keyboard accessibility');
|
|
}
|
|
});
|
|
|
|
it('should handle empty days array', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
// Should not throw with empty days
|
|
expect(() => {
|
|
renderer.renderGrid(container, [], mockTheme, mockOptions);
|
|
}).not.toThrow();
|
|
} catch (error) {
|
|
expect.fail('Should handle empty days array');
|
|
}
|
|
});
|
|
|
|
it('should respect pngScale option for export', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
const exportOptions = { ...mockOptions, pngScale: 2.0 };
|
|
|
|
// Should handle different pngScale values
|
|
expect(() => {
|
|
renderer.renderGrid(container, mockDays, mockTheme, exportOptions);
|
|
}).not.toThrow();
|
|
} catch (error) {
|
|
expect.fail('Should respect pngScale option for export');
|
|
}
|
|
});
|
|
|
|
it('should apply luminance curve based on netScore', async () => {
|
|
let renderer: Renderer;
|
|
|
|
try {
|
|
const rendererModule = await import('../../src/renderer.js');
|
|
renderer = rendererModule;
|
|
|
|
// Test with days having different netScores
|
|
const daysWithVariedScores: DayTile[] = [
|
|
{ ...mockDays[0], netScore: -2 }, // Should be dimmer
|
|
{ ...mockDays[1], netScore: 3 } // Should be brighter
|
|
];
|
|
|
|
await renderer.renderGrid(container, daysWithVariedScores, mockTheme, mockOptions);
|
|
|
|
// Canvas should reflect luminance differences based on netScore
|
|
const canvas = container.querySelector('canvas');
|
|
expect(canvas).toBeTruthy();
|
|
} catch (error) {
|
|
expect.fail('Should apply luminance curve based on netScore');
|
|
}
|
|
});
|
|
});
|