This commit is contained in:
2025-09-18 11:49:28 -06:00
parent cef846fb0b
commit d3d24223e8
3 changed files with 188 additions and 3 deletions

View File

@@ -5,9 +5,9 @@
<main class="container"> <main class="container">
<h1>GlowTrack</h1> <h1>GlowTrack</h1>
<p class="muted">Mood & Habit wellbeing grid — SvelteKit scaffold.</p> <p class="muted">Mood & Habit wellbeing grid — SvelteKit scaffold.</p>
<section class="grid" aria-label="demo grid"> <section class="grid" data-testid="wellbeing-grid" aria-label="demo grid">
{#each days as _, i} {#each days as _, i}
<div class="tile" aria-label={`day ${i + 1}`} /> <div class="tile" data-testid="day-tile" data-index={i} aria-label={`day ${i + 1}`} />
{/each} {/each}
</section> </section>
</main> </main>

View File

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

View File

@@ -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 - Steps: create few days → export JSON → clear DB → import JSON → grid identical
- Dependencies: T007, T005 - 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 - 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 - Steps: render month → export PNG (toBlob) → file within size/time budget
- Dependencies: T007, T005 - Dependencies: T007, T005