T014
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
<main class="container">
|
||||
<h1>GlowTrack</h1>
|
||||
<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}
|
||||
<div class="tile" aria-label={`day ${i + 1}`} />
|
||||
<div class="tile" data-testid="day-tile" data-index={i} aria-label={`day ${i + 1}`} />
|
||||
{/each}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
185
apps/web/tests/e2e/smoke.png-export.spec.ts
Normal file
185
apps/web/tests/e2e/smoke.png-export.spec.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user