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