636bc4ecde
- 新增 ADR 架构决策记录 (Design DNA 集成与深化) - 新增 CMS 系统设计文档 - 新增实施计划文档 (Phase1-3) - 新增 Bain 品牌升级设计规格 - 新增 E2E 分层测试套件 (P1-P4) - 新增视觉回归测试配置 - 新增光效分析、视觉验证等辅助脚本 - 更新验收测试报告
194 lines
6.5 KiB
JavaScript
194 lines
6.5 KiB
JavaScript
import { chromium } from '@playwright/test';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const testDir = './visual-test';
|
|
if (!fs.existsSync(testDir)) {
|
|
fs.mkdirSync(testDir, { recursive: true });
|
|
}
|
|
|
|
const viewports = [
|
|
{ name: 'mobile', width: 390, height: 844 },
|
|
{ name: 'tablet', width: 768, height: 1024 },
|
|
{ name: 'desktop', width: 1440, height: 900 },
|
|
];
|
|
|
|
const pages = [
|
|
{ name: 'home', url: 'http://localhost:3000', sections: ['hero', 'services', 'solutions', 'cases', 'stats', 'cta'] },
|
|
{ name: 'products', url: 'http://localhost:3000/products', sections: ['hero', 'enterprise', 'specialized', 'cta'] },
|
|
{ name: 'solutions', url: 'http://localhost:3000/solutions', sections: ['hero', 'industries', 'cta'] },
|
|
{ name: 'services', url: 'http://localhost:3000/services', sections: ['hero', 'service-areas', 'process', 'cta'] },
|
|
];
|
|
|
|
async function takeSectionScreenshot(page, sectionName, pageName, viewportName) {
|
|
const sectionSelectors = {
|
|
hero: 'h1',
|
|
services: '[class*="service"]',
|
|
solutions: '[class*="solution"], [class*="industry"]',
|
|
cases: '[class*="case"], [class*="client"]',
|
|
stats: '[class*="stat"], [class*="metric"]',
|
|
cta: '[class*="cta"], h2',
|
|
enterprise: '[class*="enterprise"], [class*="product"]',
|
|
specialized: '[class*="specialized"], [class*="product"]',
|
|
industries: '[class*="industry"], [class*="card"]',
|
|
'service-areas': '[class*="service"], [class*="card"]',
|
|
process: '[class*="process"], [class*="step"]',
|
|
};
|
|
|
|
const selector = sectionSelectors[sectionName];
|
|
if (!selector) return;
|
|
|
|
try {
|
|
const element = await page.locator(selector).first();
|
|
if (await element.isVisible({ timeout: 2000 })) {
|
|
await element.scrollIntoViewIfNeeded();
|
|
await page.waitForTimeout(500);
|
|
const screenshotPath = path.join(testDir, `${pageName}_${viewportName}_${sectionName}.png`);
|
|
await element.screenshot({ path: screenshotPath, timeout: 5000 });
|
|
console.log(` ✓ ${sectionName} 截图: ${screenshotPath}`);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.log(` - ${sectionName} 跳过: ${e.message.substring(0, 50)}`);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function checkTextVisibility(page) {
|
|
const issues = [];
|
|
|
|
const allTextElements = await page.$$eval('h1, h2, h3, h4, p, span, a', (elements) => {
|
|
return elements
|
|
.filter(el => {
|
|
const style = window.getComputedStyle(el);
|
|
const rect = el.getBoundingClientRect();
|
|
return (
|
|
rect.width > 50 &&
|
|
rect.height > 10 &&
|
|
rect.top >= 0 &&
|
|
rect.top < window.innerHeight &&
|
|
style.opacity !== '0' &&
|
|
style.display !== 'none' &&
|
|
el.textContent.trim().length > 2
|
|
);
|
|
})
|
|
.slice(0, 20)
|
|
.map(el => {
|
|
const style = window.getComputedStyle(el);
|
|
const rect = el.getBoundingClientRect();
|
|
return {
|
|
tag: el.tagName,
|
|
text: el.textContent.trim().substring(0, 30),
|
|
color: style.color,
|
|
backgroundColor: style.backgroundColor,
|
|
opacity: style.opacity,
|
|
fontSize: style.fontSize,
|
|
zIndex: style.zIndex,
|
|
mixBlendMode: style.mixBlendMode,
|
|
top: Math.round(rect.top),
|
|
left: Math.round(rect.left),
|
|
width: Math.round(rect.width),
|
|
height: Math.round(rect.height),
|
|
};
|
|
});
|
|
});
|
|
|
|
for (const el of allTextElements) {
|
|
if (el.opacity < 0.5 && el.opacity !== '1') {
|
|
issues.push(`低透明度文字: ${el.tag} "${el.text}" (opacity: ${el.opacity})`);
|
|
}
|
|
if (el.mixBlendMode && el.mixBlendMode !== 'normal') {
|
|
issues.push(`混合模式文字: ${el.tag} "${el.text}" (mix-blend: ${el.mixBlendMode})`);
|
|
}
|
|
}
|
|
|
|
return { allTextElements, issues };
|
|
}
|
|
|
|
async function runVisualTest() {
|
|
console.log('========== 视觉测试开始 ==========\n');
|
|
|
|
const browser = await chromium.launch();
|
|
const results = {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
issues: [],
|
|
};
|
|
|
|
for (const viewport of viewports) {
|
|
console.log(`\n【视口: ${viewport.name} (${viewport.width}x${viewport.height})】`);
|
|
const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height } });
|
|
const page = await context.newPage();
|
|
|
|
for (const pageInfo of pages) {
|
|
console.log(`\n 页面: ${pageInfo.name}`);
|
|
results.total++;
|
|
|
|
try {
|
|
await page.goto(pageInfo.url, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
await page.waitForTimeout(3000);
|
|
|
|
const title = await page.title();
|
|
console.log(` ✓ 页面标题: ${title}`);
|
|
|
|
const fullPagePath = path.join(testDir, `${pageInfo.name}_${viewport.name}_full.png`);
|
|
await page.screenshot({ path: fullPagePath, fullPage: true, timeout: 10000 });
|
|
console.log(` ✓ 全页截图: ${fullPagePath}`);
|
|
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await page.waitForTimeout(500);
|
|
|
|
const visibilityResult = await checkTextVisibility(page);
|
|
if (visibilityResult.issues.length > 0) {
|
|
console.log(` ⚠ 文字可见性问题:`);
|
|
for (const issue of visibilityResult.issues.slice(0, 5)) {
|
|
console.log(` - ${issue}`);
|
|
results.issues.push(`[${viewport.name}/${pageInfo.name}] ${issue}`);
|
|
}
|
|
} else {
|
|
console.log(` ✓ 文字可见性正常`);
|
|
}
|
|
|
|
console.log(` 区域截图:`);
|
|
for (const section of pageInfo.sections) {
|
|
await takeSectionScreenshot(page, section, pageInfo.name, viewport.name);
|
|
}
|
|
|
|
results.passed++;
|
|
} catch (error) {
|
|
console.log(` ✗ 错误: ${error.message.substring(0, 100)}`);
|
|
results.failed++;
|
|
results.issues.push(`[${viewport.name}/${pageInfo.name}] 加载失败: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
await context.close();
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
console.log('\n\n========== 视觉测试总结 ==========');
|
|
console.log(`总页面数: ${results.total}`);
|
|
console.log(`成功: ${results.passed}`);
|
|
console.log(`失败: ${results.failed}`);
|
|
|
|
if (results.issues.length > 0) {
|
|
console.log(`\n发现 ${results.issues.length} 个问题:`);
|
|
for (const issue of results.issues.slice(0, 10)) {
|
|
console.log(` - ${issue}`);
|
|
}
|
|
if (results.issues.length > 10) {
|
|
console.log(` ... 还有 ${results.issues.length - 10} 个问题`);
|
|
}
|
|
} else {
|
|
console.log('\n✓ 未发现明显视觉问题');
|
|
}
|
|
|
|
console.log(`\n截图保存在: ${testDir}/`);
|
|
|
|
return results;
|
|
}
|
|
|
|
runVisualTest().catch(console.error);
|