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; cssVariables: Record; } interface RenderOptions { showLegend: boolean; pngScale: number; } // Expected renderer API interface interface Renderer { renderGrid( container: HTMLElement, days: DayTile[], theme: Theme, options: RenderOptions ): Promise | 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'); } }); });