Files
Haocode/tests/verify_math_render.py
T
2026-09-17 16:30:02 +08:00

152 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""公式渲染端到端验证:真实会话消息 → 真实前端管线 → DOM 断言
运行: PYTHONIOENCODING=utf-8 QT_QPA_PLATFORM=offscreen python tests/verify_math_render.py
铁律:不污染真实 DB —— 先把 data/chat_history.db 复制到临时文件,再指向副本。
"""
import os
import sys
import json
import time
import shutil
import tempfile
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
os.environ.setdefault("HAOCODE_RENDER", "software")
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
_REAL_DB = os.path.join(os.path.dirname(__file__), "..", "data", "chat_history.db")
_TMP_DB = os.path.join(tempfile.gettempdir(), f"haocode_verify_math_{os.getpid()}.db")
shutil.copyfile(_REAL_DB, _TMP_DB)
import core.db_manager as _dbm # noqa: E402
_dbm._DEFAULT_DB = _TMP_DB
from PyQt6.QtWidgets import QApplication # noqa: E402
from PyQt6.QtCore import QTimer # noqa: E402
from ui.views.main_window import MainWindow # noqa: E402
app = QApplication(sys.argv)
results = []
win = {"w": None, "sid": None}
JS_QUERY = r"""(function(){
var scope = document.getElementById('chat-container') || document.body;
var q = function(s){ return scope.querySelectorAll(s).length; };
// 只取助手消息(user 消息按设计是纯文本,本来就不渲染公式)
var asst = scope.querySelectorAll('.message-wrapper.assistant');
var asstScope = asst.length ? asst[asst.length - 1] : scope;
// 可见文本:剔除 katex-mathmlCSS 视觉隐藏的 MathML 层,内部保留原始 tex 属正常)
var clone = asstScope.cloneNode(true);
clone.querySelectorAll('.katex-mathml').forEach(function(e){ e.parentNode.removeChild(e); });
var visTxt = clone.textContent || '';
var codeTxt = '';
asstScope.querySelectorAll('pre, code').forEach(function(e){ codeTxt += e.textContent + '\n'; });
return {
katex: q('.katex'),
display: q('.katex-display'),
mathml: q('.katex-mathml'),
vislayer: q('.katex-html'),
leakedPlaceholder: visTxt.indexOf('@@K') !== -1,
rawBackslash: visTxt.indexOf('\\operatorname') !== -1,
bareBracketFormula: visTxt.indexOf('P_4=\\operatorname') !== -1,
codeHasDollar: codeTxt.indexOf('$x + y$') !== -1,
codeHasArr: codeTxt.indexOf('arr[0]') !== -1
};
})()"""
def check(name, cond, extra=""):
print((" PASS " if cond else " FAIL ") + name + ((" | " + str(extra)) if extra else ""))
results.append(bool(cond))
def run_js(js, timeout_s=15):
r = {"v": None, "d": False}
def cb(val):
if isinstance(val, str):
try:
val = json.loads(val)
except Exception:
pass
r["v"] = val
r["d"] = True
b = win["w"].browser
if hasattr(b, "execute_js_async"):
b.execute_js_async(js, cb)
else:
b.page().runJavaScript(js, cb)
t0 = time.time()
while not r["d"] and time.time() - t0 < timeout_s:
app.processEvents()
time.sleep(0.05)
return r["v"]
def boot():
w = MainWindow()
win["w"] = w
sid = None
for s in w.db.get_all_sessions():
if s.get("title") == "公式渲染验收":
sid = s["id"]
break
if not sid:
print("FAIL 未找到「公式渲染验收」会话(先跑 tests/inject_math_demo.py")
app.quit()
return
win["sid"] = sid
print(f"会话: {sid}")
w.load_messages_to_web(sid)
QTimer.singleShot(9000, phase_check)
def phase_check():
for _ in range(40):
if run_js("window.jsReady === true ? 1 : 0", timeout_s=3) == 1:
break
time.sleep(0.5)
r = run_js(JS_QUERY)
if not isinstance(r, dict):
print(f"FAIL DOM 查询失败: {r!r}")
app.quit()
return
print("\n DOM 统计: " + json.dumps(r, ensure_ascii=False))
check("KaTeX 渲染出公式(.katex > 0", r["katex"] > 0, f"katex={r['katex']}")
check("块公式 7 个(.katex-display == 7", r["display"] == 7, f"display={r['display']}")
check("可视层存在(.katex-html > 0", r["vislayer"] > 0, f"vislayer={r['vislayer']}")
check("无占位符泄漏", not r["leakedPlaceholder"])
check("可见文本无原始 tex 残留", not r["rawBackslash"])
check("无裸括号公式残留", not r["bareBracketFormula"])
check("代码块内 $x + y$ 保持原样", r["codeHasDollar"])
check("代码块内 arr[0] 保持原样", r["codeHasArr"])
try:
shot_path = os.path.join(os.path.dirname(__file__), "_tmp_math_render.png")
win["w"].browser.grab().save(shot_path)
print(f" 截图: {shot_path}")
except Exception as e:
print(f" 截图失败: {e}")
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: {sum(results)}/{len(results)} =====")
try:
win["w"].close()
except Exception:
pass
app.quit()
QTimer.singleShot(400, boot)
QTimer.singleShot(120000, app.quit)
app.exec()
try:
os.remove(_TMP_DB)
except Exception:
pass
sys.exit(0 if all(results) else 1)