238 lines
10 KiB
Python
238 lines
10 KiB
Python
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 (
|
|
'<textarea id="latex-source" spellcheck="false">'
|
|
r"\int_{0}^{1} x^2\,dx = \frac{1}{3}</textarea>"
|
|
) 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 "<circle " not in template
|
|
assert '$("#story-experience").classList.remove("hidden")' in app
|
|
assert "function renderStoryMarks(run)" in app
|
|
assert "choice.special" in app
|
|
assert ".story-experience { position: fixed; inset: 0" in styles
|
|
|
|
|
|
def test_home_公众号入口可复制并跳转微信():
|
|
template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8")
|
|
app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8")
|
|
|
|
assert 'id="wechat-copy"' in template
|
|
assert 'href="weixin://"' in template
|
|
assert "function copyWechatName()" in app
|
|
assert 'navigator.clipboard.writeText(name)' in app
|