""" Novalon Website - Systematic Regression Test Suite =================================================== 覆盖范围:首页、导航、联系我们、产品页、关于我们、页脚、响应式、无障碍 执行方式:python scripts/regression_test.py """ import re import time from playwright.sync_api import sync_playwright, Page, expect BASE_URL = "http://localhost:3000" RESULTS = {"passed": 0, "failed": 0, "skipped": 0, "details": []} def record(test_name: str, passed: bool, detail: str = ""): status = "PASS" if passed else "FAIL" RESULTS["passed" if passed else "failed"] += 1 RESULTS["details"].append({"test": test_name, "status": status, "detail": detail}) icon = "✅" if passed else "❌" print(f" {icon} {test_name}{': ' + detail if detail else ''}") # ============================================================ # Test Group 1: 首页核心功能 # ============================================================ def test_homepage_load(page: Page): """首页加载与核心元素可见性""" resp = page.goto(BASE_URL) assert resp and resp.status == 200, f"HTTP {resp.status if resp else 'None'}" page.wait_for_load_state("networkidle") # Title 检查 title = page.title() record("首页Title正确", "睿新致远" in title or "novalon" in title.lower(), f"title={title}") # Header 可见 header = page.locator("header") record("Header可见", header.is_visible(), "") # Hero 区域 hero = page.locator("#home, [aria-labelledby='hero-heading']") record("Hero区域存在", hero.count() > 0, f"count={hero.count()}") # Footer 可见 footer = page.locator("[data-testid='footer'], footer") record("Footer可见", footer.first.is_visible(), "") def test_homepage_navigation_links(page: Page): """首页导航链接完整性""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") nav = page.locator( "[data-testid='desktop-navigation'], nav[aria-label='主导航'], nav" ) links = nav.locator("a") count = links.count() record(f"导航链接数量({count})", count >= 4, f"实际{count}个链接") # 验证关键链接文本 expected_links = ["首页", "产品", "服务", "方案", "联系我们"] nav_text = nav.inner_text() for link_text in expected_links: found = link_text in nav_text record(f"导航包含'{link_text}'", found, "") def test_homepage_cta_buttons(page: Page): """首页 CTA 按钮功能""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") # 咨询/联系按钮 cta_buttons = page.locator("a[href='/contact'], button:has-text('咨询')") record("CTA联系按钮存在", cta_buttons.count() > 0, f"count={cta_buttons.count()}") # ============================================================ # Test Group 2: 联系我们页面(核心转化入口) # ============================================================ def test_contact_page_load(page: Page): """联系我们页面加载与表单渲染""" page.goto(f"{BASE_URL}/contact") page.wait_for_load_state("networkidle") h1 = page.locator("h1") record("联系页面H1标题", h1.is_visible(), f"text={h1.inner_text()}") def test_contact_form_fields(page: Page): """联系表单字段完整性与验证""" page.goto(f"{BASE_URL}/contact") page.wait_for_load_state("networkidle") fields = { "name-input": "姓名", "phone-input": "电话", "email-input": "邮箱", "subject-input": "主题", "message-input": "留言内容", "submit-button": "提交按钮", } for testid, label in fields.items(): el = page.locator(f"[data-testid='{testid}']") record(f"表单字段[{label}]", el.is_visible(), "") def test_contact_form_validation(page: Page): """表单空提交验证 - 应显示错误提示""" page.goto(f"{BASE_URL}/contact") page.wait_for_load_state("networkidle") submit = page.locator("[data-testid='submit-button']") submit.click() page.wait_for_timeout(500) # Zod 验证应触发错误信息 error_patterns = [ r"至少需要.*字符", r"请输入有效", r"必填", r"required", ] body_text = page.content() has_error = any(re.search(p, body_text) for p in error_patterns) record("空提交触发验证错误", has_error, "Zod schema validation") def test_contact_honeypot_field(page: Page): """蜜罐字段防爬检查""" page.goto(f"{BASE_URL}/contact") page.wait_for_load_state("networkidle") honeypot = page.locator("input[name='website'][style*='display:none'], input[name='website'][style*='display: none'], input[name='website'][tabindex='-1']") record("蜜罐隐藏字段存在", honeypot.count() > 0, f"count={honeypot.count()}") # ============================================================ # Test Group 3: 产品与服务页面 # ============================================================ def test_products_page(page: Page): """产品列表页""" page.goto(f"{BASE_URL}/products") page.wait_for_load_state("networkidle") record("产品页HTTP 200", page.url.endswith("/products") or "/products" in page.url, f"url={page.url}") def test_services_page(page: Page): """服务列表页""" page.goto(f"{BASE_URL}/services") page.wait_for_load_state("networkidle") record("服务页HTTP 200", "/services" in page.url, f"url={page.url}") def test_solutions_page(page: Page): """解决方案页""" page.goto(f"{BASE_URL}/solutions") page.wait_for_load_state("networkidle") record("方案页HTTP 200", "/solutions" in page.url, f"url={page.url}") def test_about_page(page: Page): """关于我们页面""" page.goto(f"{BASE_URL}/about") page.wait_for_load_state("networkidle") record("关于页HTTP 200", "/about" in page.url, f"url={page.url}") # ============================================================ # Test Group 4: 页脚与法律页面 # ============================================================ def test_footer_content(page: Page): """页脚内容完整性""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") footer = page.locator("[data-testid='footer'], footer").first footer.scroll_into_view_if_needed() text = footer.inner_text() checks = [ ("ICP备案号", "ICP备" in text or "蜀ICP" in text), ("隐私政策链接", footer.locator("a[href='/privacy']").count() > 0), ("服务条款链接", footer.locator("a[href='/terms']").count() > 0), ("公司描述", len(text) > 50), ] for name, passed in checks: record(f"Footer-{name}", passed, "") def test_privacy_page(page: Page): """隐私政策页""" page.goto(f"{BASE_URL}/privacy") page.wait_for_load_state("networkidle") record("隐私政策页可访问", "privacy" in page.url, f"url={page.url}") def test_terms_page(page: Page): """服务条款页""" page.goto(f"{BASE_URL}/terms") page.wait_for_load_state("networkidle") record("服务条款页可访问", "terms" in page.url, f"url={page.url}") # ============================================================ # Test Group 5: 响应式设计 (Mobile) # ============================================================ def test_mobile_responsive(page: Page): """移动端响应式布局""" page.set_viewport_size({"width": 375, "height": 667}) page.goto(BASE_URL) page.wait_for_load_state("networkidle") # Header 在移动端仍可见 header = page.locator("header") record("移动端-Header可见", header.is_visible(), "") # 移动端菜单按钮出现 mobile_btn = page.locator("[data-testid='mobile-menu-button']") record("移动端-汉堡菜单按钮", mobile_btn.is_visible(timeout=5000), "") # 点击展开移动端导航 mobile_btn.click() page.wait_for_timeout(500) mobile_nav = page.locator("[data-testid='mobile-navigation']") record("移动端-导航面板展开", mobile_nav.is_visible(timeout=5000), "") # 重置视口 page.set_viewport_size({"width": 1280, "height": 720}) # ============================================================ # Test Group 6: SEO 与元数据 # ============================================================ def test_seo_metadata(page: Page): """SEO 元数据检查""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") # Meta description desc = page.locator("meta[name='description']").get_attribute("content") or "" record("Meta Description存在", len(desc) > 20, f"len={len(desc)}") # Canonical URL canonical = page.locator("link[rel='canonical']").get_attribute("href") or "" record("Canonical标签存在", "novalon" in canonical.lower(), f"href={canonical}") # OG 标签 og_title = page.locator("meta[property='og:title']").get_attribute("content") or "" record("OG Title存在", len(og_title) > 0, f"value={og_title[:30]}") def test_structured_data(page: Page): **Structured Data (JSON-LD)** page.goto(BASE_URL) page.wait_for_load_state('networkidle') json_ld_scripts = page.evaluate("""() => { const scripts = document.querySelectorAll('script[type="application/ld+json"]'); return Array.from(scripts).map(s => { try { return JSON.parse(s.textContent); } catch(e) { return null; } }).filter(Boolean); }""") record("JSON-LD结构化数据", len(json_ld_scripts) >= 2, f"count={len(json_ld_scripts)}") # ============================================================ # Test Group 7: 无障碍访问 (A11y) # ============================================================ def test_accessibility_basic(page: Page): """基础无障碍检查""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") # Skip navigation link skip_link = page.locator("a.sr-only, a[href='#main-content']") record("Skip Navigation链接", skip_link.count() >= 1, f"count={skip_link.count()}") # Language attribute html_lang = page.locator("html").get_attribute("lang") or "" record("HTML lang属性", html_lang.startswith("zh"), f"lang={html_lang}") # Main content landmark main = page.locator("main, [role='main']") record("Main landmark", main.count() >= 1, f"count={main.count()}") def test_accessibility_images(page: Page): """图片 Alt 文本检查""" page.goto(BASE_URL) page.wait_for_load_state("networkidle") images_without_alt = page.evaluate("""() => { const imgs = document.querySelectorAll('img'); const missing = []; imgs.forEach((img, i) => { const alt = img.getAttribute('alt'); if (!alt || alt.trim() === '') { missing.push({src: img.src?.substring(0, 60), index: i}); } }); return missing; }""") record( "所有图片有Alt属性", len(images_without_alt) == 0, f"{len(images_without_alt)}张缺失Alt" if images_without_alt else "", ) # ============================================================ # Test Group 8: 控制台错误检测 # ============================================================ def test_console_errors(page: Page): """控制台 JS 错误检测""" errors = [] page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None) page.goto(BASE_URL) page.wait_for_load_state("networkidle") # 过滤掉非关键错误(如第三方资源) critical_errors = [ e for e in errors if not any(skip in e for skip in ["favicon", "ga_global", "adsbygoogle", "cookie"]) ] record( "无关键JS错误", len(critical_errors) == 0, f"{len(critical_errors)}个错误: {critical_errors[:2]}" if critical_errors else "", ) # ============================================================ # Test Runner # ============================================================ def run_all(): print("=" * 70) print(" Novalon Website - 系统性回归测试套件") print(f" Target: {BASE_URL}") print(f" Time: {time.strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 70) with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( viewport={"width": 1280, "height": 720}, locale="zh-CN", ) page = context.new_page() test_groups = [ ("\n--- [G1] 首页核心功能 ---", [ test_homepage_load, test_homepage_navigation_links, test_homepage_cta_buttons, ]), ("\n--- [G2] 联系我们页面 ---", [ test_contact_page_load, test_contact_form_fields, test_contact_form_validation, test_contact_honeypot_field, ]), ("\n--- [G3] 产品/服务/方案页面 ---", [ test_products_page, test_services_page, test_solutions_page, test_about_page, ]), ("\n--- [G4] 页脚与法律页面 ---", [ test_footer_content, test_privacy_page, test_terms_page, ]), ("\n--- [G5] 响应式设计 ---", [ test_mobile_responsive, ]), ("\n--- [G6] SEO与元数据 ---", [ test_seo_metadata, test_structured_data, ]), ("\n--- [G7] 无障碍访问 ---", [ test_accessibility_basic, test_accessibility_images, ]), ("\n--- [G8] 控制台错误 ---", [ test_console_errors, ]), ] for group_name, tests in test_groups: print(group_name) for test_fn in tests: try: test_fn(page) except Exception as e: record(test_fn.__name__, False, f"异常: {str(e)[:80]}") browser.close() # Summary total = RESULTS["passed"] + RESULTS["failed"] print("\n" + "=" * 70) print(f" 测试结果汇总 | 总计: {total} | 通过: {RESULTS['passed']} | 失败: {RESULTS['failed']}") print(f" 通过率: {RESULTS['passed']/total*100:.1f}%" if total > 0 else " N/A") print("=" * 70) if RESULTS["failed"] > 0: print("\n 失败用例详情:") for d in RESULTS["details"]: if d["status"] == "FAIL": print(f" ❌ {d['test']}: {d['detail']}") return RESULTS["failed"] == 0 if __name__ == "__main__": success = run_all() exit(0 if success else 1)