929f3ed250
- IHG设计分析HTML参考页面 - 布局检查、DOM诊断、验收测试等辅助脚本
396 lines
14 KiB
Python
396 lines
14 KiB
Python
#!/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()
|