Files
ai-learning-platform/frontend/test-seo.py
T
TradeMate Dev 31cc1e02ce feat: 联盟返佣系统规范化并打通技能广场(无 ICP 证合规变现主路径)
- 新增 AffiliateProgram / AffiliateLink / AffiliateClick 规范化 Prisma 模型
  取代原手写裸表 affiliate_stats / affiliate_clicks
- 新增迁移 prisma/migrations/20260711000000_add_affiliate_models
- 重构 affiliate.service.ts 改用 Prisma ORM,消除 $queryRawUnsafe SQL 注入
- 重构 affiliate.controller.ts 接口:programs / links(?skillId,?toolId) / stats / click
- 前端 affiliate 页接入真实接口,移除硬编码 demo 数据
- 技能详情页新增「学此技能推荐使用的工具」联盟链接区块
- 新增 affiliate.service.spec.ts(4 用例通过)与幂等种子 seed-affiliate.ts
- 更新 docs/progress/current.md,明确无 ICP 经营许可证下以联盟返佣为合规变现主路径
- 含此前工作区未提交改动(工具 slug 路由、支付/订单、SEO 等)

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-11 14:47:41 +08:00

283 lines
11 KiB
Python

#!/usr/bin/env python3
"""
ai-learning-platform (yuzhiran.com) 浏览器自动化测试
测试 SEO 优化、页面加载、功能完整性
"""
import subprocess
import json
import re
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
from html.parser import HTMLParser
BASE_URL = "https://yuzhiran.com"
results = []
def run_test(name, test_func):
"""运行单个测试"""
try:
test_func()
results.append({"name": name, "status": "PASS", "error": None})
print(f"{name}")
except AssertionError as e:
results.append({"name": name, "status": "FAIL", "error": str(e)})
print(f"{name}: {e}")
except Exception as e:
results.append({"name": name, "status": "ERROR", "error": str(e)})
print(f" ⚠️ {name}: {e}")
def fetch_page(path, timeout=15):
"""获取页面内容"""
url = f"{BASE_URL}{path}"
req = Request(url, headers={
"User-Agent": "Mozilla/5.0 (compatible; Hermes-Test/1.0)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
})
resp = urlopen(req, timeout=timeout)
return resp.read().decode('utf-8', errors='replace'), resp
def check_meta_tags(html, path="/"):
"""检查 meta 标签"""
# Title
title_match = re.search(r'<title[^>]*>(.*?)</title>', html, re.DOTALL | re.IGNORECASE)
assert title_match, f"Missing <title> tag on {path}"
title = title_match.group(1).strip()
assert len(title) > 5, f"Title too short: '{title}' on {path}"
# Description
desc_match = re.search(r'<meta[^>]+name=["\']description["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert desc_match, f"Missing description meta tag on {path}"
desc = desc_match.group(1).strip()
assert len(desc) > 20, f"Description too short: '{desc[:50]}' on {path}"
# Keywords
kw_match = re.search(r'<meta[^>]+name=["\']keywords["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert kw_match, f"Missing keywords meta tag on {path}"
# Viewport
vp_match = re.search(r'<meta[^>]+name=["\']viewport["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert vp_match, f"Missing viewport meta tag on {path}"
vp = vp_match.group(1).strip()
assert "width=device-width" in vp.lower(), f"Viewport missing width=device-width on {path}"
# Canonical
canon_match = re.search(r'<link[^>]+rel=["\']canonical["\'][^>]+href=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert canon_match, f"Missing canonical link tag on {path}"
# Open Graph
og_match = re.search(r'<meta[^>]+property=["\']og:title["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert og_match, f"Missing og:title meta tag on {path}"
# Theme color
tc_match = re.search(r'<meta[^>]+name=["\']theme-color["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert tc_match, f"Missing theme-color meta tag on {path}"
def check_structured_data(html, path="/"):
"""检查结构化数据 (JSON-LD)"""
ld_match = re.search(r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', html, re.DOTALL | re.IGNORECASE)
assert ld_match, f"Missing JSON-LD structured data on {path}"
ld_content = ld_match.group(1).strip()
try:
data = json.loads(ld_content)
assert '@context' in data, f"JSON-LD missing @context on {path}"
assert '@type' in data, f"JSON-LD missing @type on {path}"
except json.JSONDecodeError:
assert False, f"Invalid JSON-LD on {path}"
def check_performance(html, path="/"):
"""检查性能相关优化"""
# Check for inline scripts (should be minimal)
inline_scripts = re.findall(r'<script[^>]*>(?!.*?</script>)([^<]{100,})', html, re.DOTALL | re.IGNORECASE)
# Large inline scripts are bad for performance
large_scripts = [s for s in inline_scripts if len(s) > 5000]
assert len(large_scripts) <= 2, f"Too many large inline scripts ({len(large_scripts)}) on {path}"
# Check for preconnect hints
preconnect = re.findall(r'<link[^>]+rel=["\']preconnect["\']', html, re.IGNORECASE)
# Not required but good to have
print(f" Preconnect hints: {len(preconnect)}")
def check_semantic_html(html, path="/"):
"""检查语义化 HTML"""
# Should have proper heading structure
h1_count = len(re.findall(r'<h1[^>]*>', html, re.IGNORECASE))
assert h1_count >= 1, f"No <h1> tag on {path}"
# Should have main element or equivalent
main_match = re.search(r'<main[^>]*>', html, re.IGNORECASE)
nav_match = re.search(r'<nav[^>]*>', html, re.IGNORECASE)
assert main_match or nav_match, f"No <main> or <nav> element on {path}"
def check_image_optimization(html, path="/"):
"""检查图片优化"""
# Check for alt attributes on images
images = re.findall(r'<img[^>]*>', html, re.IGNORECASE)
for img in images[:10]: # Check first 10 images
alt_match = re.search(r'alt=["\'](.*?)["\']', img, re.IGNORECASE)
if not alt_match:
assert False, f"Image missing alt attribute on {path}"
def check_link_validity(html, path="/"):
"""检查链接有效性"""
links = re.findall(r'<a[^>]+href=["\'](.*?)["\'][^>]*>', html, re.IGNORECASE)
# Check for broken internal links
broken = []
for link in links[:20]: # Check first 20 links
if link.startswith('#') or link.startswith('mailto:') or link.startswith('tel:'):
continue
if link.startswith('http'):
continue
try:
req = Request(f"{BASE_URL}{link}", headers={"User-Agent": "Hermes-Test/1.0"})
resp = urlopen(req, timeout=5)
if resp.status >= 400:
broken.append(link)
except:
pass
assert len(broken) == 0, f"Broken links found: {broken}"
# ==================== TESTS ====================
print("=" * 60)
print("🧪 ai-learning-platform (yuzhiran.com) 浏览器自动化测试")
print("=" * 60)
# 1. 首页 SEO
print("\n📄 首页 SEO 测试")
run_test("首页 SEO 优化", lambda: check_meta_tags(*fetch_page("/")))
run_test("首页结构化数据", lambda: check_structured_data(*fetch_page("/")))
run_test("首页性能检查", lambda: check_performance(*fetch_page("/")))
run_test("首页语义化 HTML", lambda: check_semantic_html(*fetch_page("/")))
run_test("首页图片优化", lambda: check_image_optimization(*fetch_page("/")))
run_test("首页链接有效性", lambda: check_link_validity(*fetch_page("/")))
# 2. 重要页面 SEO
print("\n📄 重要页面 SEO 测试")
important_pages = ["/tools", "/courses", "/skills", "/prompts", "/practices", "/sandbox", "/about"]
for page in important_pages:
run_test(f"{page} SEO 优化", lambda p=page: check_meta_tags(*fetch_page(p)))
run_test(f"{page} 结构化数据", lambda p=page: check_structured_data(*fetch_page(p)))
# 3. robots.txt
print("\n🤖 robots.txt 测试")
def test_robots():
html, resp = fetch_page("/robots.txt")
assert "User-agent" in html, "robots.txt missing User-agent"
assert "Disallow" in html, "robots.txt missing Disallow rules"
assert "Sitemap" in html, "robots.txt missing Sitemap"
assert "/admin/" in html, "robots.txt should disallow /admin/"
assert "/my/" in html, "robots.txt should disallow /my/"
test_robots()
# 4. sitemap.xml
print("\n🗺️ sitemap.xml 测试")
def test_sitemap():
html, resp = fetch_page("/sitemap.xml")
assert "<urlset" in html, "Invalid sitemap format"
assert "<loc>" in html, "Sitemap missing <loc> entries"
url_count = len(re.findall(r'<loc>', html))
assert url_count > 10, f"Sitemap has only {url_count} URLs"
test_sitemap()
# 5. PWA manifest
print("\n📱 PWA manifest 测试")
def test_manifest():
html, resp = fetch_page("/site.webmanifest")
data = json.loads(html)
assert "name" in data, "Manifest missing name"
assert "start_url" in data, "Manifest missing start_url"
assert "icons" in data, "Manifest missing icons"
assert "theme_color" in data, "Manifest missing theme_color"
assert "shortcuts" in data, "Manifest missing shortcuts"
test_manifest()
# 6. 页面加载性能
print("\n⚡ 页面加载性能测试")
def test_homepage_load():
start = time.time()
html, resp = fetch_page("/")
load_time = time.time() - start
assert load_time < 5, f"Homepage took {load_time:.2f}s to load"
print(f" Load time: {load_time:.2f}s")
test_homepage_load()
def test_tools_load():
start = time.time()
html, resp = fetch_page("/tools")
load_time = time.time() - start
assert load_time < 5, f"/tools took {load_time:.2f}s to load"
print(f" Load time: {load_time:.2f}s")
test_tools_load()
# 7. 移动端适配
print("\n📱 移动端适配测试")
def test_mobile_viewport():
html, resp = fetch_page("/")
vp_match = re.search(r'<meta[^>]+name=["\']viewport["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert vp_match, "Missing viewport meta tag"
vp = vp_match.group(1).strip()
assert "initial-scale=1" in vp.lower(), "Viewport missing initial-scale"
test_mobile_viewport()
# 8. 暗黑模式支持
print("\n🌙 暗黑模式测试")
def test_dark_mode():
html, resp = fetch_page("/")
# Check for dark mode CSS variables or class
dark_match = re.search(r'dark:', html)
assert dark_match, "No dark mode CSS found"
test_dark_mode()
# 9. 国际化支持
print("\n🌐 国际化测试")
def test_i18n():
html, resp = fetch_page("/")
# Check for lang attribute
lang_match = re.search(r'<html[^>]+lang=["\']zh-CN["\']', html, re.IGNORECASE)
assert lang_match, "Missing lang='zh-CN' attribute"
test_i18n()
# 10. 安全头检查
print("\n🔒 安全头检查")
def test_security_headers():
html, resp = fetch_page("/")
# Check if HTTPS
assert BASE_URL.startswith("https"), "Not using HTTPS"
# Check for CSP (optional but good)
# Check for HSTS (optional)
print(" HTTPS: ✅")
test_security_headers()
# ==================== SUMMARY ====================
print("\n" + "=" * 60)
print("📊 测试总结")
print("=" * 60)
passed = sum(1 for r in results if r["status"] == "PASS")
failed = sum(1 for r in results if r["status"] == "FAIL")
errors = sum(1 for r in results if r["status"] == "ERROR")
total = len(results)
print(f"\n总计: {total} 项测试")
print(f" ✅ 通过: {passed}")
print(f" ❌ 失败: {failed}")
print(f" ⚠️ 错误: {errors}")
print(f" 📈 通过率: {passed/total*100:.1f}%")
if failed > 0:
print("\n❌ 失败的测试:")
for r in results:
if r["status"] == "FAIL":
print(f" - {r['name']}: {r['error']}")
if errors > 0:
print("\n⚠️ 出错的测试:")
for r in results:
if r["status"] == "ERROR":
print(f" - {r['name']}: {r['error']}")
sys.exit(0 if failed == 0 and errors == 0 else 1)