diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index c200e81..b8315f7 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -5,9 +5,9 @@

GlowTrack

Mood & Habit wellbeing grid — SvelteKit scaffold.

-
+
{#each days as _, i} -
+
{/each}
diff --git a/apps/web/tests/e2e/smoke.png-export.spec.ts b/apps/web/tests/e2e/smoke.png-export.spec.ts new file mode 100644 index 0000000..5598aa3 --- /dev/null +++ b/apps/web/tests/e2e/smoke.png-export.spec.ts @@ -0,0 +1,185 @@ +import { test, expect } from '@playwright/test'; + +test.describe('PNG Export', () => { + test('renders month and exports PNG within size/time budget', async ({ page, browserName }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { level: 1 })).toHaveText(/GlowTrack/i); + + // Step 1: Ensure we have a month view rendered with tiles + const gridContainer = page.locator('[data-testid="wellbeing-grid"]'); + await expect(gridContainer).toBeVisible(); + + // Ensure at least 28-31 tiles are visible (month view) + const tiles = page.locator('[data-testid="day-tile"]'); + const tileCount = await tiles.count(); + expect(tileCount).toBeGreaterThanOrEqual(28); // At least a month's worth + + // Step 2: Add some data to a few tiles to make the export meaningful + // This creates visual content that should be captured in PNG + await tiles.first().click(); + + // Set mood if mood controls are available + const hueInput = page.locator('[data-testid="mood-hue-slider"]'); + if (await hueInput.isVisible()) { + await hueInput.fill('180'); // Blue mood + } + + // Add positive habit if controls are available + const addPositive = page.locator('[data-testid="add-positive-habit"]'); + if (await addPositive.isVisible()) { + await addPositive.click(); + } + + // Close any editor modal/overlay + const closeButton = page.locator('[data-testid="close-day-editor"]'); + if (await closeButton.isVisible()) { + await closeButton.click(); + } else { + // Try clicking outside to close + await gridContainer.click({ position: { x: 10, y: 10 } }); + } + + // Step 3: Wait for any visual updates to complete + await page.waitForTimeout(500); + + // Step 4: Trigger PNG export + const exportButton = page.locator('[data-testid="export-png-button"]'); + + // Start timing the export operation + const startTime = Date.now(); + + // Handle the download that should be triggered by PNG export + const downloadPromise = page.waitForEvent('download', { timeout: 10000 }); + + if (await exportButton.isVisible()) { + await exportButton.click(); + + // Wait for download to complete + const download = await downloadPromise; + const endTime = Date.now(); + const exportDuration = endTime - startTime; + + // Step 5: Validate the PNG export meets budgets + + // Time budget: Export should complete within 5 seconds for a month view + expect(exportDuration).toBeLessThan(5000); + + // Size budget: Get the download and check file size + const path = await download.path(); + if (path) { + const fs = await import('fs'); + const stats = fs.statSync(path); + + // Size budget: PNG should be reasonable size (not too small, not too large) + // Minimum: 1KB (should have actual content) + // Maximum: 5MB (should be reasonable for a month grid) + expect(stats.size).toBeGreaterThan(1024); // > 1KB + expect(stats.size).toBeLessThan(5 * 1024 * 1024); // < 5MB + + // Verify it's actually a PNG file by checking magic bytes + const buffer = fs.readFileSync(path); + const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + expect(buffer.subarray(0, 8)).toEqual(pngSignature); + + // Suggested filename should contain date/timestamp + const suggestedFilename = download.suggestedFilename(); + expect(suggestedFilename).toMatch(/\.png$/i); + expect(suggestedFilename).toMatch(/glowtrack|grid|export/i); + } + } else { + // If export button not yet implemented, we expect this test to fail + // This aligns with TDD approach - test should fail until implementation exists + throw new Error('PNG export button not found - export functionality not yet implemented'); + } + }); + + test('PNG export handles canvas rendering correctly', async ({ page }) => { + await page.goto('/'); + + // This test focuses on the canvas/toBlob functionality specifically + const gridContainer = page.locator('[data-testid="wellbeing-grid"]'); + await expect(gridContainer).toBeVisible(); + + // Check if canvas element is present (renderer should use Canvas for tiles) + const canvas = page.locator('canvas'); + + if (await canvas.count() > 0) { + // Verify canvas has reasonable dimensions for a month grid + const canvasElement = canvas.first(); + const boundingBox = await canvasElement.boundingBox(); + + if (boundingBox) { + expect(boundingBox.width).toBeGreaterThan(200); // Reasonable minimum width + expect(boundingBox.height).toBeGreaterThan(100); // Reasonable minimum height + + // Verify canvas has actual content (not blank) + // This is a proxy test - actual implementation would use toBlob + const canvasData = await page.evaluate(() => { + const canvas = document.querySelector('canvas') as HTMLCanvasElement; + if (!canvas) return null; + + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + // Check if canvas has any non-transparent pixels + let hasContent = false; + for (let i = 3; i < data.length; i += 4) { // Check alpha channel + if (data[i] > 0) { + hasContent = true; + break; + } + } + + return { + width: canvas.width, + height: canvas.height, + hasContent + }; + }); + + if (canvasData) { + expect(canvasData.hasContent).toBe(true); + } + } + } else { + // Canvas not yet implemented - this is expected in TDD approach + console.log('Canvas element not found - renderer not yet implemented'); + } + }); + + test('PNG export respects screen resolution and quality settings', async ({ page }) => { + await page.goto('/'); + + const gridContainer = page.locator('[data-testid="wellbeing-grid"]'); + await expect(gridContainer).toBeVisible(); + + // Test different export quality settings if available + const qualitySelector = page.locator('[data-testid="export-quality-selector"]'); + const exportButton = page.locator('[data-testid="export-png-button"]'); + + if (await qualitySelector.isVisible() && await exportButton.isVisible()) { + // Test high quality export + await qualitySelector.selectOption('high'); + + const downloadPromise = page.waitForEvent('download', { timeout: 10000 }); + await exportButton.click(); + + const download = await downloadPromise; + const path = await download.path(); + + if (path) { + const fs = await import('fs'); + const stats = fs.statSync(path); + + // High quality should produce larger files + expect(stats.size).toBeGreaterThan(2048); // > 2KB for high quality + } + } else { + // Export quality controls not yet implemented + console.log('Export quality controls not found - advanced export options not yet implemented'); + } + }); +}); diff --git a/specs/001-glowtrack-a-mood/tasks.md b/specs/001-glowtrack-a-mood/tasks.md index 274c9fb..692c378 100644 --- a/specs/001-glowtrack-a-mood/tasks.md +++ b/specs/001-glowtrack-a-mood/tasks.md @@ -98,7 +98,7 @@ Integration scenarios from quickstart.md → e2e smoke tests [P] - Steps: create few days → export JSON → clear DB → import JSON → grid identical - Dependencies: T007, T005 -- [ ] T014 [P] E2E: PNG export at screen resolution +- [X] T014 [P] E2E: PNG export at screen resolution - Create /home/jawz/Development/Projects/GlowTrack/apps/web/tests/e2e/smoke.png-export.spec.ts - Steps: render month → export PNG (toBlob) → file within size/time budget - Dependencies: T007, T005