Files
novalon-website/scripts/regression_test.py
T
张翔 415a103a24 style(theme): 更新网站主题色彩方案与字体配置
- 调整主色调从 #1C1C1C 至 #1A1A1A,优化视觉层次
- 更新背景色系为暖白色调 (#FAFAF7, #F5F4F0 等)
- 配置中文字体栈,添加 serif 字体支持
- 优化文本颜色梯度,提升可读性
- 调整边框颜色,统一水墨风格
- 添加 Google Search Console 验证码配置项
- 新增桌面应用架构专家代理配置文件
- 重构 E2E 测试等待策略,提升稳定性
- 添加回归测试脚本,增强质量保障
2026-06-17 11:37:25 +08:00

434 lines
14 KiB
Python

"""
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)