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
|
version: 6.8.0
|
||||||
'@testing-library/svelte':
|
'@testing-library/svelte':
|
||||||
specifier: ^5.0.0
|
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:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.21(postcss@8.5.6)
|
version: 10.4.21(postcss@8.5.6)
|
||||||
@@ -74,7 +74,38 @@ importers:
|
|||||||
version: 5.4.20
|
version: 5.4.20
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.1.1
|
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:
|
packages:
|
||||||
|
|
||||||
@@ -555,6 +586,11 @@ packages:
|
|||||||
'@vitest/spy@2.1.9':
|
'@vitest/spy@2.1.9':
|
||||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
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':
|
'@vitest/utils@2.1.9':
|
||||||
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||||
|
|
||||||
@@ -575,6 +611,9 @@ packages:
|
|||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
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:
|
ansi-regex@5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -906,6 +945,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
fake-indexeddb@6.2.2:
|
||||||
|
resolution: {integrity: sha512-SGbf7fzjeHz3+12NO1dYigcYn4ivviaeULV5yY5rdGihBvvgwMds4r4UBbNIUMwkze57KTDm32rq3j1Az8mzEw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
@@ -919,9 +962,24 @@ packages:
|
|||||||
fast-levenshtein@2.0.6:
|
fast-levenshtein@2.0.6:
|
||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
|
fast-uri@3.1.0:
|
||||||
|
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
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:
|
file-entry-cache@6.0.1:
|
||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
@@ -1127,6 +1185,9 @@ packages:
|
|||||||
json-schema-traverse@0.4.1:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
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:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
@@ -1325,6 +1386,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
picomatch@4.0.3:
|
||||||
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pify@2.3.0:
|
pify@2.3.0:
|
||||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1418,6 +1483,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||||
engines: {node: '>=8'}
|
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:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1619,6 +1688,10 @@ packages:
|
|||||||
tinyexec@0.3.2:
|
tinyexec@0.3.2:
|
||||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
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:
|
tinypool@1.1.1:
|
||||||
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@@ -2161,13 +2234,13 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
redent: 3.0.0
|
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:
|
dependencies:
|
||||||
'@testing-library/dom': 10.4.1
|
'@testing-library/dom': 10.4.1
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 5.4.20
|
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': {}
|
'@types/aria-query@5.0.4': {}
|
||||||
|
|
||||||
@@ -2213,6 +2286,17 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tinyspy: 3.0.2
|
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':
|
'@vitest/utils@2.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 2.1.9
|
'@vitest/pretty-format': 2.1.9
|
||||||
@@ -2234,6 +2318,13 @@ snapshots:
|
|||||||
json-schema-traverse: 0.4.1
|
json-schema-traverse: 0.4.1
|
||||||
uri-js: 4.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@5.0.1: {}
|
||||||
|
|
||||||
ansi-regex@6.2.2: {}
|
ansi-regex@6.2.2: {}
|
||||||
@@ -2574,6 +2665,8 @@ snapshots:
|
|||||||
|
|
||||||
expect-type@1.2.2: {}
|
expect-type@1.2.2: {}
|
||||||
|
|
||||||
|
fake-indexeddb@6.2.2: {}
|
||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
@@ -2588,10 +2681,18 @@ snapshots:
|
|||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
|
fast-uri@3.1.0: {}
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
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:
|
file-entry-cache@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.2.0
|
||||||
@@ -2818,6 +2919,8 @@ snapshots:
|
|||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
@@ -2976,6 +3079,8 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
pify@2.3.0: {}
|
pify@2.3.0: {}
|
||||||
|
|
||||||
pirates@4.0.7: {}
|
pirates@4.0.7: {}
|
||||||
@@ -3054,6 +3159,8 @@ snapshots:
|
|||||||
indent-string: 4.0.0
|
indent-string: 4.0.0
|
||||||
strip-indent: 3.0.0
|
strip-indent: 3.0.0
|
||||||
|
|
||||||
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve@1.22.10:
|
resolve@1.22.10:
|
||||||
@@ -3294,6 +3401,11 @@ snapshots:
|
|||||||
|
|
||||||
tinyexec@0.3.2: {}
|
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: {}
|
tinypool@1.1.1: {}
|
||||||
|
|
||||||
tinyrainbow@1.2.0: {}
|
tinyrainbow@1.2.0: {}
|
||||||
@@ -3372,7 +3484,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 5.4.20
|
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:
|
dependencies:
|
||||||
'@vitest/expect': 2.1.9
|
'@vitest/expect': 2.1.9
|
||||||
'@vitest/mocker': 2.1.9(vite@5.4.20)
|
'@vitest/mocker': 2.1.9(vite@5.4.20)
|
||||||
@@ -3395,6 +3507,7 @@ snapshots:
|
|||||||
vite-node: 2.1.9
|
vite-node: 2.1.9
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@vitest/ui': 2.1.9(vitest@2.1.9)
|
||||||
jsdom: 25.0.1
|
jsdom: 25.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ Contract files from /home/jawz/Development/Projects/GlowTrack/specs/001-glowtrac
|
|||||||
- Expect failure until DB module/migrations implemented
|
- Expect failure until DB module/migrations implemented
|
||||||
- Dependencies: T007
|
- 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
|
- 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
|
- 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
|
- 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]
|
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
|
- 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
|
- Steps: open app → set day mood → add positive+negative habits → tile glow/luminance and glyphs update
|
||||||
- Dependencies: T007, T005
|
- Dependencies: T007, T005
|
||||||
|
|||||||
Reference in New Issue
Block a user