docs(test): 添加设计文档、测试规范与 E2E 测试套件
- 新增 ADR 架构决策记录 (Design DNA 集成与深化) - 新增 CMS 系统设计文档 - 新增实施计划文档 (Phase1-3) - 新增 Bain 品牌升级设计规格 - 新增 E2E 分层测试套件 (P1-P4) - 新增视觉回归测试配置 - 新增光效分析、视觉验证等辅助脚本 - 更新验收测试报告
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user