T011-T012
This commit is contained in:
286
apps/web/tests/e2e/smoke.mood-habits.spec.ts
Normal file
286
apps/web/tests/e2e/smoke.mood-habits.spec.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Mood and Habits Integration', () => {
|
||||
test('mood + habits update tile glow and glyphs', async ({ page }) => {
|
||||
// Navigate to the app
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the app to load
|
||||
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i);
|
||||
|
||||
// Look for today's tile or a specific day tile
|
||||
// Assuming there's a grid with clickable day tiles
|
||||
const todayTile = page.locator('[data-testid="day-tile"]').first();
|
||||
await expect(todayTile).toBeVisible();
|
||||
|
||||
// Click on the tile to open the day editor
|
||||
await todayTile.click();
|
||||
|
||||
// Set the mood - assuming there's a mood selector with hue and intensity
|
||||
const moodHueSlider = page.locator('[data-testid="mood-hue-slider"]');
|
||||
const moodIntensitySlider = page.locator('[data-testid="mood-intensity-slider"]');
|
||||
|
||||
if (await moodHueSlider.isVisible()) {
|
||||
// Set hue to around 120 (green)
|
||||
await moodHueSlider.fill('120');
|
||||
|
||||
// Set intensity to 0.7
|
||||
await moodIntensitySlider.fill('0.7');
|
||||
} else {
|
||||
// Alternative: look for mood buttons or other mood input methods
|
||||
const moodSelector = page.locator('[data-testid="mood-selector"]');
|
||||
if (await moodSelector.isVisible()) {
|
||||
await moodSelector.selectOption('happy'); // or similar
|
||||
}
|
||||
}
|
||||
|
||||
// Add positive habits
|
||||
const addPositiveHabitButton = page.locator('[data-testid="add-positive-habit"]');
|
||||
if (await addPositiveHabitButton.isVisible()) {
|
||||
await addPositiveHabitButton.click();
|
||||
|
||||
// Select or enter a positive habit
|
||||
const habitInput = page.locator('[data-testid="habit-input"]');
|
||||
if (await habitInput.isVisible()) {
|
||||
await habitInput.fill('Exercise');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// Add another positive habit
|
||||
await addPositiveHabitButton.click();
|
||||
await habitInput.fill('Meditation');
|
||||
await page.keyboard.press('Enter');
|
||||
} else {
|
||||
// Alternative: look for pre-defined habit checkboxes or buttons
|
||||
const exerciseHabit = page.locator('[data-testid="habit-exercise"]');
|
||||
const meditationHabit = page.locator('[data-testid="habit-meditation"]');
|
||||
|
||||
if (await exerciseHabit.isVisible()) {
|
||||
await exerciseHabit.click();
|
||||
}
|
||||
if (await meditationHabit.isVisible()) {
|
||||
await meditationHabit.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Add negative habits
|
||||
const addNegativeHabitButton = page.locator('[data-testid="add-negative-habit"]');
|
||||
if (await addNegativeHabitButton.isVisible()) {
|
||||
await addNegativeHabitButton.click();
|
||||
|
||||
const habitInput = page.locator('[data-testid="habit-input"]');
|
||||
if (await habitInput.isVisible()) {
|
||||
await habitInput.fill('Procrastination');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
} else {
|
||||
// Alternative: look for pre-defined negative habit checkboxes
|
||||
const procrastinationHabit = page.locator('[data-testid="habit-procrastination"]');
|
||||
if (await procrastinationHabit.isVisible()) {
|
||||
await procrastinationHabit.click();
|
||||
}
|
||||
}
|
||||
|
||||
// Save or close the day editor
|
||||
const saveButton = page.locator('[data-testid="save-day"]');
|
||||
const closeButton = page.locator('[data-testid="close-editor"]');
|
||||
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
} else if (await closeButton.isVisible()) {
|
||||
await closeButton.click();
|
||||
} else {
|
||||
// Click outside the editor to close it
|
||||
await page.click('body');
|
||||
}
|
||||
|
||||
// Verify the tile updates
|
||||
// Check that the tile has the expected visual changes
|
||||
|
||||
// 1. Check that the tile has a glow/luminance based on net score
|
||||
// Since we added 2 positive and 1 negative habit, net score should be +1
|
||||
// This should result in a positive glow
|
||||
const updatedTile = page.locator('[data-testid="day-tile"]').first();
|
||||
|
||||
// Check for CSS properties or data attributes that indicate glow
|
||||
await expect(updatedTile).toHaveAttribute('data-net-score', '1');
|
||||
|
||||
// Or check for specific CSS classes or computed styles
|
||||
const tileElement = await updatedTile.elementHandle();
|
||||
if (tileElement) {
|
||||
const styles = await page.evaluate((el) => {
|
||||
const computed = window.getComputedStyle(el);
|
||||
return {
|
||||
backgroundColor: computed.backgroundColor,
|
||||
boxShadow: computed.boxShadow,
|
||||
filter: computed.filter
|
||||
};
|
||||
}, tileElement);
|
||||
|
||||
// Verify that the tile has some glow effect (box-shadow, filter, or background)
|
||||
expect(
|
||||
styles.boxShadow !== 'none' ||
|
||||
styles.filter !== 'none' ||
|
||||
styles.backgroundColor !== 'rgba(0, 0, 0, 0)'
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
// 2. Check that glyphs are displayed correctly
|
||||
// According to the spec: ticks for positive count, dots for negative count
|
||||
const positiveGlyphs = page.locator('[data-testid="positive-glyphs"]').first();
|
||||
const negativeGlyphs = page.locator('[data-testid="negative-glyphs"]').first();
|
||||
|
||||
// Should have 2 positive glyphs (ticks)
|
||||
if (await positiveGlyphs.isVisible()) {
|
||||
const positiveCount = await positiveGlyphs.locator('[data-testid="tick"]').count();
|
||||
expect(positiveCount).toBe(2);
|
||||
}
|
||||
|
||||
// Should have 1 negative glyph (dot)
|
||||
if (await negativeGlyphs.isVisible()) {
|
||||
const negativeCount = await negativeGlyphs.locator('[data-testid="dot"]').count();
|
||||
expect(negativeCount).toBe(1);
|
||||
}
|
||||
|
||||
// 3. Check that the mood hue is reflected in the tile color
|
||||
// The base hue should be around 120 (green) as we set earlier
|
||||
if (tileElement) {
|
||||
const hueValue = await page.evaluate((el) => {
|
||||
return el.getAttribute('data-mood-hue');
|
||||
}, tileElement);
|
||||
|
||||
expect(parseInt(hueValue || '0')).toBeCloseTo(120, 10);
|
||||
}
|
||||
|
||||
// 4. Verify accessibility - tile should be keyboard navigable and have proper ARIA labels
|
||||
await updatedTile.focus();
|
||||
const ariaLabel = await updatedTile.getAttribute('aria-label');
|
||||
expect(ariaLabel).toContain('mood');
|
||||
expect(ariaLabel).toContain('habit');
|
||||
|
||||
// Verify that the tile can be navigated with keyboard
|
||||
await page.keyboard.press('Tab');
|
||||
// Should move to next tile or next interactive element
|
||||
|
||||
// Test completed - the tile should now have:
|
||||
// - Updated glow/luminance based on net score (+1)
|
||||
// - 2 tick glyphs for positive habits
|
||||
// - 1 dot glyph for negative habit
|
||||
// - Green-ish hue from mood setting
|
||||
// - Proper accessibility attributes
|
||||
});
|
||||
|
||||
test('multiple habit entries affect net score correctly', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Navigate to a day tile
|
||||
const dayTile = page.locator('[data-testid="day-tile"]').first();
|
||||
await dayTile.click();
|
||||
|
||||
// Add multiple positive habits with different weights
|
||||
const addPositiveButton = page.locator('[data-testid="add-positive-habit"]');
|
||||
|
||||
// Add first positive habit (default weight 1)
|
||||
if (await addPositiveButton.isVisible()) {
|
||||
await addPositiveButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Exercise');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Add second positive habit
|
||||
await addPositiveButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Reading');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Add third positive habit
|
||||
await addPositiveButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Healthy Eating');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// Add negative habits
|
||||
const addNegativeButton = page.locator('[data-testid="add-negative-habit"]');
|
||||
if (await addNegativeButton.isVisible()) {
|
||||
await addNegativeButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Social Media');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await addNegativeButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Junk Food');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// Save changes
|
||||
const saveButton = page.locator('[data-testid="save-day"]');
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
}
|
||||
|
||||
// Verify net score: 3 positive - 2 negative = +1
|
||||
await expect(dayTile).toHaveAttribute('data-net-score', '1');
|
||||
|
||||
// Verify glyph counts
|
||||
const positiveGlyphs = page.locator('[data-testid="positive-glyphs"]').first();
|
||||
const negativeGlyphs = page.locator('[data-testid="negative-glyphs"]').first();
|
||||
|
||||
if (await positiveGlyphs.isVisible()) {
|
||||
const positiveCount = await positiveGlyphs.locator('[data-testid="tick"]').count();
|
||||
expect(positiveCount).toBe(3);
|
||||
}
|
||||
|
||||
if (await negativeGlyphs.isVisible()) {
|
||||
const negativeCount = await negativeGlyphs.locator('[data-testid="dot"]').count();
|
||||
expect(negativeCount).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
test('removing habits updates tile correctly', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const dayTile = page.locator('[data-testid="day-tile"]').first();
|
||||
await dayTile.click();
|
||||
|
||||
// Add some habits first
|
||||
const addPositiveButton = page.locator('[data-testid="add-positive-habit"]');
|
||||
if (await addPositiveButton.isVisible()) {
|
||||
await addPositiveButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Exercise');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
const addNegativeButton = page.locator('[data-testid="add-negative-habit"]');
|
||||
if (await addNegativeButton.isVisible()) {
|
||||
await addNegativeButton.click();
|
||||
await page.locator('[data-testid="habit-input"]').fill('Procrastination');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// Remove the negative habit
|
||||
const removeButton = page.locator('[data-testid="remove-habit"]').first();
|
||||
if (await removeButton.isVisible()) {
|
||||
await removeButton.click();
|
||||
}
|
||||
|
||||
// Save changes
|
||||
const saveButton = page.locator('[data-testid="save-day"]');
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
}
|
||||
|
||||
// Verify net score is now just +1 (only positive habit remains)
|
||||
await expect(dayTile).toHaveAttribute('data-net-score', '1');
|
||||
|
||||
// Verify only positive glyphs remain
|
||||
const positiveGlyphs = page.locator('[data-testid="positive-glyphs"]').first();
|
||||
const negativeGlyphs = page.locator('[data-testid="negative-glyphs"]').first();
|
||||
|
||||
if (await positiveGlyphs.isVisible()) {
|
||||
const positiveCount = await positiveGlyphs.locator('[data-testid="tick"]').count();
|
||||
expect(positiveCount).toBe(1);
|
||||
}
|
||||
|
||||
if (await negativeGlyphs.isVisible()) {
|
||||
const negativeCount = await negativeGlyphs.locator('[data-testid="dot"]').count();
|
||||
expect(negativeCount).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
19
packages/viz/package.json
Normal file
19
packages/viz/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@glowtrack/viz",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"description": "Visualization renderer for GlowTrack (Canvas/SVG grid)",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:ui": "vitest"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4",
|
||||
"vitest": "^2.1.1",
|
||||
"@vitest/ui": "^2.1.1",
|
||||
"jsdom": "^25.0.1"
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
28
packages/viz/tsconfig.json
Normal file
28
packages/viz/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["vitest/globals", "jsdom"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"tests/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
9
packages/viz/vitest.config.ts
Normal file
9
packages/viz/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: [],
|
||||
},
|
||||
});
|
||||
123
pnpm-lock.yaml
generated
123
pnpm-lock.yaml
generated
@@ -50,7 +50,7 @@ importers:
|
||||
version: 6.8.0
|
||||
'@testing-library/svelte':
|
||||
specifier: ^5.0.0
|
||||
version: 5.2.8(svelte@4.2.20)(vite@5.4.20)(vitest@2.1.9(jsdom@25.0.1))
|
||||
version: 5.2.8(svelte@4.2.20)(vite@5.4.20)(vitest@2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
@@ -74,7 +74,38 @@ importers:
|
||||
version: 5.4.20
|
||||
vitest:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.9(jsdom@25.0.1)
|
||||
version: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
packages/storage:
|
||||
dependencies:
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.17.1
|
||||
devDependencies:
|
||||
fake-indexeddb:
|
||||
specifier: ^6.0.0
|
||||
version: 6.2.2
|
||||
typescript:
|
||||
specifier: ^5.5.4
|
||||
version: 5.9.2
|
||||
vitest:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
packages/viz:
|
||||
devDependencies:
|
||||
'@vitest/ui':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.9(vitest@2.1.9)
|
||||
jsdom:
|
||||
specifier: ^25.0.1
|
||||
version: 25.0.1
|
||||
typescript:
|
||||
specifier: ^5.5.4
|
||||
version: 5.9.2
|
||||
vitest:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -555,6 +586,11 @@ packages:
|
||||
'@vitest/spy@2.1.9':
|
||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||
|
||||
'@vitest/ui@2.1.9':
|
||||
resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==}
|
||||
peerDependencies:
|
||||
vitest: 2.1.9
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||
|
||||
@@ -575,6 +611,9 @@ packages:
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -906,6 +945,10 @@ packages:
|
||||
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
fake-indexeddb@6.2.2:
|
||||
resolution: {integrity: sha512-SGbf7fzjeHz3+12NO1dYigcYn4ivviaeULV5yY5rdGihBvvgwMds4r4UBbNIUMwkze57KTDm32rq3j1Az8mzEw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -919,9 +962,24 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@@ -1127,6 +1185,9 @@ packages:
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -1325,6 +1386,10 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1418,6 +1483,10 @@ packages:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1619,6 +1688,10 @@ packages:
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinypool@1.1.1:
|
||||
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -2161,13 +2234,13 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
redent: 3.0.0
|
||||
|
||||
'@testing-library/svelte@5.2.8(svelte@4.2.20)(vite@5.4.20)(vitest@2.1.9(jsdom@25.0.1))':
|
||||
'@testing-library/svelte@5.2.8(svelte@4.2.20)(vite@5.4.20)(vitest@2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1))':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
svelte: 4.2.20
|
||||
optionalDependencies:
|
||||
vite: 5.4.20
|
||||
vitest: 2.1.9(jsdom@25.0.1)
|
||||
vitest: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
@@ -2213,6 +2286,17 @@ snapshots:
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/ui@2.1.9(vitest@2.1.9)':
|
||||
dependencies:
|
||||
'@vitest/utils': 2.1.9
|
||||
fflate: 0.8.2
|
||||
flatted: 3.3.3
|
||||
pathe: 1.1.2
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 1.2.0
|
||||
vitest: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
@@ -2234,6 +2318,13 @@ snapshots:
|
||||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ajv@8.17.1:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
@@ -2574,6 +2665,8 @@ snapshots:
|
||||
|
||||
expect-type@1.2.2: {}
|
||||
|
||||
fake-indexeddb@6.2.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
@@ -2588,10 +2681,18 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
@@ -2818,6 +2919,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
@@ -2976,6 +3079,8 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
@@ -3054,6 +3159,8 @@ snapshots:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve@1.22.10:
|
||||
@@ -3294,6 +3401,11 @@ snapshots:
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tinypool@1.1.1: {}
|
||||
|
||||
tinyrainbow@1.2.0: {}
|
||||
@@ -3372,7 +3484,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 5.4.20
|
||||
|
||||
vitest@2.1.9(jsdom@25.0.1):
|
||||
vitest@2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.20)
|
||||
@@ -3395,6 +3507,7 @@ snapshots:
|
||||
vite-node: 2.1.9
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@vitest/ui': 2.1.9(vitest@2.1.9)
|
||||
jsdom: 25.0.1
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
|
||||
@@ -80,7 +80,7 @@ Contract files from /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrac
|
||||
- Expect failure until DB module/migrations implemented
|
||||
- Dependencies: T007
|
||||
|
||||
- [ ] T011 [P] Contract test: renderer API
|
||||
- [X] T011 [P] Contract test: renderer API
|
||||
- Create /home/jawz/Development/Projects/GlowTrack/packages/viz/tests/contract/renderer.spec.ts
|
||||
- Assert renderGrid(container, days, theme, options) exists and draws required layers per /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrack-a-mood/contracts/renderer.md
|
||||
- Expect failure until viz renderer implemented
|
||||
@@ -88,7 +88,7 @@ Contract files from /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrac
|
||||
|
||||
Integration scenarios from quickstart.md → e2e smoke tests [P]
|
||||
|
||||
- [ ] T012 [P] E2E: mood + habits update tile
|
||||
- [X] T012 [P] E2E: mood + habits update tile
|
||||
- Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.mood-habits.spec.ts
|
||||
- Steps: open app → set day mood → add positive+negative habits → tile glow/luminance and glyphs update
|
||||
- Dependencies: T007, T005
|
||||
|
||||
Reference in New Issue
Block a user