T011-T012

This commit is contained in:
2025-09-18 11:36:21 -06:00
parent 530a74147b
commit a576830ce5
7 changed files with 788 additions and 7 deletions

View 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
View 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"
}
}

View 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');
}
});
});

View 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"
]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: [],
},
});

123
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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