diff --git a/.superpowers/brainstorm/39838-1777791603/.server.log b/.superpowers/brainstorm/39838-1777791603/.server.log
new file mode 100644
index 0000000..c19f633
--- /dev/null
+++ b/.superpowers/brainstorm/39838-1777791603/.server.log
@@ -0,0 +1,12 @@
+{"type":"server-started","port":53000,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:53000","screen_dir":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/animation-style.html"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-updated","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-animation-demos.html"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-ink-demos.html"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/39838-1777791603/hero-refined-demos.html"}
+{"type":"server-stopped","reason":"idle timeout"}
diff --git a/.superpowers/brainstorm/39838-1777791603/.server.pid b/.superpowers/brainstorm/39838-1777791603/.server.pid
new file mode 100644
index 0000000..2d92403
--- /dev/null
+++ b/.superpowers/brainstorm/39838-1777791603/.server.pid
@@ -0,0 +1 @@
+39911
diff --git a/.superpowers/brainstorm/71919-1777542709/.server.log b/.superpowers/brainstorm/71919-1777542709/.server.log
new file mode 100644
index 0000000..418400b
--- /dev/null
+++ b/.superpowers/brainstorm/71919-1777542709/.server.log
@@ -0,0 +1,17 @@
+{"type":"server-started","port":49579,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:49579","screen_dir":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709/atlassian-comparison.html"}
+{"source":"user-event","type":"click","text":"5\n \n 设计系统驱动一致性\n Atlassian Design System 统一了 Token、组件、模式三层。从字体(Atlassian Sans)、图标(1.5px stroke)、颜色(中性偏暖)到组件(按钮/标签/卡片),全产品线一致。Novalon 目前有设计变量但缺乏系统化组件库。","choice":"design-system","id":null,"timestamp":1777542883049}
+{"source":"user-event","type":"click","text":"1\n \n 产品矩阵导航\n 顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按\"产品\"或\"场景\"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。","choice":"product-matrix","id":null,"timestamp":1777542885404}
+{"source":"user-event","type":"click","text":"1\n \n 产品矩阵导航\n 顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按\"产品\"或\"场景\"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。","choice":"product-matrix","id":null,"timestamp":1777542923363}
+{"source":"user-event","type":"click","text":"5\n \n 设计系统驱动一致性\n Atlassian Design System 统一了 Token、组件、模式三层。从字体(Atlassian Sans)、图标(1.5px stroke)、颜色(中性偏暖)到组件(按钮/标签/卡片),全产品线一致。Novalon 目前有设计变量但缺乏系统化组件库。","choice":"design-system","id":null,"timestamp":1777542928409}
+{"source":"user-event","type":"click","text":"1\n \n 产品矩阵导航\n 顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按\"产品\"或\"场景\"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。","choice":"product-matrix","id":null,"timestamp":1777542929579}
+{"source":"user-event","type":"click","text":"3\n \n 场景化入口\n \"Get started with a template\" — Scrum / Bug Tracking / DevOps 等场景模板,让用户按自身需求快速找到切入点。Novalon 的解决方案页面目前是静态描述,缺乏场景引导。","choice":"scenario-entry","id":null,"timestamp":1777542951112}
+{"source":"user-event","type":"click","text":"1\n \n 产品矩阵导航\n 顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按\"产品\"或\"场景\"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。","choice":"product-matrix","id":null,"timestamp":1777542952419}
+{"source":"user-event","type":"click","text":"2\n \n 社会证明前置\n Hero 之后紧跟\"300K+ 公司 / 80% Fortune 500\"数据条。Novalon 的数据统计在 Hero 底部,且缺乏大客户 Logo 墙和具体量化成果展示。","choice":"social-proof","id":null,"timestamp":1777543514788}
+{"source":"user-event","type":"click","text":"1\n \n 产品矩阵导航\n 顶部导航下拉展示所有产品,每个产品有独立落地页。用户可按\"产品\"或\"场景\"两条路径发现内容。Novalon 当前将产品、服务、解决方案平铺在单页中,缺乏独立深度。","choice":"product-matrix","id":null,"timestamp":1777543918517}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709/brand-direction-comparison.html"}
+{"source":"user-event","type":"click","text":"数字化转型\n 睿新致遠\n 智连未来,成长伙伴\n 预约演示\n \n \n \n \n B — 收敛但保留印记\n 白底为主 + 极简装饰线条 + 书法标题保留 + 朱砂红品牌色。克制专业,品牌锚点仍在。","choice":"b","id":null,"timestamp":1777544379412}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709/delivery-pace-comparison.html"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709/homepage-layout-options.html"}
+{"type":"screen-added","file":"/Users/zhangxiang/Codes/Novalon/novalon-website/.superpowers/brainstorm/71919-1777542709/design-spec-full.html"}
+{"type":"server-stopped","reason":"idle timeout"}
diff --git a/.superpowers/brainstorm/71919-1777542709/.server.pid b/.superpowers/brainstorm/71919-1777542709/.server.pid
new file mode 100644
index 0000000..b82facb
--- /dev/null
+++ b/.superpowers/brainstorm/71919-1777542709/.server.pid
@@ -0,0 +1 @@
+71992
diff --git a/.superpowers/brainstorm/ihg-design-analysis.html b/.superpowers/brainstorm/ihg-design-analysis.html
new file mode 100644
index 0000000..cedc4b8
--- /dev/null
+++ b/.superpowers/brainstorm/ihg-design-analysis.html
@@ -0,0 +1,250 @@
+
+
+
+
+
+ IHG设计模式分析 - Novalon产品架构
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Product Portfolio
+ Novalon 产品品牌矩阵
+ 从企业级数字化到智能化转型,我们的产品矩阵覆盖不同规模、不同阶段的企业需求,为您提供量身定制的解决方案
+
+
+
+
+
+
+
+
+
+
+
+
企业旗舰系列
+
为大型企业和集团客户打造的全栈数字化解决方案,以深度业务理解和行业最佳实践为基础,助力企业实现卓越运营
+
+
+
+
+
+
+
+
产品描述
+
核心功能
+
+ - 功能特性一
+ - 功能特性二
+ - 功能特性三
+
+
产品优势
+
产品优势描述
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
设计模式展示
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/check_layout.js b/scripts/check_layout.js
new file mode 100644
index 0000000..78b43f7
--- /dev/null
+++ b/scripts/check_layout.js
@@ -0,0 +1,65 @@
+const { chromium } = require("playwright");
+(async () => {
+ const browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
+
+ try {
+ await page.goto("http://localhost:3001/products/novavis", { waitUntil: "domcontentloaded", timeout: 15000 });
+ await page.waitForTimeout(4000);
+
+ const result = await page.evaluate(() => {
+ const header = document.querySelector("header .container-wide");
+ const headerRect = header?.getBoundingClientRect();
+ const headerContentLeft = headerRect ? headerRect.left : null;
+
+ const sections = document.querySelectorAll("section");
+ const sectionData = [];
+
+ sections.forEach((section, i) => {
+ const rect = section.getBoundingClientRect();
+ if (rect.height < 30) return;
+
+ const container = section.querySelector(".container-wide, .container-full");
+ const containerRect = container?.getBoundingClientRect();
+
+ const firstTextBlock = section.querySelector("h1, h2, h3, p");
+ const textRect = firstTextBlock?.getBoundingClientRect();
+
+ sectionData.push({
+ index: i,
+ sectionLeft: Math.round(rect.left),
+ containerLeft: containerRect ? Math.round(containerRect.left) : null,
+ firstTextLeft: textRect ? Math.round(textRect.left) : null,
+ height: Math.round(rect.height),
+ text: firstTextBlock?.textContent?.substring(0, 40).trim(),
+ });
+ });
+
+ return { headerContentLeft, sections: sectionData };
+ });
+
+ console.log("=== NovaVis 产品页内容对齐检测 ===\n");
+ console.log(`Header container left: ${result.headerContentLeft}px\n`);
+
+ let allAligned = true;
+ for (const s of result.sections) {
+ const containerOk = s.containerLeft === result.headerContentLeft;
+ const textOk = s.firstTextLeft !== null && Math.abs(s.firstTextLeft - result.headerContentLeft) < 5;
+
+ if (!containerOk && s.containerLeft !== null) {
+ allAligned = false;
+ console.log(` ❌ Section[${s.index}]: containerLeft=${s.containerLeft} (期望 ${result.headerContentLeft}) | ${s.text?.substring(0, 30)}`);
+ } else if (s.containerLeft !== null) {
+ console.log(` ✅ Section[${s.index}]: containerLeft=${s.containerLeft} | ${s.text?.substring(0, 30)}`);
+ }
+ }
+
+ if (allAligned) {
+ console.log("\n🎉 所有区块容器与 Header 对齐!");
+ }
+
+ } catch(e) {
+ console.error("ERROR:", e.message);
+ }
+ await browser.close();
+})();
diff --git a/scripts/dogfood_test.py b/scripts/dogfood_test.py
new file mode 100644
index 0000000..4d9658c
--- /dev/null
+++ b/scripts/dogfood_test.py
@@ -0,0 +1,395 @@
+#!/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()
diff --git a/scripts/dom_diagnostic.py b/scripts/dom_diagnostic.py
new file mode 100644
index 0000000..c5f3ac7
--- /dev/null
+++ b/scripts/dom_diagnostic.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+"""Deep DOM Diagnostic v2"""
+from playwright.sync_api import sync_playwright
+
+BASE_URL = "http://localhost:3001"
+
+with sync_playwright() as p:
+ browser = p.chromium.launch(headless=True)
+ page = browser.new_page(viewport={"width": 1280, "height": 900})
+
+ console_logs = []
+ page.on("console", lambda msg: console_logs.append(f"{msg.type}: {msg.text[:150]}"))
+
+ # Navigate directly to CTA area
+ page.goto(BASE_URL + "/#cta")
+ page.wait_for_load_state("networkidle")
+ page.wait_for_timeout(2000) # Extra wait for dynamic imports
+
+ print("=== CTA SECTION DEEP PROBE ===")
+ cta = page.locator("#cta")
+ print(f"#cta exists: {cta.count() > 0}")
+ if cta.count() > 0:
+ html = cta.first.inner_html()
+ print(f"#cta innerHTML length: {len(html)}")
+ print(f"#cta innerHTML (first 500 chars): {html[:500]}")
+
+ # Check for anchor tags
+ anchors = cta.locator("a")
+ print(f"\n#cta tags: {anchors.count()}")
+ for i in range(anchors.count()):
+ a = anchors.nth(i)
+ txt = a.inner_text().strip()
+ href = a.get_attribute("href") or ""
+ cls = a.get_attribute("class") or ""
+ print(f" a[{i}]: text='{txt}' href={href} class={cls[:80]}")
+
+ # Check all clickable elements
+ clickables = cta.locator("a, button, [role='button'], [role='link']")
+ print(f"\n#cta all clickables: {clickables.count()}")
+
+ print("\n=== WHY-US DEEP PROBE ===")
+ whyus = page.locator("#why-us")
+ print(f"#why-us exists: {whyus.count() > 0}")
+ if whyus.count() > 0:
+ anchors = whyus.locator("a")
+ print(f"#why-us tags: {anchors.count()}")
+ for i in range(anchors.count()):
+ a = anchors.nth(i)
+ txt = a.inner_text().strip()
+ href = a.get_attribute("href") or ""
+ print(f" a[{i}]: text='{txt}' href={href}")
+
+ print("\n=== HEADER CONSULT DEEP PROBE ===")
+ header = page.locator("header")
+ header_anchors = header.locator("a")
+ print(f"header tags: {header_anchors.count()}")
+ for i in range(header_anchors.count()):
+ a = header_anchors.nth(i)
+ txt = a.inner_text().strip()[:30]
+ href = a.get_attribute("href") or ""
+ aria = a.get_attribute("aria-label") or ""
+ if '咨询' in txt or '咨询' in aria or 'contact' in href:
+ print(f" CONSULT a[{i}]: text='{txt}' href={href} aria={aria}")
+
+ header_buttons = header.locator("button")
+ print(f"header