E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 13s
Ping Search Engines / Notify Search Engines (push) Successful in 3s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 1s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m46s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m31s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m5s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 15m24s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 6s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h12m28s
Uptime Monitor / Health & Response Time (push) Successful in 5s
Uptime Monitor / SSL Certificate (push) Successful in 3s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 2s
259 lines
8.8 KiB
JavaScript
259 lines
8.8 KiB
JavaScript
/**
|
||
* Theme Screenshot Capture Script
|
||
* WorkRoot IT Solutions — Dark/Light Mode Visual Reference
|
||
*
|
||
* Generates full-page screenshots of all major pages in both themes
|
||
* across desktop, tablet, and mobile viewports.
|
||
*
|
||
* Usage:
|
||
* 1. Start dev server: npm run dev
|
||
* 2. Run this script: node scripts/capture-theme-screenshots.js
|
||
*
|
||
* Output: .agents/frontend-specialist/screenshots/{light,dark}/{page}-{viewport}.png
|
||
*
|
||
* Prerequisites:
|
||
* npm install playwright
|
||
* npx playwright install chromium
|
||
*/
|
||
|
||
const { chromium } = require('playwright');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
// ─── Configuration ────────────────────────────────────────────────────────────
|
||
|
||
const BASE_URL = process.env.BASE_URL || 'http://localhost:4321';
|
||
|
||
const PAGES = [
|
||
{ name: 'home', path: '/' },
|
||
{ name: 'about', path: '/about' },
|
||
{ name: 'services', path: '/services' },
|
||
{ name: 'portfolio', path: '/portfolio' },
|
||
{ name: 'contact', path: '/contact' },
|
||
];
|
||
|
||
const VIEWPORTS = [
|
||
{ name: 'desktop', width: 1440, height: 900 },
|
||
{ name: 'tablet', width: 768, height: 1024 },
|
||
{ name: 'mobile', width: 390, height: 844 },
|
||
];
|
||
|
||
const THEMES = ['light', 'dark'];
|
||
|
||
/** Milliseconds to wait after theme switch for transitions to settle */
|
||
const THEME_SETTLE_MS = 300;
|
||
|
||
/** Milliseconds to wait after page load for animations to complete */
|
||
const PAGE_SETTLE_MS = 500;
|
||
|
||
/** Output directory (relative to project root) */
|
||
const OUTPUT_DIR = path.join(
|
||
__dirname,
|
||
'..',
|
||
'.agents',
|
||
'frontend-specialist',
|
||
'screenshots'
|
||
);
|
||
|
||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
function ensureDir(dirPath) {
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
}
|
||
|
||
function screenshotPath(theme, pageName, viewportName) {
|
||
return path.join(OUTPUT_DIR, theme, `${pageName}-${viewportName}.png`);
|
||
}
|
||
|
||
async function waitForPageReady(page) {
|
||
// Wait for network idle + animations to complete
|
||
await page.waitForLoadState('networkidle');
|
||
await page.waitForTimeout(PAGE_SETTLE_MS);
|
||
}
|
||
|
||
async function setTheme(page, theme) {
|
||
await page.evaluate((t) => {
|
||
if (t === 'dark') {
|
||
document.documentElement.classList.add('dark');
|
||
} else {
|
||
document.documentElement.classList.remove('dark');
|
||
}
|
||
try {
|
||
localStorage.setItem('theme', t);
|
||
} catch (_) {
|
||
// localStorage may be unavailable in some contexts
|
||
}
|
||
}, theme);
|
||
// Wait for CSS transitions to settle
|
||
await page.waitForTimeout(THEME_SETTLE_MS);
|
||
}
|
||
|
||
async function captureScreenshot(page, filePath, fullPage = true) {
|
||
await page.screenshot({
|
||
path: filePath,
|
||
fullPage,
|
||
animations: 'disabled', // Freeze CSS/JS animations for clean captures
|
||
});
|
||
const sizeKB = Math.round(fs.statSync(filePath).size / 1024);
|
||
console.log(` ✓ Saved: ${path.relative(process.cwd(), filePath)} (${sizeKB}KB)`);
|
||
}
|
||
|
||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
async function main() {
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════════════════');
|
||
console.log(' WorkRoot Theme Screenshot Capture');
|
||
console.log(` Base URL: ${BASE_URL}`);
|
||
console.log(` Output: ${OUTPUT_DIR}`);
|
||
console.log('═══════════════════════════════════════════════════════════');
|
||
console.log('');
|
||
|
||
// Create output directories
|
||
for (const theme of THEMES) {
|
||
ensureDir(path.join(OUTPUT_DIR, theme));
|
||
}
|
||
|
||
const browser = await chromium.launch({
|
||
headless: true,
|
||
});
|
||
|
||
let totalScreenshots = 0;
|
||
let errors = [];
|
||
|
||
try {
|
||
for (const viewport of VIEWPORTS) {
|
||
console.log(`\n── Viewport: ${viewport.name} (${viewport.width}×${viewport.height}) ──`);
|
||
|
||
const context = await browser.newContext({
|
||
viewport: { width: viewport.width, height: viewport.height },
|
||
deviceScaleFactor: 2, // Retina quality for crisp screenshots
|
||
colorScheme: 'light', // Start light; we override via JS
|
||
});
|
||
|
||
const page = await context.newPage();
|
||
|
||
// Disable animations globally for cleaner screenshots
|
||
await page.addInitScript(() => {
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
*, *::before, *::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
});
|
||
|
||
for (const pageConfig of PAGES) {
|
||
const url = `${BASE_URL}${pageConfig.path}`;
|
||
console.log(`\n Page: ${pageConfig.name} (${url})`);
|
||
|
||
for (const theme of THEMES) {
|
||
try {
|
||
// Navigate to page
|
||
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
||
await waitForPageReady(page);
|
||
|
||
// Set theme
|
||
await setTheme(page, theme);
|
||
|
||
// Capture screenshot
|
||
const filePath = screenshotPath(theme, pageConfig.name, viewport.name);
|
||
await captureScreenshot(page, filePath);
|
||
totalScreenshots++;
|
||
|
||
} catch (err) {
|
||
const label = `${theme}/${pageConfig.name}-${viewport.name}`;
|
||
errors.push({ label, error: err.message });
|
||
console.error(` ✗ Failed ${label}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
await context.close();
|
||
}
|
||
|
||
} finally {
|
||
await browser.close();
|
||
}
|
||
|
||
// Summary
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════════════════');
|
||
console.log(` Done! Captured ${totalScreenshots} screenshots`);
|
||
if (errors.length > 0) {
|
||
console.log(` ⚠ ${errors.length} error(s):`);
|
||
for (const { label, error } of errors) {
|
||
console.log(` • ${label}: ${error}`);
|
||
}
|
||
}
|
||
console.log('═══════════════════════════════════════════════════════════');
|
||
console.log('');
|
||
|
||
// Generate index file listing all screenshots
|
||
generateIndex(totalScreenshots, errors);
|
||
}
|
||
|
||
// ─── Index Generator ──────────────────────────────────────────────────────────
|
||
|
||
function generateIndex(total, errors) {
|
||
const lines = [
|
||
'# Theme Screenshots Index',
|
||
'',
|
||
`Generated: ${new Date().toISOString()}`,
|
||
`Total: ${total} screenshots`,
|
||
errors.length > 0 ? `Errors: ${errors.length}` : 'All captures successful ✓',
|
||
'',
|
||
'## File Listing',
|
||
'',
|
||
];
|
||
|
||
for (const theme of THEMES) {
|
||
lines.push(`### ${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme`);
|
||
lines.push('');
|
||
lines.push('| Page | Desktop | Tablet | Mobile |');
|
||
lines.push('|------|---------|--------|--------|');
|
||
|
||
for (const pageConfig of PAGES) {
|
||
const cells = VIEWPORTS.map((vp) => {
|
||
const file = `${pageConfig.name}-${vp.name}.png`;
|
||
const fullPath = path.join(OUTPUT_DIR, theme, file);
|
||
const exists = fs.existsSync(fullPath);
|
||
if (!exists) return '❌ missing';
|
||
const sizeKB = Math.round(fs.statSync(fullPath).size / 1024);
|
||
return `[${file}](./${theme}/${file}) (${sizeKB}KB)`;
|
||
});
|
||
|
||
lines.push(`| ${pageConfig.name} | ${cells.join(' | ')} |`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
if (errors.length > 0) {
|
||
lines.push('## Errors');
|
||
lines.push('');
|
||
for (const { label, error } of errors) {
|
||
lines.push(`- **${label}**: ${error}`);
|
||
}
|
||
lines.push('');
|
||
}
|
||
|
||
lines.push('---');
|
||
lines.push('*Generated by `scripts/capture-theme-screenshots.js`*');
|
||
|
||
const indexPath = path.join(OUTPUT_DIR, 'INDEX.md');
|
||
fs.writeFileSync(indexPath, lines.join('\n'));
|
||
console.log(` Index written: ${path.relative(process.cwd(), indexPath)}`);
|
||
}
|
||
|
||
// ─── Entry Point ──────────────────────────────────────────────────────────────
|
||
|
||
main().catch((err) => {
|
||
console.error('Fatal error:', err);
|
||
process.exit(1);
|
||
});
|