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