From a576830ce5b5399b40577deec6db6ce12880fad3 Mon Sep 17 00:00:00 2001 From: Danilo Reyes Date: Thu, 18 Sep 2025 11:36:21 -0600 Subject: [PATCH] T011-T012 --- apps/web/tests/e2e/smoke.mood-habits.spec.ts | 286 ++++++++++++++++ packages/viz/package.json | 19 ++ packages/viz/tests/contract/renderer.spec.ts | 326 +++++++++++++++++++ packages/viz/tsconfig.json | 28 ++ packages/viz/vitest.config.ts | 9 + pnpm-lock.yaml | 123 ++++++- specs/001-glowtrack-a-mood/tasks.md | 4 +- 7 files changed, 788 insertions(+), 7 deletions(-) create mode 100644 apps/web/tests/e2e/smoke.mood-habits.spec.ts create mode 100644 packages/viz/package.json create mode 100644 packages/viz/tests/contract/renderer.spec.ts create mode 100644 packages/viz/tsconfig.json create mode 100644 packages/viz/vitest.config.ts diff --git a/apps/web/tests/e2e/smoke.mood-habits.spec.ts b/apps/web/tests/e2e/smoke.mood-habits.spec.ts new file mode 100644 index 0000000..66d6dcb --- /dev/null +++ b/apps/web/tests/e2e/smoke.mood-habits.spec.ts @@ -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); + } + }); +}); diff --git a/packages/viz/package.json b/packages/viz/package.json new file mode 100644 index 0000000..260df06 --- /dev/null +++ b/packages/viz/package.json @@ -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" + } +} diff --git a/packages/viz/tests/contract/renderer.spec.ts b/packages/viz/tests/contract/renderer.spec.ts new file mode 100644 index 0000000..10845cb --- /dev/null +++ b/packages/viz/tests/contract/renderer.spec.ts @@ -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; + cssVariables: Record; +} + +interface RenderOptions { + showLegend: boolean; + pngScale: number; +} + +// Expected renderer API interface +interface Renderer { + renderGrid( + container: HTMLElement, + days: DayTile[], + theme: Theme, + options: RenderOptions + ): Promise | void; +} + +describe('Renderer Contract', () => { + let container: HTMLElement; + let mockDays: DayTile[]; + let mockTheme: Theme; + let mockOptions: RenderOptions; + + beforeEach(() => { + // Create a test container + container = document.createElement('div'); + container.style.width = '800px'; + container.style.height = '600px'; + document.body.appendChild(container); + + // Mock data following the data model + mockDays = [ + { + date: '2025-09-18', + mood: { + hue: 120, // Green + intensity: 0.7, + note: 'Good day' + }, + entries: [ + { + id: 'entry-1', + type: 'positive', + habitId: 'habit-1', + label: 'Exercise', + weight: 1, + timestamp: '2025-09-18T08:00:00Z' + }, + { + id: 'entry-2', + type: 'negative', + habitId: 'habit-2', + label: 'Junk food', + weight: 1, + timestamp: '2025-09-18T14:00:00Z' + } + ], + netScore: 0 // 1 positive - 1 negative + }, + { + date: '2025-09-17', + mood: { + hue: 240, // Blue + intensity: 0.5 + }, + entries: [ + { + id: 'entry-3', + type: 'positive', + habitId: 'habit-3', + label: 'Meditation', + weight: 1, + timestamp: '2025-09-17T07:00:00Z' + } + ], + netScore: 1 + } + ]; + + mockTheme = { + palette: { + primary: '#3b82f6', + secondary: '#8b5cf6', + background: '#ffffff', + text: '#1f2937' + }, + cssVariables: { + '--color-mood-base': '#ffffff', + '--color-glow-intensity': '0.8', + '--color-negative-overlay': '#ff000020' + } + }; + + mockOptions = { + showLegend: true, + pngScale: 1.0 + }; + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + it('should have renderGrid function available', async () => { + // This test should fail until the renderer is implemented + let renderer: Renderer; + + try { + // Try to import the renderer module + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + } catch (error) { + expect.fail('Renderer module should exist at packages/viz/src/renderer.ts'); + } + + expect(renderer.renderGrid).toBeDefined(); + expect(typeof renderer.renderGrid).toBe('function'); + }); + + it('should accept correct parameters for renderGrid', async () => { + // This test should fail until the renderer is implemented + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + } catch (error) { + expect.fail('Renderer module should exist'); + } + + // Should not throw when called with correct parameters + expect(() => { + renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + }).not.toThrow(); + }); + + it('should render Canvas element for tiles', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + await renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + + // Should create a Canvas element for tile rendering + const canvas = container.querySelector('canvas'); + expect(canvas).toBeTruthy(); + expect(canvas?.tagName).toBe('CANVAS'); + } catch (error) { + expect.fail('Should render Canvas element for tiles'); + } + }); + + it('should render SVG element for glyphs and overlays', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + await renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + + // Should create an SVG element for glyph overlays + const svg = container.querySelector('svg'); + expect(svg).toBeTruthy(); + expect(svg?.tagName).toBe('SVG'); + } catch (error) { + expect.fail('Should render SVG element for glyphs and overlays'); + } + }); + + it('should apply mood hue to tiles', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + await renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + + // Canvas should be configured to use mood hues + const canvas = container.querySelector('canvas') as HTMLCanvasElement; + expect(canvas).toBeTruthy(); + + // The canvas context should have been used for drawing + // This is a basic check - actual hue application would be tested in integration tests + const ctx = canvas.getContext('2d'); + expect(ctx).toBeTruthy(); + } catch (error) { + expect.fail('Should apply mood hue to tiles'); + } + }); + + it('should render glyph counts for habit entries', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + await renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + + // SVG should contain glyph elements + const svg = container.querySelector('svg'); + expect(svg).toBeTruthy(); + + // Should have glyph elements for positive (ticks) and negative (dots) entries + // This is a structural test - actual glyph rendering would be tested visually + } catch (error) { + expect.fail('Should render glyph counts for habit entries'); + } + }); + + it('should support keyboard accessibility', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + await renderer.renderGrid(container, mockDays, mockTheme, mockOptions); + + // Should have focusable elements for keyboard navigation + const focusableElements = container.querySelectorAll('[tabindex]'); + expect(focusableElements.length).toBeGreaterThan(0); + + // Should have ARIA labels for screen readers + const ariaElements = container.querySelectorAll('[aria-label]'); + expect(ariaElements.length).toBeGreaterThan(0); + } catch (error) { + expect.fail('Should support keyboard accessibility'); + } + }); + + it('should handle empty days array', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + // Should not throw with empty days + expect(() => { + renderer.renderGrid(container, [], mockTheme, mockOptions); + }).not.toThrow(); + } catch (error) { + expect.fail('Should handle empty days array'); + } + }); + + it('should respect pngScale option for export', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + const exportOptions = { ...mockOptions, pngScale: 2.0 }; + + // Should handle different pngScale values + expect(() => { + renderer.renderGrid(container, mockDays, mockTheme, exportOptions); + }).not.toThrow(); + } catch (error) { + expect.fail('Should respect pngScale option for export'); + } + }); + + it('should apply luminance curve based on netScore', async () => { + let renderer: Renderer; + + try { + const rendererModule = await import('../../src/renderer.js'); + renderer = rendererModule; + + // Test with days having different netScores + const daysWithVariedScores: DayTile[] = [ + { ...mockDays[0], netScore: -2 }, // Should be dimmer + { ...mockDays[1], netScore: 3 } // Should be brighter + ]; + + await renderer.renderGrid(container, daysWithVariedScores, mockTheme, mockOptions); + + // Canvas should reflect luminance differences based on netScore + const canvas = container.querySelector('canvas'); + expect(canvas).toBeTruthy(); + } catch (error) { + expect.fail('Should apply luminance curve based on netScore'); + } + }); +}); diff --git a/packages/viz/tsconfig.json b/packages/viz/tsconfig.json new file mode 100644 index 0000000..8257740 --- /dev/null +++ b/packages/viz/tsconfig.json @@ -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" + ] +} diff --git a/packages/viz/vitest.config.ts b/packages/viz/vitest.config.ts new file mode 100644 index 0000000..4630cd1 --- /dev/null +++ b/packages/viz/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + setupFiles: [], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 570f4b5..53a5929 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/specs/001-glowtrack-a-mood/tasks.md b/specs/001-glowtrack-a-mood/tasks.md index 4979aed..d7f3a5e 100644 --- a/specs/001-glowtrack-a-mood/tasks.md +++ b/specs/001-glowtrack-a-mood/tasks.md @@ -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