cf99e7556c
- 更新 CI/CD 子域名反向代理配置 - 调整 Nginx 静态文件服务器配置 - 更新 CONTEXT.md 领域共享语言文档 - 添加 CLAUDE.md 代理工作指南 - 更新 Playwright E2E 测试配置
434 lines
14 KiB
Python
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)
|