T011-T012
This commit is contained in:
326
packages/viz/tests/contract/renderer.spec.ts
Normal file
326
packages/viz/tests/contract/renderer.spec.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user