docs(test): 添加设计文档、测试规范与 E2E 测试套件

- 新增 ADR 架构决策记录 (Design DNA 集成与深化)
- 新增 CMS 系统设计文档
- 新增实施计划文档 (Phase1-3)
- 新增 Bain 品牌升级设计规格
- 新增 E2E 分层测试套件 (P1-P4)
- 新增视觉回归测试配置
- 新增光效分析、视觉验证等辅助脚本
- 更新验收测试报告
This commit is contained in:
张翔
2026-07-07 06:54:25 +08:00
parent 8def296301
commit 636bc4ecde
31 changed files with 9262 additions and 41 deletions
+76
View File
@@ -0,0 +1,76 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
console.log('正在访问首页...');
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// 详细检查 Hero 区域的所有层级
console.log('\n=== Hero 区域详细层级分析 ===');
const heroDetail = await page.evaluate(() => {
const heroSection = document.querySelector('section');
if (!heroSection) return null;
const getAllChildren = (el, depth = 0, maxDepth = 5) => {
const result = [];
if (depth > maxDepth) return result;
const style = window.getComputedStyle(el);
const bg = style.backgroundImage || '';
const hasGlow = bg.includes('radial-gradient') || bg.includes('linear-gradient') || el.className?.includes?.('blur');
if (hasGlow) {
result.push({
depth,
tag: el.tagName,
className: (el.className || '').substring(0, 120),
zIndex: style.zIndex,
position: style.position,
opacity: style.opacity,
background: bg.substring(0, 150),
mixBlendMode: style.mixBlendMode,
backgroundColor: style.backgroundColor,
});
}
for (const child of Array.from(el.children)) {
result.push(...getAllChildren(child, depth + 1, maxDepth));
}
return result;
};
return getAllChildren(heroSection);
});
console.log(JSON.stringify(heroDetail, null, 2));
// 检查所有带 mix-blend-mode 的元素
console.log('\n=== 所有带 mix-blend-mode 的元素 ===');
const mixBlendElements = await page.evaluate(() => {
const all = document.querySelectorAll('*');
const results = [];
all.forEach(el => {
const style = window.getComputedStyle(el);
if (style.mixBlendMode && style.mixBlendMode !== 'normal') {
results.push({
tag: el.tagName,
className: (el.className || '').substring(0, 100),
mixBlendMode: style.mixBlendMode,
opacity: style.opacity,
zIndex: style.zIndex,
position: style.position,
});
}
});
return results.slice(0, 20);
});
console.log(JSON.stringify(mixBlendElements, null, 2));
await browser.close();
console.log('\n分析完成!');
})();
+95
View File
@@ -0,0 +1,95 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
console.log('正在访问首页...');
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// 检查 Hero 区域的层级结构
console.log('\n=== Hero 区域层级分析 ===');
const heroLayers = await page.evaluate(() => {
const heroSection = document.querySelector('section');
if (!heroSection) return [];
const layers = [];
const children = heroSection.querySelectorAll(':scope > *');
children.forEach((child, i) => {
const style = window.getComputedStyle(child);
layers.push({
index: i,
tag: child.tagName,
className: child.className.substring(0, 100),
zIndex: style.zIndex,
position: style.position,
opacity: style.opacity,
});
});
return layers;
});
console.log(JSON.stringify(heroLayers, null, 2));
// 检查 Stats 区域
console.log('\n=== Stats 区域层级分析 ===');
const statsLayers = await page.evaluate(() => {
const sections = document.querySelectorAll('section');
let statsSection = null;
for (const s of sections) {
if (s.textContent?.includes('用数据说话')) {
statsSection = s;
break;
}
}
if (!statsSection) return [];
const layers = [];
const children = statsSection.querySelectorAll(':scope > *');
children.forEach((child, i) => {
const style = window.getComputedStyle(child);
layers.push({
index: i,
tag: child.tagName,
className: child.className.substring(0, 100),
zIndex: style.zIndex,
position: style.position,
opacity: style.opacity,
});
});
return layers;
});
console.log(JSON.stringify(statsLayers, null, 2));
// 检查所有带 radial-gradient 的元素
console.log('\n=== 所有径向渐变光晕元素 ===');
const glowElements = await page.evaluate(() => {
const all = document.querySelectorAll('*');
const results = [];
all.forEach(el => {
const style = window.getComputedStyle(el);
const bg = style.backgroundImage || '';
if (bg.includes('radial-gradient') && style.position !== 'static') {
results.push({
tag: el.tagName,
className: el.className.substring(0, 80),
zIndex: style.zIndex,
position: style.position,
background: bg.substring(0, 100),
opacity: style.opacity,
parentTag: el.parentElement?.tagName,
parentClass: el.parentElement?.className?.substring(0, 60) || '',
});
}
});
return results.slice(0, 20);
});
console.log(JSON.stringify(glowElements, null, 2));
await browser.close();
console.log('\n分析完成!');
})();
+93
View File
@@ -0,0 +1,93 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
console.log('正在访问首页...');
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// 检查 Hero 区域的所有直接子元素,按 DOM 顺序
console.log('\n=== Hero Section 直接子元素(按 DOM 顺序) ===');
const heroChildren = await page.evaluate(() => {
const heroSection = document.querySelector('section');
if (!heroSection) return [];
const results = [];
let index = 0;
for (const child of Array.from(heroSection.children)) {
const style = window.getComputedStyle(child);
results.push({
index: index++,
tag: child.tagName,
className: (child.className || '').substring(0, 80),
zIndex: style.zIndex,
position: style.position,
isText: child.textContent?.trim()?.substring(0, 30) || '',
});
}
return results;
});
console.log(JSON.stringify(heroChildren, null, 2));
// 检查每个 section 的背景色
console.log('\n=== 各 Section 背景色 ===');
const sectionsBg = await page.evaluate(() => {
const sections = document.querySelectorAll('section');
const results = [];
sections.forEach((s, i) => {
const style = window.getComputedStyle(s);
results.push({
index: i,
tag: s.tagName,
backgroundColor: style.backgroundColor,
className: (s.className || '').substring(0, 60),
firstText: s.textContent?.trim()?.substring(0, 25) || '',
});
});
return results;
});
console.log(JSON.stringify(sectionsBg, null, 2));
// 截图对比:先截一张,然后用 JS 去掉所有 blur 和 radial-gradient 元素,再截一张
const screenshotDir = '/tmp/screenshots';
// 原始截图
await page.screenshot({ path: `${screenshotDir}/before-remove-glow.png` });
console.log('\n✓ 原始截图已保存');
// 去掉所有光晕相关元素
await page.evaluate(() => {
const all = document.querySelectorAll('*');
let removedCount = 0;
let hiddenCount = 0;
all.forEach(el => {
const style = window.getComputedStyle(el);
const bg = style.backgroundImage || '';
const hasRadialGradient = bg.includes('radial-gradient');
const hasBlur = style.filter?.includes('blur') || el.className?.includes?.('blur');
if (hasRadialGradient || hasBlur) {
if (el.style) {
el.style.visibility = 'hidden';
hiddenCount++;
}
}
});
return { hiddenCount };
});
await page.screenshot({ path: `${screenshotDir}/after-remove-glow.png` });
console.log('✓ 去掉光晕后的截图已保存');
await browser.close();
console.log('\n对比截图已保存到 /tmp/screenshots/ 目录');
console.log('before-remove-glow.png - 原始效果');
console.log('after-remove-glow.png - 去掉光晕后效果');
})();
+206
View File
@@ -0,0 +1,206 @@
import { chromium } from '@playwright/test';
async function runDetailedCheck() {
console.log('========================================');
console.log(' Detailed Glow & Text Check');
console.log('========================================');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 1080 },
});
const page = await context.newPage();
await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
console.log('\n1. Checking Hero section text visibility...');
const heroTextInfo = await page.evaluate(() => {
const heroSection = document.querySelector('section.min-h-screen');
if (!heroSection) return { error: 'Hero section not found' };
const headings = heroSection.querySelectorAll('h1, h2, p, span');
const textElements = [];
headings.forEach((el) => {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
if (rect.width > 50 && rect.height > 20) {
textElements.push({
tag: el.tagName,
text: el.textContent?.trim().substring(0, 50) || '',
opacity: style.opacity,
color: style.color,
zIndex: style.zIndex || 'auto',
position: style.position,
mixBlendMode: style.mixBlendMode,
y: Math.round(rect.top),
height: Math.round(rect.height),
});
}
});
return textElements.slice(0, 10);
});
console.log(` Found ${heroTextInfo.length} text elements in Hero:`);
heroTextInfo.forEach((el, i) => {
console.log(` ${i + 1}. ${el.tag}: "${el.text}"`);
console.log(` opacity: ${el.opacity}, color: ${el.color.substring(0, 30)}`);
console.log(` position: ${el.position}, zIndex: ${el.zIndex}`);
});
console.log('\n2. Checking Stats section (可衡量成果)...');
await page.evaluate(() => {
const statsSection = document.querySelector('section.bg-ink-light');
if (statsSection) {
statsSection.scrollIntoView({ behavior: 'auto' });
}
});
await page.waitForTimeout(1000);
const statsInfo = await page.evaluate(() => {
const statsSection = document.querySelector('section.bg-ink-light');
if (!statsSection) return { error: 'Stats section not found' };
const glowElements = [];
const allElements = statsSection.querySelectorAll('*');
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.01) {
const rect = el.getBoundingClientRect();
if (rect.width > 50 && rect.height > 50) {
glowElements.push({
tag: el.tagName,
className: el.className.substring(0, 80),
opacity: opacity.toFixed(3),
hasBlur,
hasRadialGradient,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
position: style.position,
zIndex: style.zIndex || 'auto',
pointerEvents: style.pointerEvents,
});
}
}
});
const textElements = [];
const texts = statsSection.querySelectorAll('h2, h3, p, span, div');
texts.forEach((el) => {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
const text = el.textContent?.trim() || '';
if (text.length > 5 && rect.width > 30 && rect.height > 15) {
textElements.push({
text: text.substring(0, 40),
color: style.color,
opacity: style.opacity,
});
}
});
return {
glowCount: glowElements.length,
glowElements: glowElements.slice(0, 8),
textSample: textElements.slice(0, 5),
};
});
console.log(` Glow/blur elements in Stats: ${statsInfo.glowCount}`);
if (statsInfo.glowElements?.length > 0) {
statsInfo.glowElements.forEach((el, i) => {
const type = el.hasBlur && el.hasRadialGradient ? 'blur+gradient' : el.hasBlur ? 'blur' : 'gradient';
console.log(` ${i + 1}. ${el.tag} (${el.size}, ${type}, opacity: ${el.opacity})`);
console.log(` position: ${el.position}, zIndex: ${el.zIndex}, pointerEvents: ${el.pointerEvents}`);
});
} else {
console.log(' ✓ No glow/blur elements found in Stats section (good!)');
}
console.log('\n3. Checking Case Studies section...');
await page.evaluate(() => {
const sections = document.querySelectorAll('section');
let caseSection = null;
sections.forEach((s) => {
if (s.textContent?.includes('可衡量的业务成果') || s.textContent?.includes('Case Studies')) {
caseSection = s;
}
});
if (caseSection) {
caseSection.scrollIntoView({ behavior: 'auto' });
}
});
await page.waitForTimeout(1000);
const caseInfo = await page.evaluate(() => {
const sections = document.querySelectorAll('section');
let caseSection = null;
sections.forEach((s) => {
if (s.textContent?.includes('可衡量的业务成果') || s.textContent?.includes('Case Studies')) {
caseSection = s;
}
});
if (!caseSection) return { error: 'Case section not found' };
const cards = caseSection.querySelectorAll('.group, a[href]');
const cardInfo = [];
cards.forEach((card, index) => {
const style = window.getComputedStyle(card);
const rect = card.getBoundingClientRect();
if (rect.width > 200 && rect.height > 100) {
const glowElementsInside = [];
const insideElements = card.querySelectorAll('*');
insideElements.forEach((el) => {
const elStyle = window.getComputedStyle(el);
if (elStyle.filter.includes('blur') && elStyle.backgroundImage.includes('radial-gradient')) {
glowElementsInside.push({
opacity: elStyle.opacity,
className: el.className.substring(0, 60),
});
}
});
cardInfo.push({
index: index + 1,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
glowInside: glowElementsInside.length,
glowElements: glowElementsInside.slice(0, 3),
});
}
});
return {
cardCount: cardInfo.length,
cards: cardInfo.slice(0, 4),
};
});
console.log(` Found ${caseInfo.cardCount} cards in Case Studies`);
if (caseInfo.cards) {
caseInfo.cards.forEach((card) => {
console.log(` Card ${card.index}: ${card.size}, glow elements: ${card.glowInside}`);
card.glowElements?.forEach((glow, i) => {
console.log(` - glow-${i + 1}: opacity ${glow.opacity}`);
});
});
}
await browser.close();
console.log('\n========================================');
console.log(' Detailed Check Complete');
console.log('========================================');
}
runDetailedCheck().catch(console.error);
+157
View File
@@ -0,0 +1,157 @@
import { chromium } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const screenshotsDir = '/tmp/screenshots/final-visual-test';
fs.mkdirSync(screenshotsDir, { recursive: true });
async function runTests() {
console.log('========================================');
console.log(' Novalon Homepage Visual Test');
console.log('========================================');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 1080 },
});
const page = await context.newPage();
console.log('\n1. Loading homepage...');
await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
console.log(' ✓ Page loaded');
console.log('\n2. Capturing full page screenshot...');
await page.screenshot({
path: path.join(screenshotsDir, '01-home-full.png'),
fullPage: true,
});
console.log(' ✓ Full page captured');
const sections = [
{ name: '02-hero', scrollY: 0, desc: 'Hero Section' },
{ name: '03-stats', scrollY: 900, desc: 'Stats Section (可衡量成果)' },
{ name: '04-services', scrollY: 1800, desc: 'Services Matrix' },
{ name: '05-industries', scrollY: 2800, desc: 'Industries Section' },
{ name: '06-cases', scrollY: 3600, desc: 'Case Studies' },
{ name: '07-approach', scrollY: 4500, desc: 'Approach Section' },
{ name: '08-insights', scrollY: 5400, desc: 'Insights Section' },
{ name: '09-cta', scrollY: 6300, desc: 'CTA Section' },
];
console.log('\n3. Capturing section screenshots...');
for (const section of sections) {
await page.evaluate((scrollY) => {
window.scrollTo(0, scrollY);
}, section.scrollY);
await page.waitForTimeout(800);
await page.screenshot({
path: path.join(screenshotsDir, `${section.name}.png`),
});
console.log(`${section.desc}`);
}
console.log('\n4. Checking text readability...');
const textIssues = await page.evaluate(() => {
const issues = [];
const allElements = document.querySelectorAll('*');
allElements.forEach((el) => {
const style = window.getComputedStyle(el);
const opacity = parseFloat(style.opacity);
const mixBlendMode = style.mixBlendMode;
if (mixBlendMode && mixBlendMode !== 'normal') {
const rect = el.getBoundingClientRect();
if (rect.width > 50 && rect.height > 20) {
issues.push({
type: 'mix-blend-mode',
tag: el.tagName,
className: el.className.substring(0, 80),
blendMode: mixBlendMode,
text: el.textContent?.trim().substring(0, 40) || '',
});
}
}
if (opacity < 0.5 && opacity > 0) {
const rect = el.getBoundingClientRect();
if (rect.width > 100 && rect.height > 30 && el.textContent?.trim().length > 10) {
issues.push({
type: 'low-opacity-text',
tag: el.tagName,
className: el.className.substring(0, 80),
opacity: opacity.toFixed(2),
text: el.textContent.trim().substring(0, 40),
});
}
}
});
return issues.slice(0, 20);
});
if (textIssues.length > 0) {
console.log(` ⚠ Found ${textIssues.length} potential issues:`);
textIssues.forEach((issue, i) => {
console.log(` ${i + 1}. [${issue.type}] ${issue.tag}: "${issue.text}"`);
if (issue.blendMode) console.log(` blendMode: ${issue.blendMode}`);
if (issue.opacity) console.log(` opacity: ${issue.opacity}`);
});
} else {
console.log(' ✓ No major text readability issues');
}
console.log('\n5. Checking glow/blur elements...');
const glowElements = await page.evaluate(() => {
const elements = [];
const allElements = document.querySelectorAll('*');
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.03) {
const rect = el.getBoundingClientRect();
if (rect.width > 150 && rect.height > 150) {
elements.push({
tag: el.tagName,
className: el.className.substring(0, 100),
opacity: opacity.toFixed(3),
hasBlur,
hasRadialGradient,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
position: style.position,
zIndex: style.zIndex || 'auto',
pointerEvents: style.pointerEvents,
});
}
}
});
return elements.sort((a, b) => parseFloat(b.opacity) - parseFloat(a.opacity)).slice(0, 12);
});
console.log(` Found ${glowElements.length} significant glow/blur elements:`);
glowElements.forEach((el, i) => {
const type = el.hasBlur && el.hasRadialGradient ? 'blur+gradient' : el.hasBlur ? 'blur' : 'gradient';
console.log(` ${i + 1}. ${el.tag} (${el.size}, ${type}, opacity: ${el.opacity})`);
console.log(` position: ${el.position}, zIndex: ${el.zIndex}, pointerEvents: ${el.pointerEvents}`);
console.log(` class: ${el.className.substring(0, 60)}`);
});
await browser.close();
console.log('\n========================================');
console.log(' Test Complete');
console.log('========================================');
console.log(` Screenshots saved to: ${screenshotsDir}`);
console.log('\n To view screenshots:');
console.log(` open ${screenshotsDir}`);
console.log('========================================');
}
runTests().catch(console.error);
+221
View File
@@ -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);
+30
View File
@@ -0,0 +1,30 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
console.log('正在访问首页...');
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// 滚动到 Case Studies 区域
await page.evaluate(() => {
const sections = document.querySelectorAll('section');
for (const s of sections) {
if (s.textContent?.includes('可衡量的业务成果')) {
s.scrollIntoView({ behavior: 'smooth' });
break;
}
}
});
await page.waitForTimeout(1500);
// 截图 Case Studies 区域
await page.screenshot({ path: '/tmp/screenshots/case-studies-fixed.png' });
console.log('✓ Case Studies 区域截图已保存');
await browser.close();
console.log('\n截图完成!');
})();
+49
View File
@@ -0,0 +1,49 @@
import { chromium } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
console.log('正在访问首页...');
await page.goto('http://localhost:3000');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const screenshotDir = '/tmp/screenshots';
if (!fs.existsSync(screenshotDir)) {
fs.mkdirSync(screenshotDir, { recursive: true });
}
// 首页 Hero
await page.screenshot({ path: path.join(screenshotDir, '01-home-hero.png') });
console.log('✓ 首页 Hero 截图已保存');
// Stats 区域
await page.evaluate(() => window.scrollTo(0, 1400));
await page.waitForTimeout(1000);
await page.screenshot({ path: path.join(screenshotDir, '02-stats-section.png') });
console.log('✓ Stats 区域截图已保存');
// 服务区域
await page.evaluate(() => window.scrollTo(0, 2000));
await page.waitForTimeout(1000);
await page.screenshot({ path: path.join(screenshotDir, '03-services-section.png') });
console.log('✓ 服务区域截图已保存');
// CTA 区域
await page.evaluate(() => window.scrollTo(0, 3800));
await page.waitForTimeout(1000);
await page.screenshot({ path: path.join(screenshotDir, '04-cta-section.png') });
console.log('✓ CTA 区域截图已保存');
// 整页截图
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
await page.screenshot({ path: path.join(screenshotDir, '00-full-page.png'), fullPage: true });
console.log('✓ 整页截图已保存');
await browser.close();
console.log('\n所有截图已保存到 /tmp/screenshots/ 目录');
})();
+90
View File
@@ -0,0 +1,90 @@
const { chromium } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
const BASE_URL = 'http://localhost:3000';
const OUTPUT_DIR = '/tmp/screenshots/phase7-visual-test';
const PAGES = [
{ name: '01-home', path: '/' },
{ name: '02-products-list', path: '/products' },
{ name: '03-product-detail-erp', path: '/products/erp' },
{ name: '04-solutions-list', path: '/solutions' },
{ name: '05-solutions-detail', path: '/solutions/manufacturing' },
{ name: '06-services-list', path: '/services' },
{ name: '07-service-detail', path: '/services/consulting' },
{ name: '08-about', path: '/about' },
{ name: '09-contact', path: '/contact' },
{ name: '10-news-list', path: '/news' },
{ name: '11-team', path: '/team' },
];
const VIEWPORTS = [
{ name: 'mobile', width: 390, height: 844 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1440, height: 900 },
];
async function runVisualTest() {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
const browser = await chromium.launch();
const results = [];
for (const viewport of VIEWPORTS) {
const viewportDir = path.join(OUTPUT_DIR, viewport.name);
if (!fs.existsSync(viewportDir)) {
fs.mkdirSync(viewportDir, { recursive: true });
}
const page = await browser.newPage({
viewport: { width: viewport.width, height: viewport.height },
});
for (const pageInfo of PAGES) {
const url = `${BASE_URL}${pageInfo.path}`;
const screenshotPath = path.join(viewportDir, `${pageInfo.name}.png`);
try {
console.log(`[${viewport.name}] Capturing: ${pageInfo.name}...`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForTimeout(2000);
await page.screenshot({ path: screenshotPath, fullPage: true });
results.push({ viewport: viewport.name, page: pageInfo.name, status: 'success', path: screenshotPath });
console.log(` ✓ Saved to ${screenshotPath}`);
} catch (error) {
results.push({ viewport: viewport.name, page: pageInfo.name, status: 'failed', error: error.message });
console.log(` ✗ Failed: ${error.message}`);
}
}
await page.close();
}
await browser.close();
const successCount = results.filter(r => r.status === 'success').length;
const failCount = results.filter(r => r.status === 'failed').length;
console.log('\n' + '='.repeat(60));
console.log('Visual Test Summary');
console.log('='.repeat(60));
console.log(`Total: ${results.length} pages`);
console.log(`Success: ${successCount}`);
console.log(`Failed: ${failCount}`);
console.log(`Output: ${OUTPUT_DIR}`);
console.log('='.repeat(60));
if (failCount > 0) {
console.log('\nFailed pages:');
results.filter(r => r.status === 'failed').forEach(r => {
console.log(` - [${r.viewport}] ${r.page}: ${r.error}`);
});
}
return results;
}
runVisualTest().catch(console.error);