#!/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'
]*>(.*?)', html, re.DOTALL | re.IGNORECASE)
assert title_match, f"Missing 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']+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']+name=["\']keywords["\'][^>]+content=["\'](.*?)["\'][^>]*/?>', html, re.DOTALL | re.IGNORECASE)
assert kw_match, f"Missing keywords meta tag on {path}"
# Viewport
vp_match = re.search(r']+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']+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']+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']+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'', 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')([^<]{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']+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'