196 lines
7.7 KiB
Python
196 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""时间线 DOM 冒烟测试(真实 WebEngine 页面内执行):
|
|
1. 流式事件序列 → 思考/正文/工具按事件顺序穿插
|
|
2. 工具 chip 按 call_id 对号入座 + ok 状态(修复反转 bug)
|
|
3. SVG 箭头存在、流式光标移除
|
|
4. 历史消息静态路径不受影响
|
|
运行: QT_QPA_PLATFORM=offscreen python tests/smoke_timeline.py
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
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" # 绕过 AMD 核显 context lost
|
|
|
|
# 铁律(P0-01):临时数据库 + 临时配置必须在 import MainWindow 之前完成
|
|
from tests._test_env import isolate # noqa: E402
|
|
_TMP = isolate("timeline") # noqa: E402
|
|
|
|
from PyQt6.QtWidgets import QApplication # noqa: E402
|
|
from PyQt6.QtCore import QTimer # noqa: E402
|
|
from ui.views.main_window import MainWindow # noqa: E402 (QtWebEngine 已先导入)
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
results = []
|
|
window = None
|
|
done = {"ok": False}
|
|
|
|
|
|
def check(name, ok, detail=""):
|
|
print(f" {'PASS' if ok else 'FAIL'} {name}" + (f" [{detail}]" if detail and not ok else ""))
|
|
results.append(ok)
|
|
|
|
|
|
def js(s):
|
|
return s
|
|
|
|
|
|
JS_SIM = js(r"""
|
|
(function() {
|
|
var out = {};
|
|
try {
|
|
// ---- 1. 流式时间线 ----
|
|
var mid = 'tl-test-' + Date.now();
|
|
createMessage(mid, 'assistant', '', 'TL');
|
|
var wrapper = document.getElementById(mid);
|
|
appendReasoning(mid, 'think part 1 ');
|
|
appendReasoning(mid, 'more thinking');
|
|
appendToken(mid, 'before tool text ');
|
|
toolExecutionStarted(mid, 'call-1', 'bash', JSON.stringify({command: 'echo hi'}));
|
|
toolExecutionUpdated(mid, 'call-1', 'hi\n');
|
|
toolExecutionFinished(mid, 'call-1', 'bash', true, '$ echo hi\nhi\n[exit 0]');
|
|
appendToken(mid, ' after tool');
|
|
appendReasoning(mid, ' second thinking');
|
|
appendToken(mid, ' final answer');
|
|
finishMessage(mid);
|
|
|
|
var tl = wrapper.querySelector('.reply-content');
|
|
out.blocks = Array.prototype.map.call(tl.children, function(el) {
|
|
return el.className.split(' ')[0];
|
|
});
|
|
var chip = wrapper.querySelector('.tool-chip');
|
|
var status = chip ? chip.querySelector('.tool-chip-status') : null;
|
|
out.chipStatusText = status ? status.textContent : null;
|
|
out.chipOk = status ? status.classList.contains('ok') : false;
|
|
out.chipBrief = chip ? (chip.querySelector('.tool-chip-brief') || {}).textContent : null;
|
|
out.chipCallId = chip ? chip.getAttribute('data-call-id') : null;
|
|
out.thinkChevron = !!wrapper.querySelector('.think-block .chev');
|
|
out.chipChevron = !!wrapper.querySelector('.tool-chip .chev');
|
|
out.typingRemoved = !wrapper.querySelector('.streaming-typing');
|
|
out.streamClassOff = !wrapper.classList.contains('streaming');
|
|
out.thinkBlocks = wrapper.querySelectorAll('.think-block').length;
|
|
out.mdSegs = wrapper.querySelectorAll('.md-segment').length;
|
|
|
|
// ---- 2. 历史消息静态路径 ----
|
|
var mid2 = 'tl-hist-' + Date.now();
|
|
createMessage(mid2, 'assistant', 'hello **world**', 'TL');
|
|
insertThinkBlock(mid2, 'history thinking');
|
|
finishMessage(mid2);
|
|
var w2 = document.getElementById(mid2);
|
|
var content2 = w2.querySelector('.message-content');
|
|
out.histOrder = Array.prototype.map.call(content2.children, function(el) {
|
|
return el.className.split(' ')[0];
|
|
});
|
|
out.histBold = w2.querySelector('.reply-content strong') !== null;
|
|
out.histThinkChevron = !!w2.querySelector('.think-block .chev');
|
|
out.histThinkLabel = (w2.querySelector('.think-label') || {}).textContent;
|
|
|
|
// ---- 3. 失败工具 chip(反转修复验证)----
|
|
var mid3 = 'tl-fail-' + Date.now();
|
|
createMessage(mid3, 'assistant', '', 'TL');
|
|
toolExecutionStarted(mid3, 'call-x', 'bash', '{"command":"false"}');
|
|
toolExecutionFinished(mid3, 'call-x', 'bash', false, 'boom');
|
|
var w3 = document.getElementById(mid3);
|
|
var st3 = w3.querySelector('.tool-chip-status');
|
|
out.failChipFail = st3 ? st3.classList.contains('fail') : false;
|
|
out.failChipText = st3 ? st3.textContent : null;
|
|
finishMessage(mid3);
|
|
|
|
// 清理
|
|
[mid, mid2, mid3].forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.parentNode.removeChild(el);
|
|
});
|
|
} catch (e) {
|
|
out.error = String(e) + ' | ' + (e.stack || '').split('\n')[1];
|
|
}
|
|
return JSON.stringify(out);
|
|
})()
|
|
""")
|
|
|
|
|
|
def run_js_check():
|
|
page = window.browser.page() if hasattr(window, "browser") else None
|
|
if page is None:
|
|
# 找 QWebEnginePage
|
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
views = window.findChildren(QWebEngineView)
|
|
page = views[0].page() if views else None
|
|
if page is None:
|
|
check("页面句柄", False, "no page")
|
|
finish()
|
|
return
|
|
|
|
def on_result(res):
|
|
try:
|
|
import json
|
|
data = json.loads(str(res))
|
|
except Exception as e:
|
|
check("JS 执行", False, f"bad json: {res} ({e})")
|
|
finish()
|
|
return
|
|
if "error" in data:
|
|
check("JS 执行", False, data["error"])
|
|
finish()
|
|
return
|
|
|
|
# 期望时间线: think → text → chip → text → think → text
|
|
expected = ["think-block", "md-segment", "tool-chip",
|
|
"md-segment", "think-block", "md-segment"]
|
|
check("时间线块顺序(思考/正文/工具穿插)", data["blocks"] == expected,
|
|
str(data["blocks"]))
|
|
check("两个思考段 + 三个正文段",
|
|
data["thinkBlocks"] == 2 and data["mdSegs"] == 3,
|
|
f"think={data['thinkBlocks']} segs={data['mdSegs']}")
|
|
check("工具 chip 成功状态(非反转)",
|
|
data["chipOk"] and data["chipStatusText"] == "✓ 完成",
|
|
f"{data['chipStatusText']} ok={data['chipOk']}")
|
|
check("chip 摘要=结果首行", data["chipBrief"] == "$ echo hi",
|
|
str(data["chipBrief"]))
|
|
check("chip call_id 贯通", data["chipCallId"] == "call-1",
|
|
str(data["chipCallId"]))
|
|
check("SVG 箭头(思考+工具)", data["thinkChevron"] and data["chipChevron"])
|
|
check("流式光标已移除 + streaming 类移除",
|
|
data["typingRemoved"] and data["streamClassOff"])
|
|
check("失败 chip 标 ✗", data["failChipFail"] and data["failChipText"] == "✗ 失败",
|
|
f"{data['failChipText']}")
|
|
check("历史消息: 思考在正文前",
|
|
data["histOrder"].index("think-block") < data["histOrder"].index("reply-content"),
|
|
str(data["histOrder"]))
|
|
check("历史消息: markdown 正常解析", data["histBold"])
|
|
check("历史消息: 思考块带 SVG 箭头+标签",
|
|
data["histThinkChevron"] and data["histThinkLabel"] == "已完成深度思考",
|
|
str(data["histThinkLabel"]))
|
|
finish()
|
|
|
|
page.runJavaScript(JS_SIM, on_result)
|
|
|
|
|
|
def finish():
|
|
done["ok"] = True
|
|
print(f"\n===== {'ALL PASS' if all(results) else 'HAS FAILURES'}: "
|
|
f"{sum(results)}/{len(results)} =====")
|
|
app.quit()
|
|
|
|
|
|
def main():
|
|
global window
|
|
try:
|
|
window = MainWindow()
|
|
except Exception:
|
|
import traceback
|
|
traceback.print_exc()
|
|
app.quit()
|
|
return
|
|
QTimer.singleShot(6000, run_js_check)
|
|
QTimer.singleShot(25000, lambda: (check("超时", False, "25s 未完成"), finish())
|
|
if not done["ok"] else None)
|
|
app.exec()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|