#!/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'<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)