style(theme): 更新网站主题色彩方案与字体配置
- 调整主色调从 #1C1C1C 至 #1A1A1A,优化视觉层次 - 更新背景色系为暖白色调 (#FAFAF7, #F5F4F0 等) - 配置中文字体栈,添加 serif 字体支持 - 优化文本颜色梯度,提升可读性 - 调整边框颜色,统一水墨风格 - 添加 Google Search Console 验证码配置项 - 新增桌面应用架构专家代理配置文件 - 重构 E2E 测试等待策略,提升稳定性 - 添加回归测试脚本,增强质量保障
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user