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