const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); const TARGET_URL = process.env.TARGET_URL || 'http://127.0.0.1:3001'; const TOKEN = process.env.ADMIN_TOKEN || 'shop_admin_2024_secure_token'; const REPORTS_DIR = process.env.REPORTS_DIR || './reports'; const OUTPUT_DIR = process.env.OUTPUT_DIR || './visual/admin-screenshots'; const PAGES = [ { path: '/', name: 'dashboard' }, { path: '/users', name: 'users' }, { path: '/products', name: 'products' }, { path: '/categories', name: 'categories' }, { path: '/catalog', name: 'catalog' }, { path: '/purchases', name: 'purchases' }, { path: '/audit', name: 'audit' }, { path: '/settings', name: 'settings' }, { path: '/wallets', name: 'wallets' }, { path: '/payment-wallets', name: 'payment-wallets' }, { path: '/seed', name: 'seed' }, { path: '/locales', name: 'locales' }, ]; const VIEWPORTS = { desktop: { width: 1920, height: 1080 }, tablet: { width: 768, height: 1024 }, mobile: { width: 375, height: 667 }, }; async function login(context) { const page = await context.newPage(); const loginUrl = `${TARGET_URL}/login`; console.log(`Logging in at ${loginUrl}`); await page.goto(loginUrl, { waitUntil: 'networkidle', timeout: 30000 }); const tokenInput = await page.locator('input[name="token"], input#token, input[type="password"]').first(); if (await tokenInput.count() > 0) { await tokenInput.fill(TOKEN); } const submitBtn = await page.locator('button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")').first(); if (await submitBtn.count() > 0) { await submitBtn.click(); } else { await page.evaluate((token) => { const form = document.querySelector('form'); if (form) { const input = form.querySelector('input[name="token"]') || form.querySelector('input[type="password"]'); if (input) input.value = token; form.submit(); } }, TOKEN); } await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); const cookies = await context.cookies(); console.log('Cookies after login:', cookies.map(c => c.name)); await page.close(); } async function captureAndMonitor(context, pageInfo, viewportName, outputDir) { const page = await context.newPage(); const fullUrl = `${TARGET_URL}${pageInfo.path}`; const filename = `${pageInfo.name}_${viewportName}.png`; const filepath = path.join(outputDir, filename); const consoleErrors = []; const networkErrors = []; const assetErrors = []; page.on('console', msg => { if (msg.type() === 'error') { consoleErrors.push({ type: 'console_error', text: msg.text(), location: msg.location() }); } }); page.on('pageerror', err => { consoleErrors.push({ type: 'page_error', text: err.message, stack: err.stack }); }); page.on('requestfailed', req => { networkErrors.push({ type: 'network_failure', url: req.url(), resourceType: req.resourceType(), error: req.failure()?.errorText }); }); page.on('response', res => { if (res.status() >= 400) { const req = res.request(); const entry = { type: 'http_error', url: res.url(), status: res.status(), statusText: res.statusText(), resourceType: req.resourceType() }; if (req.resourceType() === 'stylesheet' || req.resourceType() === 'script' || req.url().match(/\.(css|js|woff|woff2)$/)) { assetErrors.push(entry); } networkErrors.push(entry); } }); await page.setViewportSize(VIEWPORTS[viewportName]); try { await page.goto(fullUrl, { waitUntil: 'networkidle', timeout: 30000 }); } catch (e) { await page.goto(fullUrl, { waitUntil: 'load', timeout: 30000 }); } await page.waitForTimeout(1500); if (pageInfo.name === 'catalog') { try { const accordionHeaders = await page.locator('.accordion-button, [data-toggle="collapse"], .catalog-toggle').all(); for (const header of accordionHeaders.slice(0, 3)) { await header.click().catch(() => {}); await page.waitForTimeout(300); } await page.waitForTimeout(500); } catch (e) {} } if (pageInfo.name === 'users') { try { const collapseBtn = await page.locator('button:has-text("Adjust Balance"), [data-toggle="collapse"]:has-text("Adjust"), .adjust-balance-toggle').first(); if (await collapseBtn.count() > 0) { await collapseBtn.click().catch(() => {}); await page.waitForTimeout(500); } } catch (e) {} } await page.screenshot({ path: filepath, fullPage: false }); await page.close(); return { page: pageInfo.name, path: pageInfo.path, viewport: viewportName, file: filename, url: fullUrl, consoleErrors, networkErrors, assetErrors, status: networkErrors.some(e => e.status >= 500) ? 'ERROR' : 'OK', }; } async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.mkdirSync(REPORTS_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ ignoreHTTPSErrors: true }); await login(context); const results = []; for (const pageInfo of PAGES) { for (const vpName of Object.keys(VIEWPORTS)) { try { const result = await captureAndMonitor(context, pageInfo, vpName, OUTPUT_DIR); results.push(result); const errSummary = result.consoleErrors.length + result.networkErrors.length; console.log(`[${result.status}] ${pageInfo.name} @ ${vpName} (${errSummary} errors) -> ${result.file}`); } catch (err) { results.push({ page: pageInfo.name, path: pageInfo.path, viewport: vpName, status: 'ERROR', error: err.message, consoleErrors: [], networkErrors: [], assetErrors: [] }); console.error(`[ERROR] ${pageInfo.name} @ ${vpName}: ${err.message}`); } } } await browser.close(); const summary = { timestamp: new Date().toISOString(), targetUrl: TARGET_URL, totalScreenshots: results.filter(r => !r.error).length, totalErrors: results.filter(r => r.status === 'ERROR' || r.error).length, pages: PAGES.map(p => { const pageResults = results.filter(r => r.page === p.name); const anyError = pageResults.some(r => r.status === 'ERROR' || r.error || r.consoleErrors?.length > 0 || r.networkErrors?.length > 0); return { name: p.name, path: p.path, status: pageResults.some(r => r.status === 'ERROR') ? 'ERROR' : (anyError ? 'WARN' : 'OK'), viewports: Object.fromEntries(pageResults.map(r => [r.viewport, { file: r.file, status: r.status, consoleErrors: r.consoleErrors?.length || 0, networkErrors: r.networkErrors?.length || 0 }])), consoleErrors: pageResults.flatMap(r => r.consoleErrors || []), networkErrors: pageResults.flatMap(r => r.networkErrors || []), assetErrors: pageResults.flatMap(r => r.assetErrors || []), }; }), results, }; const reportPath = path.join(REPORTS_DIR, 'admin-visual-test-report.json'); fs.writeFileSync(reportPath, JSON.stringify(summary, null, 2)); console.log(`\nReport saved to ${reportPath}`); console.log('\n## Visual Test Results\n'); console.log('| Page | Status | Viewports | Console Errors | Network Errors | Notes |'); console.log('|------|--------|-----------|----------------|----------------|-------|'); for (const p of summary.pages) { const vpStatus = Object.entries(p.viewports).map(([vp, data]) => `${vp}: ${data.status}`).join(', '); const ce = p.consoleErrors.length; const ne = p.networkErrors.length; const notes = []; if (p.assetErrors?.length > 0) notes.push(`${p.assetErrors.length} asset 404s`); if (p.consoleErrors.some(e => e.type === 'page_error')) notes.push('page errors'); console.log(`| ${p.name} | ${p.status} | ${vpStatus} | ${ce} | ${ne} | ${notes.join('; ') || '-'} |`); } const mdPath = path.join(REPORTS_DIR, 'admin-visual-test-report.md'); let md = `## Visual Test Results for Admin Panel\n\n`; md += `**URL:** ${TARGET_URL}\n`; md += `**Date:** ${summary.timestamp}\n\n`; md += `| Page | Status | Desktop | Tablet | Mobile | Console | Network | Notes |\n`; md += `|------|--------|---------|--------|--------|---------|---------|-------|\n`; for (const p of summary.pages) { const d = p.viewports.desktop?.status || 'N/A'; const t = p.viewports.tablet?.status || 'N/A'; const m = p.viewports.mobile?.status || 'N/A'; const ce = (p.consoleErrors || []).length; const ne = (p.networkErrors || []).length; const notes = []; if ((p.assetErrors || []).length > 0) notes.push(`${p.assetErrors.length} asset 404s`); if (p.consoleErrors.some(e => e.type === 'page_error')) notes.push('JS page errors'); md += `| ${p.name} | ${p.status} | ${d} | ${t} | ${m} | ${ce} | ${ne} | ${notes.join('; ') || '-'} |\n`; } md += `\n### Screenshot Files\n\n`; for (const p of summary.pages) { for (const [vp, data] of Object.entries(p.viewports)) { md += `- \`${data.file}\`\n`; } } md += `\n### Asset 404 Errors\n\n`; const allAssetErrors = summary.pages.flatMap(p => p.assetErrors || []); if (allAssetErrors.length === 0) { md += 'No asset 404 errors found.\n'; } else { for (const e of allAssetErrors) { md += `- \`${e.url}\` (${e.status} ${e.statusText})\n`; } } md += `\n### Runtime JS Errors\n\n`; const allPageErrors = summary.pages.flatMap(p => p.consoleErrors.filter(e => e.type === 'page_error') || []); if (allPageErrors.length === 0) { md += 'No runtime JS page errors found.\n'; } else { for (const e of allPageErrors) { md += `- \`${e.text}\`\n`; } } fs.writeFileSync(mdPath, md); console.log(`Markdown report saved to ${mdPath}`); } main().catch(err => { console.error('Fatal error:', err); process.exit(1); });