Files
张翔 929f3ed250 chore: 添加设计分析参考和辅助脚本
- IHG设计分析HTML参考页面
- 布局检查、DOM诊断、验收测试等辅助脚本
2026-06-07 16:22:11 +08:00

486 lines
20 KiB
JavaScript

const { chromium } = require("playwright");
const BASE = "http://localhost:3001";
const TIMEOUT = 20000;
const RESULTS = { pass: 0, fail: 0, warn: 0, details: [] };
function log(category, status, message) {
const icon = status === "pass" ? "✅" : status === "fail" ? "❌" : "⚠️";
RESULTS[status === "pass" ? "pass" : status === "fail" ? "fail" : "warn"]++;
RESULTS.details.push({ category, status, message });
console.log(` ${icon} [${category}] ${message}`);
}
async function waitForPage(page, url) {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: TIMEOUT });
await page.waitForTimeout(4000);
}
function createErrorCollector() {
const errors = [];
const listener = (msg) => {
if (msg.type() === "error") {
const text = msg.text();
if (!text.includes("hydration") && !text.includes("Fast Refresh") && !text.includes("React DevTools")) {
errors.push(text.substring(0, 120));
}
}
};
return { listener, getErrors: () => [...errors] };
}
(async () => {
const browser = await chromium.launch({ headless: true });
const desktop = { width: 1440, height: 900 };
const mobile = { width: 375, height: 812 };
// ========== 1. 首页测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 1. 首页全面测试");
console.log("═══════════════════════════════════════");
{
const page = await browser.newPage({ viewport: desktop });
const errorCollector = createErrorCollector();
page.on("console", errorCollector.listener);
await waitForPage(page, BASE);
// 1a. 页面加载
const title = await page.title();
if (title && title.length > 0) {
log("首页", "pass", `页面标题: "${title}"`);
} else {
log("首页", "fail", "页面标题为空");
}
// 1b. Header 存在
const header = await page.$("header");
log("首页", header ? "pass" : "fail", "Header 元素存在性");
// 1c. Footer 存在
const footer = await page.$("footer");
log("首页", footer ? "pass" : "fail", "Footer 元素存在性");
// 1d. 布局对齐检测
const alignment = await page.evaluate(() => {
const header = document.querySelector("header .container-wide");
const footer = document.querySelector("footer .container-wide, footer .container-full");
const hRect = header?.getBoundingClientRect();
const fRect = footer?.getBoundingClientRect();
const allContainers = document.querySelectorAll(".container-wide, .container-full");
const positions = [];
allContainers.forEach((el) => {
const r = el.getBoundingClientRect();
if (r.height > 20) positions.push(Math.round(r.left));
});
return {
headerLeft: hRect ? Math.round(hRect.left) : null,
footerLeft: fRect ? Math.round(fRect.left) : null,
allLefts: positions,
uniqueLefts: [...new Set(positions)],
};
});
if (alignment.uniqueLefts.length === 1 && alignment.uniqueLefts[0] === alignment.headerLeft) {
log("首页-对齐", "pass", `所有 ${alignment.allLefts.length} 个容器 left=${alignment.headerLeft}px 对齐`);
} else {
log("首页-对齐", "fail", `容器左边距不一致: [${alignment.uniqueLefts.join(", ")}]px,期望 ${alignment.headerLeft}px`);
}
// 1e. Hero 区域
const heroH1 = await page.$("h1");
const heroText = heroH1 ? await heroH1.textContent() : "";
log("首页-Hero", heroH1 ? "pass" : "fail", `主标题: "${heroText?.substring(0, 40)}"`);
// 1f. 产品卡片
const productCards = await page.$$("[class*='product-card'], [class*='ProductCard'], a[href*='/products/']");
log("首页-产品", productCards.length > 0 ? "pass" : "warn", `产品卡片/链接数量: ${productCards.length}`);
// 1g. CTA 按钮
const ctaButtons = await page.$$("a[href='/contact'], a[href*='contact'], button:has-text('预约'), button:has-text('体验')");
log("首页-CTA", ctaButtons.length > 0 ? "pass" : "warn", `CTA 按钮数量: ${ctaButtons.length}`);
// 1h. 控制台错误
await page.waitForTimeout(2000);
if (errorCollector.getErrors().length === 0) {
log("首页-控制台", "pass", "无真实控制台错误");
} else {
log("首页-控制台", "warn", `${errorCollector.getErrors().length} 个控制台错误: ${errorCollector.getErrors()[0]?.substring(0, 60)}`);
}
await page.close();
}
// ========== 2. 产品详情页测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 2. 产品详情页测试");
console.log("═══════════════════════════════════════");
const productPages = [
{ id: "novavis", name: "睿视NovaVis" },
{ id: "erp", name: "睿管ERP" },
{ id: "crm", name: "睿客CRM" },
{ id: "bi", name: "睿析BI" },
];
for (const p of productPages) {
const page = await browser.newPage({ viewport: desktop });
const errorCollector = createErrorCollector();
page.on("console", errorCollector.listener);
try {
await waitForPage(page, `${BASE}/products/${p.id}`);
const title = await page.title();
log(p.name, title ? "pass" : "fail", `页面标题: "${title?.substring(0, 40)}"`);
// 布局对齐
const alignment = await page.evaluate(() => {
const header = document.querySelector("header .container-wide");
const hRect = header?.getBoundingClientRect();
const headerLeft = hRect ? Math.round(hRect.left) : null;
const sections = document.querySelectorAll("section");
const misaligned = [];
sections.forEach((s, i) => {
const container = s.querySelector(".container-wide, .container-full");
if (container) {
const cRect = container.getBoundingClientRect();
const cLeft = Math.round(cRect.left);
if (cLeft !== headerLeft && cRect.height > 30) {
misaligned.push({ index: i, left: cLeft });
}
}
});
return { headerLeft, misaligned, sectionCount: sections.length };
});
if (alignment.misaligned.length === 0) {
log(`${p.name}-对齐`, "pass", `所有 ${alignment.sectionCount} 个 section 容器与 Header 对齐 (left=${alignment.headerLeft})`);
} else {
log(`${p.name}-对齐`, "fail", `${alignment.misaligned.length} 个 section 未对齐: ${JSON.stringify(alignment.misaligned)}`);
}
// 内容完整性
const content = await page.evaluate(() => document.body.innerText);
const hasContent = content.length > 100;
log(`${p.name}-内容`, hasContent ? "pass" : "fail", `页面内容长度: ${content.length} 字符`);
// CTA 检测
const hasCTA = content.includes("预约") || content.includes("体验") || content.includes("联系");
log(`${p.name}-CTA`, hasCTA ? "pass" : "warn", "CTA 文案检测");
// 控制台错误
await page.waitForTimeout(1000);
if (errorCollector.getErrors().length > 0) {
log(`${p.name}-控制台`, "warn", `${errorCollector.getErrors().length} 个错误`);
} else {
log(`${p.name}-控制台`, "pass", "无真实控制台错误");
}
} catch (e) {
log(p.name, "fail", `页面加载失败: ${e.message.substring(0, 60)}`);
}
await page.close();
}
// ========== 3. 产品列表页测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 3. 产品列表页测试");
console.log("═══════════════════════════════════════");
{
const page = await browser.newPage({ viewport: desktop });
try {
await waitForPage(page, `${BASE}/products`);
const alignment = await page.evaluate(() => {
const header = document.querySelector("header .container-wide");
const hRect = header?.getBoundingClientRect();
const headerLeft = hRect ? Math.round(hRect.left) : null;
const containers = document.querySelectorAll(".container-wide, .container-full");
const positions = [];
containers.forEach((el) => {
const r = el.getBoundingClientRect();
if (r.height > 20) positions.push(Math.round(r.left));
});
return { headerLeft, uniqueLefts: [...new Set(positions)], count: positions.length };
});
if (alignment.uniqueLefts.length === 1 && alignment.uniqueLefts[0] === alignment.headerLeft) {
log("产品列表-对齐", "pass", `${alignment.count} 个容器对齐 (left=${alignment.headerLeft})`);
} else {
log("产品列表-对齐", "fail", `容器左边距不一致: [${alignment.uniqueLefts.join(", ")}]`);
}
const productLinks = await page.$$("a[href*='/products/']");
log("产品列表-卡片", productLinks.length > 0 ? "pass" : "fail", `产品链接数量: ${productLinks.length}`);
} catch (e) {
log("产品列表", "fail", `加载失败: ${e.message.substring(0, 60)}`);
}
await page.close();
}
// ========== 4. 暗色模式测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 4. 暗色模式测试");
console.log("═══════════════════════════════════════");
{
const page = await browser.newPage({ viewport: desktop });
await waitForPage(page, BASE);
// 查找主题切换按钮
const themeToggle = await page.$("button[aria-label='切换主题'], [data-theme-toggle], button[aria-label*='主题'], button[aria-label*='theme']");
if (themeToggle) {
await themeToggle.click();
await page.waitForTimeout(800);
const darkOption = await page.$("button:has-text('深色')");
if (darkOption) {
await darkOption.click();
await page.waitForTimeout(1500);
} else {
log("暗色模式", "warn", "未找到深色选项按钮");
}
const darkMode = await page.evaluate(() => {
const html = document.documentElement;
const dataTheme = html.getAttribute("data-theme");
const classList = html.classList.contains("dark");
const bgColor = getComputedStyle(document.body).backgroundColor;
return { dataTheme, classList, bgColor };
});
log("暗色模式", darkMode.dataTheme === "dark" || darkMode.classList ? "pass" : "fail",
`data-theme="${darkMode.dataTheme}", dark class=${darkMode.classList}`);
// 暗色模式下 Logo 可见性
const logoVisible = await page.evaluate(() => {
const logo = document.querySelector("header img, header svg, header .logo");
if (!logo) return { found: false };
const rect = logo.getBoundingClientRect();
const style = getComputedStyle(logo);
return {
found: true,
visible: rect.width > 0 && rect.height > 0,
filter: style.filter,
width: Math.round(rect.width),
height: Math.round(rect.height),
};
});
if (logoVisible.found) {
log("暗色-Logo", logoVisible.visible ? "pass" : "fail",
`Logo 可见: ${logoVisible.visible}, filter: ${logoVisible.filter?.substring(0, 30)}`);
} else {
log("暗色-Logo", "warn", "未找到 Logo 元素");
}
// 暗色模式布局对齐
const darkAlignment = await page.evaluate(() => {
const header = document.querySelector("header .container-wide");
const hRect = header?.getBoundingClientRect();
const headerLeft = hRect ? Math.round(hRect.left) : null;
const containers = document.querySelectorAll(".container-wide, .container-full");
const positions = [];
containers.forEach((el) => {
const r = el.getBoundingClientRect();
if (r.height > 20) positions.push(Math.round(r.left));
});
return { headerLeft, uniqueLefts: [...new Set(positions)] };
});
if (darkAlignment.uniqueLefts.length === 1 && darkAlignment.uniqueLefts[0] === darkAlignment.headerLeft) {
log("暗色-对齐", "pass", "暗色模式布局对齐正常");
} else {
log("暗色-对齐", "fail", `暗色模式容器不对齐: [${darkAlignment.uniqueLefts.join(", ")}]`);
}
// 暗色模式硬编码白色背景检测
const whiteBgSections = await page.evaluate(() => {
const sections = document.querySelectorAll("section");
const whiteBg = [];
sections.forEach((s, i) => {
const bg = getComputedStyle(s).backgroundColor;
const classes = s.className || "";
if ((bg === "rgb(255, 255, 255)" || classes.includes("bg-white")) &&
document.documentElement.getAttribute("data-theme") === "dark") {
whiteBg.push(i);
}
});
return whiteBg;
});
if (whiteBgSections.length > 0) {
log("暗色-硬编码", "warn", `${whiteBgSections.length} 个 section 在暗色模式下仍使用白色背景: [${whiteBgSections.join(", ")}]`);
} else {
log("暗色-硬编码", "pass", "未检测到暗色模式下的白色硬编码背景");
}
} else {
log("暗色模式", "warn", "未找到主题切换按钮");
}
await page.close();
}
// ========== 5. 移动端响应式测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 5. 移动端响应式测试");
console.log("═══════════════════════════════════════");
{
const page = await browser.newPage({ viewport: mobile });
await waitForPage(page, BASE);
// 移动端导航
const mobileNav = await page.evaluate(() => {
const hamburger = document.querySelector("button[aria-label*='菜单'], button[aria-label*='menu'], [class*='hamburger'], [class*='mobile-nav']");
const header = document.querySelector("header");
const headerRect = header?.getBoundingClientRect();
return {
hasHamburger: !!hamburger,
headerWidth: headerRect ? Math.round(headerRect.width) : null,
viewportWidth: window.innerWidth,
};
});
log("移动端-导航", mobileNav.hasHamburger ? "pass" : "warn",
`汉堡菜单: ${mobileNav.hasHamburger ? "有" : "无"}, header宽: ${mobileNav.headerWidth}px`);
// 移动端布局不溢出
const overflow = await page.evaluate(() => {
const bodyWidth = document.body.scrollWidth;
const vpWidth = window.innerWidth;
return { bodyWidth, vpWidth, overflow: bodyWidth > vpWidth + 2 };
});
log("移动端-溢出", !overflow.overflow ? "pass" : "fail",
`body=${overflow.bodyWidth}px, viewport=${overflow.vpWidth}px, 溢出=${overflow.overflow}`);
// 移动端产品详情页
try {
await waitForPage(page, `${BASE}/products/novavis`);
const mobileContent = await page.evaluate(() => document.body.innerText.length);
log("移动端-产品页", mobileContent > 100 ? "pass" : "fail", `NovaVis 内容: ${mobileContent} 字符`);
} catch (e) {
log("移动端-产品页", "fail", `加载失败: ${e.message.substring(0, 50)}`);
}
await page.close();
}
// ========== 6. 导航与路由测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 6. 导航与路由测试");
console.log("═══════════════════════════════════════");
{
const page = await browser.newPage({ viewport: desktop });
await waitForPage(page, BASE);
const navLinks = await page.evaluate(() => {
const links = document.querySelectorAll("header a[href], nav a[href]");
return Array.from(links).map((a) => ({
href: a.getAttribute("href"),
text: a.textContent?.trim().substring(0, 20),
})).filter(l => l.href && !l.href.startsWith("http") && !l.href.startsWith("#"));
});
const uniqueLinks = [...new Map(navLinks.map(l => [l.href, l])).values()];
log("导航-链接", "pass", `发现 ${uniqueLinks.length} 个内部导航链接`);
for (const link of uniqueLinks.slice(0, 8)) {
try {
const resp = await page.goto(`${BASE}${link.href}`, { timeout: 10000, waitUntil: "domcontentloaded" });
const status = resp?.status();
log("导航-路由", status === 200 ? "pass" : "warn", `${link.href}${status}`);
} catch (e) {
log("导航-路由", "fail", `${link.href} → 加载失败`);
}
}
await page.close();
}
// ========== 7. 解决方案/服务页面测试 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 7. 其他页面测试");
console.log("═══════════════════════════════════════");
{
const otherPages = ["/solutions", "/services", "/about", "/contact"];
const page = await browser.newPage({ viewport: desktop });
for (const path of otherPages) {
try {
const resp = await page.goto(`${BASE}${path}`, { timeout: 10000, waitUntil: "domcontentloaded" });
await page.waitForTimeout(2000);
const status = resp?.status();
if (status === 200) {
const alignment = await page.evaluate(() => {
const header = document.querySelector("header .container-wide");
const hRect = header?.getBoundingClientRect();
const headerLeft = hRect ? Math.round(hRect.left) : null;
const containers = document.querySelectorAll(".container-wide, .container-full");
const positions = [];
containers.forEach((el) => {
const r = el.getBoundingClientRect();
if (r.height > 20) positions.push(Math.round(r.left));
});
return { headerLeft, uniqueLefts: [...new Set(positions)], allAligned: [...new Set(positions)].length === 1 };
});
log(`${path}-对齐`, alignment.allAligned ? "pass" : "fail",
`left=${alignment.headerLeft}, 对齐=${alignment.allAligned}`);
log(`${path}-加载`, "pass", `HTTP ${status}`);
} else if (status === 404) {
log(`${path}`, "warn", `HTTP 404 - 页面不存在`);
} else {
log(`${path}`, "warn", `HTTP ${status}`);
}
} catch (e) {
log(`${path}`, "warn", `加载失败: ${e.message.substring(0, 40)}`);
}
}
await page.close();
}
// ========== 汇总 ==========
console.log("\n═══════════════════════════════════════");
console.log(" 📊 测试报告汇总");
console.log("═══════════════════════════════════════");
console.log(` ✅ 通过: ${RESULTS.pass}`);
console.log(` ❌ 失败: ${RESULTS.fail}`);
console.log(` ⚠️ 警告: ${RESULTS.warn}`);
console.log(` 📋 总计: ${RESULTS.pass + RESULTS.fail + RESULTS.warn}`);
if (RESULTS.fail > 0) {
console.log("\n 🔴 失败项:");
RESULTS.details.filter(d => d.status === "fail").forEach(d => {
console.log(` ❌ [${d.category}] ${d.message}`);
});
}
if (RESULTS.warn > 0) {
console.log("\n 🟡 警告项:");
RESULTS.details.filter(d => d.status === "warn").forEach(d => {
console.log(` ⚠️ [${d.category}] ${d.message}`);
});
}
await browser.close();
})();