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 template.count('data-tool="whiteboard"') == 1 assert 'data-tool="geometry"' not in template assert 'id="board-join-form"' in template assert "函数书写规则" in template assert 'id="math-game-list"' in template assert "window.HuluToolbox" in toolbox assert "pointerdown" in toolbox assert "class BoardRealtime" in toolbox assert 'boardRealtime.send("canvas", payload)' in toolbox assert 'boardRealtime.send("geometry", payload)' 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_v121_工具箱分类与响应式函数探索器资源完整(): template = (settings.BASE_DIR / "templates" / "index.html").read_text( encoding="utf-8" ) app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") toolbox = (STATIC_ROOT / "js" / "toolbox.js").read_text(encoding="utf-8") graph = (STATIC_ROOT / "js" / "graph.js").read_text(encoding="utf-8") styles = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert template.count("js/graph.js") == 1 assert 'data-tool="mental"' not in template assert 'class="tool-groups"' in template assert 'data-calc-category="calculus"' in template assert 'value="solve_system"' in template assert 'value="matrix_eigenvalues"' in template assert 'id="graph-function-list"' in template assert 'id="graph-x-min"' in template assert 'id="graph-y-max"' in template assert 'id="graph-auto-fit"' in template assert 'id="graph-expression"' not in template assert 'id="graph-range"' not in template assert "function setCalculatorCategory(" in toolbox assert "window.HuluGraph?.init()" in toolbox assert "drawGraph" not in toolbox assert 'tool === "mental"' not in toolbox assert "drawGraph" not in app assert 'tool === "mental"' not in app assert "function addFunction(" in graph assert "function drawAxes(" in graph assert "function niceStep(" in graph assert "window.devicePixelRatio" in graph assert '"pointerdown"' in graph assert '"wheel"' in graph assert "ResizeObserver" in graph assert "最多同时绘制 8 条曲线" in graph assert ".tool-groups" in styles assert ".calc-category-tabs" in styles assert ".graph-function-row" in styles assert ".graph-stage" in styles assert "aspect-ratio: 16 / 10" in styles assert "touch-action: none" 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 def test_math_life_使用独立界面印记面板且清理交叉点(): template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8") app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8") styles = (STATIC_ROOT / "css" / "app.css").read_text(encoding="utf-8") assert 'id="story-experience"' in template assert 'id="story-mark-list"' in template assert "数学人生交叉点" not in template assert "