diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..38348ae --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "github": { + "transport": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "" + } + } + } +} diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 044d211..70e61fc 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "index.html", - "revision": "0.v4bl8mgtcdg" + "revision": "0.tf31j3pna0c" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/playwright.config.ts b/playwright.config.ts index e714a1d..361f3e8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,41 +1,41 @@ -import { defineConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from '@playwright/test'; /** * Playwright configuration for screenshot generation * See https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: "./tests", - // Only run screenshot tests - testMatch: "**/screenshots.spec.ts", - // Run tests in parallel - fullyParallel: true, - // Fail the build on CI if you accidentally left test.only in the source code - forbidOnly: !!process.env.CI, - // Retry on CI only - retries: process.env.CI ? 2 : 0, - // Opt out of parallel tests on CI - workers: process.env.CI ? 1 : undefined, - // Reporter to use - reporter: "list", - // Shared settings for all the projects below - use: { - // Base URL to use in actions like `await page.goto('/')` - baseURL: "http://localhost:8080", - // Collect trace when retrying the failed test - trace: "on-first-retry", - // Screenshot on failure - screenshot: "only-on-failure", - }, + testDir: './tests', + // Only run screenshot tests + testMatch: '**/screenshots.spec.ts', + // Run tests in parallel + fullyParallel: true, + // Fail the build on CI if you accidentally left test.only in the source code + forbidOnly: !!process.env.CI, + // Retry on CI only + retries: process.env.CI ? 2 : 0, + // Opt out of parallel tests on CI + workers: process.env.CI ? 1 : undefined, + // Reporter to use + reporter: 'list', + // Shared settings for all the projects below + use: { + // Base URL to use in actions like `await page.goto('/')` + baseURL: 'http://localhost:8080', + // Collect trace when retrying the failed test + trace: 'on-first-retry', + // Screenshot on failure + screenshot: 'only-on-failure' + }, - // Configure projects for major browsers (only chromium needed for screenshots) - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], + // Configure projects for major browsers (only chromium needed for screenshots) + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ] - // Don't start the dev server automatically - // Run `npm run dev` manually before taking screenshots + // Don't start the dev server automatically + // Run `npm run dev` manually before taking screenshots }); diff --git a/public/screenshots/Categories-1920x1080.png b/public/screenshots/Categories-1920x1080.png deleted file mode 100644 index 36082e9..0000000 Binary files a/public/screenshots/Categories-1920x1080.png and /dev/null differ diff --git a/public/screenshots/Categories-750x1334.png b/public/screenshots/Categories-750x1334.png deleted file mode 100644 index b69a333..0000000 Binary files a/public/screenshots/Categories-750x1334.png and /dev/null differ diff --git a/public/screenshots/EnteredTime-1920x1080.png b/public/screenshots/EnteredTime-1920x1080.png deleted file mode 100644 index c111094..0000000 Binary files a/public/screenshots/EnteredTime-1920x1080.png and /dev/null differ diff --git a/public/screenshots/EnteredTime-750x1334.png b/public/screenshots/EnteredTime-750x1334.png deleted file mode 100644 index b4c059f..0000000 Binary files a/public/screenshots/EnteredTime-750x1334.png and /dev/null differ diff --git a/public/screenshots/Settings-1920x1080.png b/public/screenshots/Settings-1920x1080.png deleted file mode 100644 index 6d6f722..0000000 Binary files a/public/screenshots/Settings-1920x1080.png and /dev/null differ diff --git a/public/screenshots/Settings-750x1334.png b/public/screenshots/Settings-750x1334.png deleted file mode 100644 index 217ac89..0000000 Binary files a/public/screenshots/Settings-750x1334.png and /dev/null differ diff --git a/public/screenshots/TimeTrackerPro-1920x1080.png b/public/screenshots/TimeTrackerPro-1920x1080.png deleted file mode 100644 index 2ea85c3..0000000 Binary files a/public/screenshots/TimeTrackerPro-1920x1080.png and /dev/null differ diff --git a/public/screenshots/TimeTrackerPro-750x1334.png b/public/screenshots/TimeTrackerPro-750x1334.png deleted file mode 100644 index e3966be..0000000 Binary files a/public/screenshots/TimeTrackerPro-750x1334.png and /dev/null differ diff --git a/public/screenshots/desktop-1-archive.png b/public/screenshots/desktop-1-archive.png new file mode 100644 index 0000000..2d5c90a Binary files /dev/null and b/public/screenshots/desktop-1-archive.png differ diff --git a/public/screenshots/desktop-1.png b/public/screenshots/desktop-1.png new file mode 100644 index 0000000..8613ce5 Binary files /dev/null and b/public/screenshots/desktop-1.png differ diff --git a/public/screenshots/desktop-2-active.png b/public/screenshots/desktop-2-active.png new file mode 100644 index 0000000..16aa257 Binary files /dev/null and b/public/screenshots/desktop-2-active.png differ diff --git a/public/screenshots/mobile-1.png b/public/screenshots/mobile-1.png new file mode 100644 index 0000000..187cbf3 Binary files /dev/null and b/public/screenshots/mobile-1.png differ diff --git a/public/screenshots/mobile-2-nav.png b/public/screenshots/mobile-2-nav.png new file mode 100644 index 0000000..187cbf3 Binary files /dev/null and b/public/screenshots/mobile-2-nav.png differ diff --git a/public/screenshots/mobile-settings-1.png b/public/screenshots/mobile-settings-1.png new file mode 100644 index 0000000..6c276dd Binary files /dev/null and b/public/screenshots/mobile-settings-1.png differ diff --git a/tests/SCREENSHOTS_README.md b/tests/SCREENSHOTS_README.md index e42f48b..cc8bfe6 100644 --- a/tests/SCREENSHOTS_README.md +++ b/tests/SCREENSHOTS_README.md @@ -5,6 +5,7 @@ Automated screenshot generation for TimeTracker Pro PWA using Playwright. ## 🎯 What This Does This script automatically captures professional screenshots for your PWA in the correct sizes: + - **Desktop:** 1920×1080 (wide format for desktop/laptop displays) - **Mobile:** 750×1334 (narrow format for mobile devices) @@ -35,6 +36,7 @@ npm run screenshots ``` Screenshots will be saved to: + - `public/screenshots/desktop-1.png` (1920×1080) - `public/screenshots/mobile-1.png` (750×1334) - `public/screenshots/desktop-2-active.png` (optional - with active day) @@ -42,11 +44,11 @@ Screenshots will be saved to: ## 📋 Available Commands -| Command | Description | -|---------|-------------| -| `npm run screenshots` | Capture all screenshots (headless) | -| `npm run screenshots:headed` | Capture with visible browser (debugging) | -| `npm run screenshots:install` | Install Playwright browsers | +| Command | Description | +| ----------------------------- | ---------------------------------------- | +| `npm run screenshots` | Capture all screenshots (headless) | +| `npm run screenshots:headed` | Capture with visible browser (debugging) | +| `npm run screenshots:install` | Install Playwright browsers | ## 🎨 Customization @@ -56,16 +58,16 @@ Edit `tests/screenshots.spec.ts` to customize what's captured: ```typescript // Example: Navigate to specific page before screenshot -await page.goto("http://localhost:8080/archive"); +await page.goto('http://localhost:8080/archive'); // Example: Click a button to show specific content await page.click("button:has-text('Start Day')"); // Example: Fill a form -await page.fill("input[name='title']", "Example Task"); +await page.fill("input[name='title']", 'Example Task'); // Example: Wait for element to appear -await page.waitForSelector(".task-item"); +await page.waitForSelector('.task-item'); ``` ### Add More Screenshots @@ -73,18 +75,18 @@ await page.waitForSelector(".task-item"); Add new test cases in `screenshots.spec.ts`: ```typescript -test("capture archive page", async ({ browser }) => { - const context = await browser.newContext(DESKTOP_CONFIG); - const page = await context.newPage(); +test('capture archive page', async ({ browser }) => { + const context = await browser.newContext(DESKTOP_CONFIG); + const page = await context.newPage(); - await page.goto("http://localhost:8080/archive"); - await page.waitForSelector("#root"); + await page.goto('http://localhost:8080/archive'); + await page.waitForSelector('#root'); - await page.screenshot({ - path: path.join(SCREENSHOTS_DIR, "desktop-archive.png"), - }); + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'desktop-archive.png') + }); - await context.close(); + await context.close(); }); ``` @@ -95,55 +97,64 @@ Update the configuration in `screenshots.spec.ts`: ```typescript // Custom desktop size const DESKTOP_CONFIG = { - viewport: { width: 2560, height: 1440 }, // 4K - deviceScaleFactor: 1, + viewport: { width: 2560, height: 1440 }, // 4K + deviceScaleFactor: 1 }; // Custom mobile size const MOBILE_CONFIG = { - viewport: { width: 1080, height: 1920 }, // Full HD mobile - deviceScaleFactor: 2, + viewport: { width: 1080, height: 1920 }, // Full HD mobile + deviceScaleFactor: 2 }; ``` ## 🔧 Troubleshooting ### "Connection refused" or "net::ERR_CONNECTION_REFUSED" + **Problem:** Dev server is not running. **Solution:** Run `npm run dev` in a separate terminal first. ### Screenshots are blank or show loading state + **Problem:** App taking too long to load. **Solution:** Increase timeout in `screenshots.spec.ts`: + ```typescript -await page.waitForSelector("#root", { timeout: 20000 }); // 20 seconds +await page.waitForSelector('#root', { timeout: 20000 }); // 20 seconds ``` ### Want to see what's happening + **Problem:** Headless mode makes it hard to debug. **Solution:** Use headed mode: + ```bash npm run screenshots:headed ``` ### Screenshots include unwanted elements + **Problem:** Test data or UI elements you don't want visible. **Solution:** Hide elements before screenshot: + ```typescript // Hide specific element await page.evaluate(() => { - document.querySelector(".unwanted-element")?.remove(); + document.querySelector('.unwanted-element')?.remove(); }); // Or add CSS to hide it await page.addStyleTag({ - content: ".unwanted-element { display: none !important; }" + content: '.unwanted-element { display: none !important; }' }); ``` ### File size too large + **Problem:** Screenshots are > 1MB. **Solution:** Use image optimization tools after capture: + ```bash # Install optimizer npm install -g sharp-cli @@ -157,6 +168,7 @@ npx sharp -i public/screenshots/desktop-1.png -o public/screenshots/desktop-1.pn Your screenshots meet PWA standards: ### Desktop Screenshots (Wide) + - ✅ **Size:** 1920×1080 - ✅ **Aspect Ratio:** 16:9 - ✅ **Form Factor:** wide @@ -164,6 +176,7 @@ Your screenshots meet PWA standards: - ✅ **Max File Size:** < 1MB recommended ### Mobile Screenshots (Narrow) + - ✅ **Size:** 750×1334 - ✅ **Aspect Ratio:** 9:16 - ✅ **Form Factor:** narrow @@ -173,6 +186,7 @@ Your screenshots meet PWA standards: ## 🎯 Best Practices ### ✅ DO: + - Show real data (realistic tasks, projects) - Capture app in use (active timer, tasks visible) - Use light mode for better visibility @@ -180,6 +194,7 @@ Your screenshots meet PWA standards: - Keep UI clean (no debug tools visible) ### ❌ DON'T: + - Show empty states or placeholders - Include sensitive client data - Capture error messages or warnings @@ -200,26 +215,26 @@ When you update your app's design: ### Capture Specific User Flow ```typescript -test("capture onboarding flow", async ({ browser }) => { - const context = await browser.newContext(DESKTOP_CONFIG); - const page = await context.newPage(); +test('capture onboarding flow', async ({ browser }) => { + const context = await browser.newContext(DESKTOP_CONFIG); + const page = await context.newPage(); - await page.goto("http://localhost:8080"); + await page.goto('http://localhost:8080'); - // Step 1: Homepage - await page.screenshot({ path: "public/screenshots/step-1.png" }); + // Step 1: Homepage + await page.screenshot({ path: 'public/screenshots/step-1.png' }); - // Step 2: Start day - await page.click("button:has-text('Start Day')"); - await page.waitForTimeout(500); - await page.screenshot({ path: "public/screenshots/step-2.png" }); + // Step 2: Start day + await page.click("button:has-text('Start Day')"); + await page.waitForTimeout(500); + await page.screenshot({ path: 'public/screenshots/step-2.png' }); - // Step 3: Create task - await page.click("button:has-text('New Task')"); - await page.waitForTimeout(500); - await page.screenshot({ path: "public/screenshots/step-3.png" }); + // Step 3: Create task + await page.click("button:has-text('New Task')"); + await page.waitForTimeout(500); + await page.screenshot({ path: 'public/screenshots/step-3.png' }); - await context.close(); + await context.close(); }); ``` @@ -228,12 +243,15 @@ test("capture onboarding flow", async ({ browser }) => { ```typescript // Inject mock data before screenshot await page.evaluate(() => { - localStorage.setItem("mock_data", JSON.stringify({ - tasks: [ - { id: 1, title: "Design Homepage", duration: 7200 }, - { id: 2, title: "Code Feature", duration: 5400 } - ] - })); + localStorage.setItem( + 'mock_data', + JSON.stringify({ + tasks: [ + { id: 1, title: 'Design Homepage', duration: 7200 }, + { id: 2, title: 'Code Feature', duration: 5400 } + ] + }) + ); }); await page.reload(); diff --git a/tests/screenshots.spec.ts b/tests/screenshots.spec.ts index fb8f80d..3f604e5 100644 --- a/tests/screenshots.spec.ts +++ b/tests/screenshots.spec.ts @@ -1,5 +1,5 @@ -import { test, devices } from "@playwright/test"; -import path from "path"; +import { test, devices } from '@playwright/test'; +import path from 'path'; /** * PWA Screenshot Generator @@ -16,137 +16,173 @@ import path from "path"; * - Run `npm run dev` in a separate terminal first */ -const BASE_URL = "http://localhost:8080"; -const SCREENSHOTS_DIR = path.join(process.cwd(), "public", "screenshots"); +const BASE_URL = 'http://localhost:8080'; +const ARCHIVE_URL = 'http://localhost:8080/archive'; +const SETTINGS_URL = 'http://localhost:8080/settings'; +const SCREENSHOTS_DIR = path.join(process.cwd(), 'public', 'screenshots'); // Desktop screenshot configuration const DESKTOP_CONFIG = { - viewport: { width: 1920, height: 1080 }, - deviceScaleFactor: 1, + viewport: { width: 1920, height: 1080 }, + deviceScaleFactor: 1 }; // Mobile screenshot configuration (iPhone 6/7/8 Plus equivalent) const MOBILE_CONFIG = { - viewport: { width: 750, height: 1334 }, - deviceScaleFactor: 2, - isMobile: true, - hasTouch: true, + viewport: { width: 750, height: 1334 }, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true }; -test.describe("PWA Screenshots", () => { - test("capture desktop screenshot", async ({ browser }) => { - const context = await browser.newContext(DESKTOP_CONFIG); - const page = await context.newPage(); - - try { - // Navigate to the app - await page.goto(BASE_URL, { waitUntil: "networkidle" }); - - // Wait for app to be fully loaded - await page.waitForSelector("#root", { timeout: 10000 }); - - // Optional: Wait for any animations to complete - await page.waitForTimeout(1000); - - // Take the screenshot - await page.screenshot({ - path: path.join(SCREENSHOTS_DIR, "desktop-1.png"), - fullPage: false, // Capture viewport only - }); - - console.log("✅ Desktop screenshot saved: public/screenshots/desktop-1.png"); - } catch (error) { - console.error("❌ Failed to capture desktop screenshot:", error); - throw error; - } finally { - await context.close(); - } - }); - - test("capture mobile screenshot", async ({ browser }) => { - const context = await browser.newContext(MOBILE_CONFIG); - const page = await context.newPage(); - - try { - // Navigate to the app - await page.goto(BASE_URL, { waitUntil: "networkidle" }); - - // Wait for app to be fully loaded - await page.waitForSelector("#root", { timeout: 10000 }); - - // Optional: Wait for mobile nav to render - await page.waitForTimeout(1000); - - // Take the screenshot - await page.screenshot({ - path: path.join(SCREENSHOTS_DIR, "mobile-1.png"), - fullPage: false, // Capture viewport only - }); - - console.log("✅ Mobile screenshot saved: public/screenshots/mobile-1.png"); - } catch (error) { - console.error("❌ Failed to capture mobile screenshot:", error); - throw error; - } finally { - await context.close(); - } - }); - - test("capture desktop screenshot with active day", async ({ browser }) => { - const context = await browser.newContext(DESKTOP_CONFIG); - const page = await context.newPage(); - - try { - await page.goto(BASE_URL, { waitUntil: "networkidle" }); - await page.waitForSelector("#root", { timeout: 10000 }); - - // Try to start a day if possible (adjust selectors based on your app) - try { - const startButton = page.locator("button:has-text('Start Day')").first(); - if (await startButton.isVisible({ timeout: 2000 })) { - await startButton.click(); - await page.waitForTimeout(1500); - } - } catch (e) { - console.log("Note: Could not start day (may already be started)"); - } - - await page.screenshot({ - path: path.join(SCREENSHOTS_DIR, "desktop-2-active.png"), - fullPage: false, - }); - - console.log("✅ Desktop active screenshot saved: public/screenshots/desktop-2-active.png"); - } catch (error) { - console.error("❌ Failed to capture desktop active screenshot:", error); - // Don't throw - this is optional - } finally { - await context.close(); - } - }); - - test("capture mobile screenshot with bottom nav", async ({ browser }) => { - const context = await browser.newContext(MOBILE_CONFIG); - const page = await context.newPage(); - - try { - await page.goto(BASE_URL, { waitUntil: "networkidle" }); - await page.waitForSelector("#root", { timeout: 10000 }); - - // Wait for mobile nav to be visible - await page.waitForTimeout(1500); - - await page.screenshot({ - path: path.join(SCREENSHOTS_DIR, "mobile-2-nav.png"), - fullPage: false, - }); - - console.log("✅ Mobile nav screenshot saved: public/screenshots/mobile-2-nav.png"); - } catch (error) { - console.error("❌ Failed to capture mobile nav screenshot:", error); - // Don't throw - this is optional - } finally { - await context.close(); - } - }); +test.describe('PWA Screenshots', () => { + test('capture desktop screenshot', async ({ browser }) => { + const context = await browser.newContext(DESKTOP_CONFIG); + const page = await context.newPage(); + + try { + // Navigate to the app + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + + // Wait for app to be fully loaded + await page.waitForSelector('#root', { timeout: 10000 }); + + // Optional: Wait for any animations to complete + await page.waitForTimeout(1000); + + // Take the screenshot + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'desktop-1.png'), + fullPage: false // Capture viewport only + }); + + console.log( + '✅ Desktop screenshot saved: public/screenshots/desktop-1.png' + ); + } catch (error) { + console.error('❌ Failed to capture desktop screenshot:', error); + throw error; + } finally { + await context.close(); + } + }); + + test('capture mobile screenshot', async ({ browser }) => { + const context = await browser.newContext(MOBILE_CONFIG); + const page = await context.newPage(); + + try { + // Navigate to the app + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + + // Wait for app to be fully loaded + await page.waitForSelector('#root', { timeout: 10000 }); + + // Optional: Wait for mobile nav to render + await page.waitForTimeout(1000); + + // Take the screenshot + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'mobile-1.png'), + fullPage: false // Capture viewport only + }); + + console.log( + '✅ Mobile screenshot saved: public/screenshots/mobile-1.png' + ); + } catch (error) { + console.error('❌ Failed to capture mobile screenshot:', error); + throw error; + } finally { + await context.close(); + } + }); + + test('capture desktop screenshot with active day', async ({ browser }) => { + const context = await browser.newContext(DESKTOP_CONFIG); + const page = await context.newPage(); + + try { + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + await page.waitForSelector('#root', { timeout: 10000 }); + + // Try to start a day if possible (adjust selectors based on your app) + try { + const startButton = page + .locator("button:has-text('Start Day')") + .first(); + if (await startButton.isVisible({ timeout: 2000 })) { + await startButton.click(); + await page.waitForTimeout(1500); + } + } catch (e) { + console.log('Note: Could not start day (may already be started)'); + } + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'desktop-2-active.png'), + fullPage: false + }); + + console.log( + '✅ Desktop active screenshot saved: public/screenshots/desktop-2-active.png' + ); + } catch (error) { + console.error('❌ Failed to capture desktop active screenshot:', error); + // Don't throw - this is optional + } finally { + await context.close(); + } + }); + + test('capture desktop screenshot with archives', async ({ browser }) => { + const context = await browser.newContext(DESKTOP_CONFIG); + const page = await context.newPage(); + + try { + await page.goto(ARCHIVE_URL, { waitUntil: 'networkidle' }); + await page.waitForSelector('#root', { timeout: 10000 }); + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'desktop-1-archive.png'), + fullPage: false + }); + + console.log( + '✅ Desktop archive screenshot saved: public/screenshots/desktop-1-archive.png' + ); + } catch (error) { + console.error('❌ Failed to capture desktop archive screenshot:', error); + // Don't throw - this is optional + } finally { + await context.close(); + } + }); + + test('capture mobile screenshot with bottom nav', async ({ browser }) => { + const context = await browser.newContext(MOBILE_CONFIG); + const page = await context.newPage(); + + try { + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); + await page.waitForSelector('#root', { timeout: 10000 }); + + // Wait for mobile nav to be visible + await page.waitForTimeout(1500); + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'mobile-2-nav.png'), + fullPage: false + }); + + console.log( + '✅ Mobile nav screenshot saved: public/screenshots/mobile-2-nav.png' + ); + } catch (error) { + console.error('❌ Failed to capture mobile nav screenshot:', error); + // Don't throw - this is optional + } finally { + await context.close(); + } + }); });