import re from pathlib import Path from django.conf import settings from django.core.management import call_command from django.test import override_settings STATIC_ROOT = Path(settings.BASE_DIR) / "static" def test_app_js_幂等键兼容非安全上下文(): source = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") assert "function createIdempotencyKey()" in source assert "crypto.randomUUID()" not in source assert source.count('"Idempotency-Key": createIdempotencyKey()') == 2 def test_app_js_api_错误优先显示详情并输出安全诊断日志(): source = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") assert "function collectApiErrorMessages(" in source assert "const baseMessage = detailMessages.length" in source assert 'console.error("[Hulumath API]", {' in source assert "requestId: error.requestId" in source assert "error.details = details || null" in source assert "payload.error?.message || payload.detail ||" in source assert "payload.error?.message || payload.detail ||\n (details ?" not in source def test_app_css_答题提交按钮可见且长弹窗可滚动(): source = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert ".choice-list .primary-button" in source assert "background: var(--green); color: white; text-align: center" in source assert "max-height: calc(100dvh - 30px)" in source assert "overflow-y: auto" in source def test_admin_css_深色系统下仍保持完整浅色主题(): source = (STATIC_ROOT / "admin" / "css" / "hulumath_admin.css").read_text( encoding="utf-8" ) assert "color-scheme: light" in source assert "--body-fg: #17211b" in source assert "--body-bg: #f3f1e9" in source assert "--darkened-bg: #ebeae3" in source assert "background-color: #ffffff !important" in source assert ".theme-toggle" in source assert "display: none" in source def test_toolbox_and_games_资源入口与移动端触控样式存在(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") toolbox = (STATIC_ROOT / "js" / "toolbox.js").read_text(encoding="utf-8") games = (STATIC_ROOT / "js" / "games.js").read_text(encoding="utf-8") styles = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert "js/toolbox.js" in template assert "js/games.js" in template assert 'id="whiteboard-canvas"' in template assert 'id="geometry-canvas"' in template assert 'id="math-game-list"' in template assert "window.HuluToolbox" in toolbox assert "pointerdown" in toolbox assert "window.HuluGames" in games assert "sudoku-board" in games assert 'errorMessage.className = "form-error game-form-error"' in games assert "errorMessage.textContent = error.message" in games assert "touch-action: none" in styles assert ".calculator-controls [hidden]" in styles assert ".sudoku-board" in styles assert "@media (max-width: 700px)" in styles def test_latex_默认源码可渲染且_katex_静态资源完整(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") katex_root = STATIC_ROOT / "vendor" / "katex" katex_css = (katex_root / "katex.min.css").read_text(encoding="utf-8") font_paths = set(re.findall(r"url\(fonts/([^)]+)\)", katex_css)) assert ( '" ) in template assert r"\\int_{0}^{1}" not in template assert "function normalizeLatexSource(source)" in app assert "globalThis.katex.render(normalized, target" in app assert "throwOnError: true" in app assert len(font_paths) == 60 assert all((katex_root / "fonts" / path).is_file() for path in font_paths) assert (katex_root / "LICENSE.txt").is_file() def test_latex_长公式不会挤走保存按钮(): styles = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert "grid-template-rows: auto minmax(0, 1fr) auto" in styles assert "#latex-preview { align-self: center; width: 100%; min-width: 0" in styles assert ".preview-pane button { align-self: end; justify-self: end" in styles def test_rating_历史和登录后档案立即刷新(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") realtime = (STATIC_ROOT / "js" / "realtime.js").read_text(encoding="utf-8") assert 'id="home-match-history"' in template assert "function renderMatchHistory(root, matches)" in app assert "function loadHomeMatchHistory()" in app assert '$("#view-profile").classList.contains("active") ? loadProfile() : null' in app assert "state.user.rating = match.result.rating_after" in realtime assert "updateUserUI();" in realtime def test_production_manifest_可处理全部静态资源(tmp_path): storage = "whitenoise.storage.CompressedManifestStaticFilesStorage" with override_settings( STATIC_ROOT=tmp_path, STORAGES={ "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, "staticfiles": {"BACKEND": storage}, }, ): call_command("collectstatic", interactive=False, clear=True, verbosity=0) assert (tmp_path / "staticfiles.json").is_file() assert (tmp_path / "vendor" / "katex" / "katex.min.css").is_file() def test_realtime_match_联机码与_websocket_前端资源存在(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") realtime = (STATIC_ROOT / "js" / "realtime.js").read_text(encoding="utf-8") styles = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert 'id="challenge-create"' in template assert 'id="challenge-join-form"' in template assert template.count("js/realtime.js") == 1 assert "new WebSocket" in realtime assert "setInterval(refreshMatch, 2000)" in realtime assert "Idempotency-Key" in realtime assert ".challenge-panel" in styles assert ".realtime-progress-panel" in styles def test_contest_统一玩家池并提供三种实时玩法(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") games = (STATIC_ROOT / "js" / "games.js").read_text(encoding="utf-8") realtime = (STATIC_ROOT / "js" / "realtime.js").read_text(encoding="utf-8") assert 'id="track-switch"' not in template assert 'data-match-mode="quiz"' in template assert 'data-match-mode="sudoku"' in template assert 'data-match-mode="twenty_four"' in template assert "state.matchMode = button.dataset.matchMode" in app assert "difficultySelect" not in games assert "body: { game_kind: state.matchMode }" in realtime