Files
novalon-website/visual-test.mjs
T
张翔 636bc4ecde docs(test): 添加设计文档、测试规范与 E2E 测试套件
- 新增 ADR 架构决策记录 (Design DNA 集成与深化)
- 新增 CMS 系统设计文档
- 新增实施计划文档 (Phase1-3)
- 新增 Bain 品牌升级设计规格
- 新增 E2E 分层测试套件 (P1-P4)
- 新增视觉回归测试配置
- 新增光效分析、视觉验证等辅助脚本
- 更新验收测试报告
2026-07-07 06:54:25 +08:00

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);