#!/usr/bin/env python3 """Novalon Website Dogfood Test v3 - Final""" import os import re import sys from playwright.sync_api import sync_playwright BASE_URL = "http://localhost:3001" SCREENSHOT_DIR = "/tmp/novalon-dogfood" os.makedirs(SCREENSHOT_DIR, exist_ok=True) results = {"passed": 0, "failed": 0, "warnings": [], "errors": []} def log(test_name: str, passed: bool, detail: str = ""): status = "✅ PASS" if passed else "❌ FAIL" if passed: results["passed"] += 1 else: results["failed"] += 1 results["errors"].append(f"{test_name}: {detail}") print(f" [{status}] {test_name} {f'— {detail}' if detail else ''}") def wait_for_dynamic_content(page, timeout_ms=3000): """Wait for dynamically-imported sections to hydrate""" page.wait_for_timeout(timeout_ms) def test_homepage_full_render(page): """Dogfood-1: 首页全量渲染""" print("\n" + "=" * 60) print("📋 Dogfood-1: 首页全量渲染测试") print("=" * 60) page.goto(BASE_URL) page.wait_for_load_state("networkidle") wait_for_dynamic_content(page) page.screenshot(path=f"{SCREENSHOT_DIR}/01-homepage-full.png", full_page=True) log("首页完整截图保存", True) sections = [ ("Hero Section", "#home"), ("Product Matrix", "#products"), ("Challenge Section", "#challenges"), ("Social Proof", "#social-proof"), ("Why Us (NEW)", "#why-us"), ("CTA Section", "#cta"), ] for name, selector in sections: el = page.locator(selector) visible = el.count() > 0 and el.first.is_visible() log(f"Section 可见性: {name}", visible, f"selector={selector}") body_text = page.locator("body").inner_text() for keyword in ["产品矩阵", "为什么选择我们"]: found = keyword in body_text log(f"关键词存在: '{keyword}'", found) def test_navigation_system(page): """Dogfood-2: 导航系统""" print("\n" + "=" * 60) print("📋 Dogfood-2: 导航系统测试") print("=" * 60) page.goto(BASE_URL) page.wait_for_load_state("networkidle") wait_for_dynamic_content(page) nav = page.locator('nav[aria-label="主导航"]') log("桌面导航可见", nav.count() > 0 and nav.is_visible()) btns = nav.locator("button") links = nav.locator("a") total_nav = btns.count() + links.count() log(f"导航项总数 >= 5 (btn={btns.count()} + link={links.count()})", total_nav >= 5, f"实际={total_nav}") logo = page.locator('a[aria-label="返回首页"] img') log("Logo 存在且可见", logo.count() > 0 and logo.first.is_visible()) dropdowns = nav.locator('[aria-haspopup="true"]') dd_count = dropdowns.count() log(f"MegaDropdown 数量 >= 1", dd_count >= 1, f"实际={dd_count}") if dd_count > 0: dropdowns.first.hover() page.wait_for_timeout(1000) page.screenshot(path=f"{SCREENSHOT_DIR}/02-dropdown-open.png") dd_visible = False for sel in [ '[data-state="open"]', '[data-expanded="true"]', 'div[class*="dropdown"][class*="absolute"]', 'div[class*="mega"]', ]: try: el = page.locator(sel) if el.count() > 0 and el.first.is_visible(): dd_visible = True break except Exception: continue if not dd_visible: body_before = page.evaluate("document.body.innerText.length") dd_text = page.locator('header').get_by_text(re.compile("ERP|CRM|CMS|企业套装")) dd_visible = dd_text.count() > 3 log("MegaDropdown 展开后有可见内容", dd_visible) contact_link = page.locator('header').get_by_text(re.compile("联系")) log("头部存在'联系'相关链接/按钮", contact_link.count() > 0, f"实际={contact_link.count()}") def test_product_cards(page): """Dogfood-3: 产品卡片状态验证""" print("\n" + "=" * 60) print("📋 Dogfood-3: 产品卡片状态验证") print("=" * 60) page.goto(BASE_URL + "/#products") page.wait_for_load_state("networkidle") wait_for_dynamic_content(page) cards = page.locator("#products").locator('a[href*="/products/"]') card_count = cards.count() log(f"产品卡片数量 = 6", card_count == 6, f"实际={card_count}") page.screenshot(path=f"{SCREENSHOT_DIR}/03-product-cards.png") status_badges = page.locator("#products").get_by_text(re.compile("研发中|内测|已发布")) badge_count = status_badges.count() log(f"状态标签数量 >= 6", badge_count >= 6, f"实际={badge_count}") dev_indicators = page.locator("#products").get_by_text("正在积极开发") dev_count = dev_indicators.count() log(f"'正在积极开发'提示条数量 = 6", dev_count == 6, f"实际={dev_count}") section_status = page.locator("#products").get_by_text(re.compile("全部研发中")) log("Section '全部研发中' 标签可见", section_status.count() > 0) if card_count > 0: cards.first.hover() page.wait_for_timeout(300) log("产品卡片 hover 无崩溃", True) def wait_for_text(page, text, timeout_ms=5000): """Wait for specific text to appear in body (handles dynamic hydration)""" try: page.wait_for_function( f"document.body.innerText.includes({text!r})", timeout=timeout_ms, ) return True except Exception: return False def test_cta_and_whyus(page): """Dogfood-4: CTA + WhyUs 新增Section""" print("\n" + "=" * 60) print("📋 Dogfood-4: CTA + WhyUs 新功能验证") print("=" * 60) page.goto(BASE_URL) page.wait_for_load_state("networkidle") whyus = page.locator("#why-us") log("WhyUs Section 存在", whyus.count() > 0) has_whyus_text = wait_for_text(page, "为什么选择我们", 5000) log("WhyUs 动态内容已水合", has_whyus_text) page.evaluate("document.getElementById('why-us')?.scrollIntoView({ behavior: 'instant' })") page.wait_for_timeout(800) page.screenshot(path=f"{SCREENSHOT_DIR}/04-whyus-section.png") pillar_titles = ["12年+", "大型 IT 企业背景", "复合型技术团队", "全栈技术能力", "结果导向交付"] for title in pillar_titles: found = whyus.get_by_text(title).count() > 0 log(f" 支柱标题: '{title}'", found) team_text = whyus.get_by_text(re.compile("了解核心|核心团队|/team")) log("'了解核心团队' 链接文本存在", team_text.count() > 0) cta = page.locator("#cta") log("CTA Section 存在", cta.count() > 0) has_cta_text = wait_for_text(page, "一起聊聊", 5000) log("CTA 动态内容已水合", has_cta_text) page.evaluate("document.getElementById('cta')?.scrollIntoView({ behavior: 'instant' })") page.wait_for_timeout(800) page.screenshot(path=f"{SCREENSHOT_DIR}/04a-cta-section.png") cta_title = cta.get_by_text(re.compile("一起聊聊.*需求")) log("CTA 标题文案正确", cta_title.count() > 0) cta_primary = cta.get_by_text(re.compile("预约.*咨询")) if cta_primary.count() > 0: log("CTA 主按钮含'预约.*咨询'文本", True) else: body_cta = page.locator("body").get_by_text(re.compile("预约.*咨询")) log("CTA 主按钮含'预约.*咨询'文本 (body级)", body_cta.count() > 0) trust_badges = ["免费咨询", "30 分钟内响应", "无销售压力"] for badge in trust_badges: found = cta.get_by_text(badge).count() > 0 log(f" 信任徽章: '{badge}'", found) def test_responsive_layout(page): """Dogfood-5: 响应式布局""" print("\n" + "=" * 60) print("📋 Dogfood-5: 响应式布局测试") print("=" * 60) viewports = [ ("Desktop 1280px", 1280, 900), ("Tablet 768px", 768, 1024), ("Mobile 375px", 375, 812), ] for name, w, h in viewports: page.set_viewport_size({"width": w, "height": h}) page.goto(BASE_URL) page.wait_for_load_state("networkidle") page.wait_for_timeout(800) safe_name = name.replace(" ", "-").replace("/", "_") page.screenshot(path=f"{SCREENSHOT_DIR}/05-responsive-{safe_name}.png", full_page=True) log(f"{name} 截图成功", True) if w < 768: mobile_btn = page.locator('button[aria-label*="菜单"], button[aria-label*="menu"]') is_mobile = mobile_btn.count() > 0 log(f" {name}: 移动端菜单按钮存在", is_mobile) if is_mobile: mobile_btn.first.click() page.wait_for_timeout(500) page.screenshot(path=f"{SCREENSHOT_DIR}/05-mobile-open-{w}.png") log(f" {name}: 移动端菜单展开截图", True) else: nav_area = page.locator('nav[aria-label*="导航"]') visible = nav_area.count() > 0 and nav_area.first.is_visible() log(f" {name}: 桌面导航可见", visible) page.set_viewport_size({"width": 1280, "height": 900}) def test_theme_toggle(page): """Dogfood-6: 主题切换""" print("\n" + "=" * 60) print("📋 Dogfood-6: 主题切换 + 暗色模式") print("=" * 60) page.set_viewport_size({"width": 1280, "height": 900}) page.goto(BASE_URL) page.wait_for_load_state("networkidle") wait_for_dynamic_content(page) toggle = None for sel in ['button[aria-label*="切换"]', 'button[aria-label*="theme"]', '[data-testid="theme-toggle"]', 'button[class*="theme"]']: t = page.locator(sel) if t.count() > 0: toggle = t.first break log("主题切换按钮存在", toggle is not None) if toggle: page.screenshot(path=f"{SCREENSHOT_DIR}/06-theme-light.png", full_page=True) toggle.click() page.wait_for_timeout(500) dark_option = page.get_by_text(re.compile("深色|Dark")) if dark_option.count() > 0: dark_option.first.click() page.wait_for_timeout(600) else: toggle.click() page.screenshot(path=f"{SCREENSHOT_DIR}/06-theme-dark.png", full_page=True) html_data_theme = page.locator("html").get_attribute("data-theme") or "" log("Dark 模式已激活 (data-theme)", html_data_theme == "dark", f"data-theme={html_data_theme}") def test_page_navigation_and_console(page): """Dogfood-7: 页面导航 + 控制台错误扫描""" print("\n" + "=" * 60) print("📋 Dogfood-7: 页面导航 + 控制台错误扫描") print("=" * 60) console_msgs = [] def on_console(msg): if msg.type in ["error", "warning"]: console_msgs.append({"type": msg.type, "text": msg.text}) page.on("console", on_console) pages_to_test = [("/", "首页"), ("/team", "团队页面"), ("/contact", "联系页面"), ("/about", "关于页面")] for path, name in pages_to_test: try: resp = page.goto(BASE_URL + path, timeout=15000) page.wait_for_load_state("networkidle") page.wait_for_timeout(600) status = resp.status if resp else "N/A" safe_name = name.replace(" ", "-") page.screenshot(path=f"{SCREENSHOT_DIR}/07-nav-{safe_name}.png", full_page=True) ok = status == 200 and page.locator("body").inner_text() != "" log(f"页面: {name} ({path}) — HTTP {status}", ok) except Exception as e: log(f"页面: {name} ({path})", False, str(e)[:100]) errors = [m for m in console_msgs if m["type"] == "error"] real_errors = [e for e in errors if not any( p in e["text"] for p in [ "Expected server HTML to contain a matching", "Hydration failed", "There was an error while hydrating", "lucide-react", ] )] log(f"控制台真实 Error 数量 = 0", len(real_errors) == 0, f"实际={len(real_errors)}") for e in real_errors[:5]: results["errors"].append(f"CONSOLE_ERROR: {e['text'][:200]}") warnings = [m for m in console_msgs if m["type"] == "warning"] ignore = ["Each child in a list should have a unique", "Prop `className` did not match", "Download the React DevTools", "HMR", "Expected server HTML to contain a matching", "Hydration failed", "There was an error while hydrating", "lucide-react"] fw = [w for w in warnings if not any(p in w["text"] for p in ignore)] log(f"控制台 Warning(过滤后)<= 5", len(fw) <= 5, f"实际={len(fw)}") for w in fw[:5]: results["warnings"].append(w["text"][:200]) def main(): print("🐕 Novalon Website Dogfood Test Suite v3") print(f" Target: {BASE_URL}") print(f" Screenshots: {SCREENSHOT_DIR}/") print("=" * 60) with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1280, "height": 900}, locale="zh-CN") page = context.new_page() try: test_homepage_full_render(page) test_navigation_system(page) test_product_cards(page) test_cta_and_whyus(page) test_responsive_layout(page) test_theme_toggle(page) test_page_navigation_and_console(page) except Exception as e: print(f"\n💥 测试套件异常中断: {e}") page.screenshot(path=f"{SCREENSHOT_DIR}/error-crash.png") results["errors"].append(f"SUITE_CRASH: {e}") finally: browser.close() print("\n" + "=" * 60) print("📊 DOGFOOD TEST SUMMARY") print("=" * 60) total = results["passed"] + results["failed"] rate = (results["passed"] / total * 100) if total > 0 else 0 print(f" 总计: {total} | ✅ 通过: {results['passed']} | ❌ 失败: {results['failed']}") print(f" 通过率: {rate:.1f}%") if results["errors"]: print(f"\n ❌ 失败项 ({len(results['errors'])}):") for err in results["errors"]: print(f" • {err}") if results["warnings"]: print(f"\n ⚠️ 警告 ({len(results['warnings'])}):") for w in results["warnings"]: print(f" • {w[:120]}") print(f"\n 📸 截图目录: {SCREENSHOT_DIR}/") print("=" * 60) sys.exit(0 if results["failed"] == 0 else 1) if __name__ == "__main__": main()