import { chromium } from '@playwright/test'; import { mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; const screenshotDir = '/tmp/screenshots/phase4-visual-test'; mkdirSync(screenshotDir, { recursive: true }); const pages = [ { name: 'product-detail', url: 'http://localhost:3000/products/erp', waitMs: 2000 }, { name: 'solution-detail', url: 'http://localhost:3000/solutions/manufacturing', waitMs: 2000 }, { name: 'service-detail', url: 'http://localhost:3000/services/software', waitMs: 2000 }, ]; const sections = [ { name: 'hero', scrollY: 0 }, { name: 'mid-section', scrollY: 800 }, { name: 'cta-section', scrollY: 2500 }, ]; (async () => { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, }); const results = []; for (const page of pages) { console.log(`\n=== Testing: ${page.name} ===`); const pageObj = await context.newPage(); try { await pageObj.goto(page.url, { waitUntil: 'domcontentloaded', timeout: 30000 }); await pageObj.waitForTimeout(page.waitMs + 2000); // 检查页面是否有错误 const pageErrors = []; pageObj.on('pageerror', (err) => { pageErrors.push(err.message); }); for (const section of sections) { await pageObj.evaluate((y) => window.scrollTo(0, y), section.scrollY); await pageObj.waitForTimeout(800); const screenshotPath = join(screenshotDir, `${page.name}-${section.name}.png`); await pageObj.screenshot({ path: screenshotPath, fullPage: false }); console.log(` ✓ ${section.name} screenshot saved`); } // 检查文字可见性 const textVisibility = await pageObj.evaluate(() => { const textElements = document.querySelectorAll('h1, h2, h3, p, span'); let total = 0; let visible = 0; let lowOpacity = 0; for (const el of textElements) { const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight) { total++; const opacity = parseFloat(style.opacity); if (opacity >= 0.9) visible++; if (opacity < 0.5) lowOpacity++; } } return { total, visible, lowOpacity }; }); console.log(` Text visibility: ${textVisibility.visible}/${textVisibility.total} visible, ${textVisibility.lowOpacity} low opacity`); // 检查光晕元素 const glowElements = await pageObj.evaluate(() => { const elements = document.querySelectorAll('*'); const glowEls = []; for (const el of elements) { const style = window.getComputedStyle(el); const hasBlur = style.filter.includes('blur'); const hasLowOpacity = parseFloat(style.opacity) < 0.1; const isRounded = style.borderRadius.includes('%') || parseInt(style.borderRadius) > 50; if (hasBlur && hasLowOpacity && isRounded && el.offsetWidth > 100) { glowEls.push({ tag: el.tagName, width: el.offsetWidth, opacity: style.opacity, pointerEvents: style.pointerEvents, }); } } return glowEls.slice(0, 10); }); console.log(` Glow elements found: ${glowElements.length}`); glowElements.forEach((el, i) => { console.log(` [${i}] ${el.tag} ${el.width}px opacity:${el.opacity} pointer-events:${el.pointerEvents}`); }); results.push({ page: page.name, status: pageErrors.length === 0 ? 'pass' : 'fail', errors: pageErrors, textVisibility, glowCount: glowElements.length, }); } catch (err) { console.log(` ✗ Error: ${err.message}`); results.push({ page: page.name, status: 'error', error: err.message }); } await pageObj.close(); } await browser.close(); console.log('\n\n=== Phase 4 Visual Test Summary ==='); results.forEach((r) => { console.log(`${r.status === 'pass' ? '✓' : '✗'} ${r.page}: ${r.status}`); if (r.errors?.length) console.log(` Errors: ${r.errors.join(', ')}`); if (r.error) console.log(` Error: ${r.error}`); }); const allPassed = results.every((r) => r.status === 'pass'); console.log(`\nOverall: ${allPassed ? 'ALL PASSED ✓' : 'SOME FAILED ✗'}`); writeFileSync( join(screenshotDir, 'results.json'), JSON.stringify(results, null, 2) ); process.exit(allPassed ? 0 : 1); })();