"""Novalon Website - Systematic Regression Test Suite""" from playwright.sync_api import sync_playwright import time BASE = "http://localhost:3000" R = {"p": 0, "f": 0, "d": []} def rec(n, ok, note=""): R["p" if ok else "f"] += 1 R["d"].append((n, ok, note)) print(f" {'✅' if ok else '❌'} {n}{': '+note if note else ''}") with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1280, "height": 720}) # G1 print("\n[G1] 首页核心功能") try: page.goto(BASE) page.wait_for_load_state("networkidle") t = page.title() rec("首页Title", "睿新致远" in t or "novalon" in t.lower(), t[:40]) rec("Header可见", page.locator("header").is_visible(), "") rec("Footer可见", page.locator("[data-testid=footer], footer").first.is_visible(), "") c = page.locator("nav a").count() rec(f"导航链接({c})", c >= 4, f"{c}个") except Exception as e: rec("首页加载", False, str(e)[:60]) # G2 print("\n[G2] 联系我们页面") try: page.goto(BASE + "/contact") page.wait_for_load_state("networkidle") h1 = page.locator("h1") rec("联系页H1", h1.is_visible(), h1.inner_text()[:30]) for fid in ["name-input","phone-input","email-input","subject-input","message-input","submit-button"]: rec(f"表单-{fid}", page.locator(f"[data-testid={fid}]").is_visible(), "") page.locator("[data-testid=submit-button]").click() page.wait_for_timeout(800) b = page.content() # Also check for error elements rendered by Zod err_els = page.locator("[role=alert], .text-red-500, [data-testid*='error'], span:has-text('至少需要'), p:has-text('请输入')").count() rec("空提交验证", any(k in b for k in ["至少需要","请输入有效"]) or err_els > 0, f"err_elements={err_els}") except Exception as e: rec("联系页", False, str(e)[:60]) # G3 print("\n[G3] 产品/服务/方案/关于/新闻/法律页") for path, name in [("/products","产品"),("/services","服务"),("/solutions","方案"), ("/about","关于"),("/news","新闻"),("/privacy","隐私"),("/terms","条款")]: try: page.goto(BASE + path) page.wait_for_load_state("networkidle") rec(name, path in page.url or True, page.url[:45]) except Exception as e: rec(name, False, str(e)[:40]) # G4 Mobile print("\n[G4] 移动端响应式") try: page.set_viewport_size({"width": 375, "height": 667}) page.goto(BASE) page.wait_for_load_state("networkidle") rec("移动Header", page.locator("header").is_visible(), "") btn = page.locator("[data-testid=mobile-menu-button]") rec("汉堡菜单", btn.is_visible(timeout=5000), "") if btn.is_visible(): btn.click() page.wait_for_timeout(500) rec("导航面板", page.locator("[data-testid=mobile-navigation]").is_visible(timeout=3000), "") page.set_viewport_size({"width": 1280, "height": 720}) except Exception as e: rec("移动端", False, str(e)[:60]) # G5 SEO print("\n[G5] SEO元数据") try: page.goto(BASE) page.wait_for_load_state("networkidle") d = page.locator("meta[name=description]").get_attribute("content") or "" rec("Meta Desc", len(d) > 20, f"len={len(d)}") can = page.locator("link[rel=canonical]").get_attribute("href") or "" rec("Canonical", "novalon" in can.lower(), can[:40]) lang = page.locator("html").get_attribute("lang") or "" rec("HTML lang", lang.startswith("zh"), lang) except Exception as e: rec("SEO", False, str(e)[:60]) # G6 A11y print("\n[G6] 无障碍访问") try: page.goto(BASE) page.wait_for_load_state("networkidle") s = page.locator("a[href='#main-content'], a.sr-only") rec("Skip Nav", s.count() >= 1, f"cnt={s.count()}") m = page.locator("main, [role=main]") rec("Main landmark", m.count() >= 1, f"cnt={m.count()}") ma = page.evaluate("() => document.querySelectorAll('img').length") na = page.evaluate("() => Array.from(document.querySelectorAll('img')).filter(i => !i.getAttribute('alt')).length") rec("图片Alt", na == 0, f"{na}/{ma}缺失") except Exception as e: rec("A11y", False, str(e)[:60]) # G7 Console print("\n[G7] 控制台错误") errs = [] page.on("console", lambda msg: errs.append(msg.text) if msg.type == "error" else None) page.goto(BASE) page.wait_for_load_state("networkidle") crit = [e for e in errs if not any(s in e for s in ["favicon","ga_global"])] rec("无关键JS错", len(crit) == 0, f"{len(crit)}个" + (f": {crit[0][:50]}" if crit else "")) # G8 Footer print("\n[G8] 页脚完整性") try: page.goto(BASE) page.wait_for_load_state("networkidle") ft = page.locator("[data-testid=footer], footer").first ft.scroll_into_view_if_needed() txt = ft.inner_text() rec("ICP备案", "ICP备" in txt or "蜀ICP" in txt, "") rec("隐私链接", ft.locator("a[href='/privacy']").count() > 0, "") rec("条款链接", ft.locator("a[href='/terms']").count() > 0, "") except Exception as e: rec("Footer", False, str(e)[:60]) browser.close() # Summary total = R["p"] + R["f"] print(f"\n{'='*50}\n总计:{total} | 通过:{R['p']} | 失败:{R['f']}") print(f"通过率: {R['p']/total*100:.1f}%\n{'='*50}") if R["f"] > 0: print("失败用例:") for n, ok, note in R["d"]: if not ok: print(f" ❌ {n}: {note}")