- 新增 ADR 架构决策记录 (Design DNA 集成与深化) - 新增 CMS 系统设计文档 - 新增实施计划文档 (Phase1-3) - 新增 Bain 品牌升级设计规格 - 新增 E2E 分层测试套件 (P1-P4) - 新增视觉回归测试配置 - 新增光效分析、视觉验证等辅助脚本 - 更新验收测试报告
222 lines
7.1 KiB
JavaScript
222 lines
7.1 KiB
JavaScript
import { chromium } from '@playwright/test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const screenshotsDir = '/tmp/screenshots/full-visual-test';
|
|
fs.mkdirSync(screenshotsDir, { recursive: true });
|
|
|
|
const pages = [
|
|
{ name: 'home', url: 'http://localhost:3000', sections: ['hero', 'stats', 'services', 'industries', 'cases', 'approach', 'insights', 'cta'] },
|
|
{ name: 'products', url: 'http://localhost:3000/products', sections: ['hero', 'product-grid'] },
|
|
{ name: 'solutions', url: 'http://localhost:3000/solutions', sections: ['hero', 'solutions-grid'] },
|
|
{ name: 'services', url: 'http://localhost:3000/services', sections: ['hero', 'services-grid'] },
|
|
{ name: 'about', url: 'http://localhost:3000/about', sections: ['hero', 'about-content'] },
|
|
];
|
|
|
|
async function capturePage(page, pageInfo) {
|
|
console.log(`\n=== Testing ${pageInfo.name} page ===`);
|
|
|
|
await page.goto(pageInfo.url, { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(1500);
|
|
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, `${pageInfo.name}-full.png`),
|
|
fullPage: true,
|
|
});
|
|
console.log(` ✓ Full page screenshot captured`);
|
|
|
|
const viewportHeight = 1080;
|
|
let currentScroll = 0;
|
|
let sectionIndex = 0;
|
|
|
|
while (sectionIndex < 5) {
|
|
await page.evaluate((scroll) => {
|
|
window.scrollTo(0, scroll);
|
|
}, currentScroll);
|
|
|
|
await page.waitForTimeout(800);
|
|
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, `${pageInfo.name}-section-${sectionIndex + 1}.png`),
|
|
});
|
|
|
|
currentScroll += viewportHeight * 0.8;
|
|
sectionIndex++;
|
|
}
|
|
|
|
console.log(` ✓ Section screenshots captured`);
|
|
|
|
const cards = await page.$$('a[href], .card, [class*="group"]');
|
|
if (cards.length > 0) {
|
|
const firstCard = cards[0];
|
|
|
|
await firstCard.scrollIntoViewIfNeeded();
|
|
await page.waitForTimeout(500);
|
|
|
|
const box = await firstCard.boundingBox();
|
|
if (box) {
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, `${pageInfo.name}-card-before.png`),
|
|
clip: {
|
|
x: Math.max(0, box.x - 20),
|
|
y: Math.max(0, box.y - 20),
|
|
width: Math.min(box.width + 40, 800),
|
|
height: Math.min(box.height + 40, 600),
|
|
},
|
|
});
|
|
|
|
await firstCard.hover();
|
|
await page.waitForTimeout(600);
|
|
|
|
await page.screenshot({
|
|
path: path.join(screenshotsDir, `${pageInfo.name}-card-hover.png`),
|
|
clip: {
|
|
x: Math.max(0, box.x - 20),
|
|
y: Math.max(0, box.y - 20),
|
|
width: Math.min(box.width + 40, 800),
|
|
height: Math.min(box.height + 40, 600),
|
|
},
|
|
});
|
|
|
|
console.log(` ✓ Card hover states captured`);
|
|
}
|
|
}
|
|
|
|
await page.evaluate(() => {
|
|
window.scrollTo(0, 0);
|
|
});
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
async function checkTextReadability(page, pageName) {
|
|
console.log(`\n Checking text readability...`);
|
|
|
|
const issues = await page.evaluate(() => {
|
|
const problems = [];
|
|
const textElements = document.querySelectorAll('h1, h2, h3, h4, p, span, a');
|
|
|
|
textElements.forEach((el, index) => {
|
|
const style = window.getComputedStyle(el);
|
|
const opacity = parseFloat(style.opacity);
|
|
const color = style.color;
|
|
const bgColor = style.backgroundColor;
|
|
const fontSize = parseFloat(style.fontSize);
|
|
const zIndex = parseInt(style.zIndex) || 0;
|
|
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) return;
|
|
|
|
if (opacity < 0.4 && opacity > 0 && el.textContent.trim().length > 5) {
|
|
problems.push({
|
|
type: 'low-opacity',
|
|
text: el.textContent.trim().substring(0, 50),
|
|
opacity: opacity.toFixed(2),
|
|
tag: el.tagName,
|
|
});
|
|
}
|
|
|
|
if (style.mixBlendMode && style.mixBlendMode !== 'normal') {
|
|
problems.push({
|
|
type: 'mix-blend-mode',
|
|
text: el.textContent.trim().substring(0, 50),
|
|
blendMode: style.mixBlendMode,
|
|
tag: el.tagName,
|
|
});
|
|
}
|
|
});
|
|
|
|
return problems.slice(0, 10);
|
|
});
|
|
|
|
if (issues.length > 0) {
|
|
console.log(` ⚠ Found ${issues.length} potential readability issues:`);
|
|
issues.forEach((issue, i) => {
|
|
console.log(` ${i + 1}. [${issue.type}] ${issue.tag}: "${issue.text}"`);
|
|
});
|
|
} else {
|
|
console.log(` ✓ No major readability issues found`);
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
async function checkGlowElements(page, pageName) {
|
|
console.log(`\n Checking glow/blur elements...`);
|
|
|
|
const glowInfo = await page.evaluate(() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
const glowElements = [];
|
|
|
|
allElements.forEach((el) => {
|
|
const style = window.getComputedStyle(el);
|
|
const hasBlur = style.filter.includes('blur');
|
|
const hasRadialGradient = style.backgroundImage.includes('radial-gradient');
|
|
const opacity = parseFloat(style.opacity);
|
|
|
|
if ((hasBlur || hasRadialGradient) && opacity > 0.05) {
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width > 100 && rect.height > 100) {
|
|
glowElements.push({
|
|
tag: el.tagName,
|
|
className: el.className.substring(0, 100),
|
|
opacity: opacity.toFixed(3),
|
|
hasBlur,
|
|
hasRadialGradient,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
zIndex: style.zIndex || 'auto',
|
|
position: style.position,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return glowElements.slice(0, 15);
|
|
});
|
|
|
|
console.log(` Found ${glowInfo.length} glow/blur elements (>100px, opacity>0.05)`);
|
|
glowInfo.forEach((el, i) => {
|
|
console.log(` ${i + 1}. ${el.tag} (${Math.round(el.width)}x${Math.round(el.height)}, opacity: ${el.opacity})`);
|
|
console.log(` position: ${el.position}, zIndex: ${el.zIndex}`);
|
|
});
|
|
|
|
return glowInfo;
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('========================================');
|
|
console.log(' Novalon Website Visual Test Suite');
|
|
console.log('========================================');
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 1080 },
|
|
});
|
|
const page = await context.newPage();
|
|
|
|
let totalIssues = 0;
|
|
|
|
for (const pageInfo of pages.slice(0, 3)) {
|
|
try {
|
|
await capturePage(page, pageInfo);
|
|
const readabilityIssues = await checkTextReadability(page, pageInfo.name);
|
|
const glowElements = await checkGlowElements(page, pageInfo.name);
|
|
totalIssues += readabilityIssues.length;
|
|
} catch (e) {
|
|
console.log(` ✗ Error testing ${pageInfo.name}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
console.log('\n========================================');
|
|
console.log(' Test Summary');
|
|
console.log('========================================');
|
|
console.log(` Screenshots saved to: ${screenshotsDir}`);
|
|
console.log(` Total readability issues found: ${totalIssues}`);
|
|
console.log('\n Check the screenshots for visual verification.');
|
|
console.log('========================================');
|
|
}
|
|
|
|
runTests().catch(console.error);
|