test: update smoke and regression suites for dual-renderer paths

Offscreen harnesses now resize+show before load, use per-instance webengine profile dirs and explicit exit codes; compaction and bash stream suites follow the renamed internals.
This commit is contained in:
2026-09-17 16:40:06 +08:00
parent 0a62877cde
commit acbf73f2c3
10 changed files with 916 additions and 213 deletions
+251 -63
View File
@@ -1,74 +1,262 @@
# -*- coding: utf-8 -*-
"""问题 5 取证:会话改名覆盖层(RenameOverlay)的"聚光灯"遮罩现状"""
"""P2-03 结构+行为诊断:重命名遮罩 = 独立顶层透明窗(可盖住 WebView2 原生子窗)。
覆盖修复目标与硬约束:
R1 结构:顶层窗(非 bg_widget 子控件)、Tool+无边框、WA_TranslucentBackground、WA_DeleteOnClose
R2 几何:覆盖主窗口客户区(标题栏/窗口控制不被盖)、卡片居中、输入框初始全选
R3 跟随:主窗口移动/缩放/窗口状态变化 → 遮罩同步(move/resize/WindowStateChange 事件过滤器)
R4 行为:Enter 提交(renamed 信号→DB+侧栏)、Esc 关闭、点空白关闭、✕ 关闭、取消关闭
R5 释放:关闭后顶层窗口消失、无残留(deleteLater + 事件过滤器卸载)
R6 焦点(软检查,仅打印):关闭后焦点回主窗口
隔离 + offscreenos._exit 收尾。
用法: QT_QPA_PLATFORM=offscreen HAOCODE_RENDER=software .venv/Scripts/python.exe tests/diag_rename_overlay.py
"""
import os
import sys
import tempfile
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
os.environ.setdefault("HAOCODE_RENDER", "software")
os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-gpu")
import core.db_manager as _dbm # noqa: E402
_dbm._DEFAULT_DB = os.path.join(tempfile.gettempdir(), f"haocode_q5_{os.getpid()}.db")
_cfg = os.path.join(tempfile.gettempdir(), f"haocode_q5_{os.getpid()}.json")
open(_cfg, "w", encoding="utf-8").write('{"providers": {}}')
os.environ["HAOCODE_CONFIG_FILE"] = _cfg
from tests._test_env import isolate
isolate()
from PyQt6.QtWidgets import QApplication # noqa: E402
from PyQt6.QtTest import QTest # noqa: E402
from ui.views.main_window import MainWindow, RenameOverlay, SessionContextPopup # noqa: E402
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtTest import QTest
from ui.views.main_window import MainWindow, RenameOverlay
app = QApplication(sys.argv)
w = MainWindow()
w.resize(1300, 900)
w.show()
for _ in range(40):
results = []
def check(name, cond, info=""):
ok = bool(cond)
results.append((name, ok))
print(f" [{'PASS' if ok else 'FAIL'}] {name} {info}", flush=True)
def settle(ms=120):
t0 = time.time()
while (time.time() - t0) * 1000 < ms:
QtWidgets.QApplication.processEvents()
time.sleep(0.01)
# offscreen 无真实事件循环:processEvents 不处理 DeferredDelete,显式冲刷(生产中事件循环常驻,deleteLater 正常)
QtCore.QCoreApplication.sendPostedEvents(None, QtCore.QEvent.Type.DeferredDelete)
QtWidgets.QApplication.processEvents()
def find_overlay():
for w in QtWidgets.QApplication.topLevelWidgets():
if isinstance(w, RenameOverlay):
return w
return None
def main():
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.resize(1400, 800)
window.show()
settle(400)
sid = window.db.create_session("旧标题A")["id"]
window.rebuild_sidebar()
settle(150)
# ---------- 打开遮罩 ----------
window._rename_session(sid)
settle(250) # 含 150ms 入场动画
ov = find_overlay()
check("R0.1 overlay 已创建且为顶层窗口", ov is not None,
"" if ov is not None else "topLevelWidgets 中找不到 RenameOverlay")
if ov is None:
print(f"\n{'='*60}\nRESULT: 1 FAIL -> FAIL\n{'='*60}", flush=True)
os._exit(1)
# ---------- R1 结构 ----------
print("R1 结构(独立顶层透明窗)", flush=True)
check("R1.1 isWindow()", ov.isWindow())
check("R1.2 自身即顶层窗口(非 bg_widget 内嵌子控件)", ov.window() is ov,
f"window() is ov={ov.window() is ov}")
check("R1.3 非主窗口自身", ov is not window)
check("R1.4 Tool 窗(不入任务栏)", bool(ov.windowFlags() & QtCore.Qt.WindowType.Tool))
check("R1.5 无边框", bool(ov.windowFlags() & QtCore.Qt.WindowType.FramelessWindowHint))
check("R1.6 WA_TranslucentBackground", ov.testAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground))
check("R1.7 WA_DeleteOnClose", ov.testAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose))
check("R1.8 主窗口未设 WA_TranslucentBackground(保持原生不透明底)",
not window.testAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground))
# ---------- R2 几何 ----------
print("R2 几何(覆盖客户区,标题栏可操作)", flush=True)
exp_tl = window.mapToGlobal(window.rect().topLeft())
exp_size = window.rect().size()
g = ov.geometry()
check("R2.1 覆盖客户区左上角", abs(g.left() - exp_tl.x()) <= 2 and abs(g.top() - exp_tl.y()) <= 2,
f"overlay=({g.left()},{g.top()}) expect=({exp_tl.x()},{exp_tl.y()})")
check("R2.2 覆盖客户区尺寸", abs(g.width() - exp_size.width()) <= 2 and abs(g.height() - exp_size.height()) <= 2,
f"overlay={g.width()}x{g.height()} expect={exp_size.width()}x{exp_size.height()}")
fx, fy = (g.width() - ov.form.width()) // 2, (g.height() - ov.form.height()) // 2
check("R2.3 卡片居中", abs(ov.form.x() - fx) <= 2 and abs(ov.form.y() - fy) <= 2,
f"form=({ov.form.x()},{ov.form.y()}) expect=({fx},{fy})")
check("R2.4 输入框初始全选", ov.input.hasSelectedText(), f"text={ov.input.text()!r}")
check("R2.5 输入框预填旧标题", ov.input.text() == "旧标题A", f"text={ov.input.text()!r}")
# ---------- R3 跟随 ----------
print("R3 跟随(move/resize/状态变化)", flush=True)
base = window.pos()
window.move(base.x() + 150, base.y() + 80)
settle(150)
exp_tl2 = window.mapToGlobal(window.rect().topLeft())
check("R3.1 主窗口移动→遮罩跟随", abs(ov.geometry().left() - exp_tl2.x()) <= 2
and abs(ov.geometry().top() - exp_tl2.y()) <= 2,
f"overlay_tl=({ov.geometry().left()},{ov.geometry().top()}) expect=({exp_tl2.x()},{exp_tl2.y()})")
window.resize(1200, 700)
settle(150)
g2 = ov.geometry()
check("R3.2 主窗口缩放→遮罩同步尺寸",
abs(g2.width() - 1200) <= 4 and abs(g2.height() - 700) <= 4,
f"overlay={g2.width()}x{g2.height()} expect=1200x700")
fx2 = (g2.width() - ov.form.width()) // 2
check("R3.3 缩放后卡片重新居中", abs(ov.form.x() - fx2) <= 2, f"form.x={ov.form.x()} expect={fx2}")
# WindowStateChange 分支(最大化/还原走同一条 _sync_geometry 路径;offscreen 直接投递事件验证分支)
before = ov.geometry()
QtWidgets.QApplication.sendEvent(window, QtCore.QEvent(QtCore.QEvent.Type.WindowStateChange))
QtWidgets.QApplication.processEvents()
check("R3.4 WindowStateChange 分支不崩溃且几何仍正确",
abs(ov.geometry().width() - 1200) <= 4 and ov is find_overlay(),
f"geometry={ov.geometry().width()}x{ov.geometry().height()}")
# ---------- R4 行为 ----------
print("R4 行为", flush=True)
# R4.1 Enter 提交 → renamed 信号 → DB + 侧栏
ov.input.setText("P203新会话名")
ov.confirm()
settle(250)
sessions = {s["id"]: s for s in window.db.get_all_sessions()}
check("R4.1a confirm 后 DB 标题已更新", sessions.get(sid, {}).get("title") == "P203新会话名",
f"db_title={sessions.get(sid, {}).get('title')!r}")
side_ok = False
for i in range(window.history_list.count()):
it = window.history_list.item(i)
if it and it.data(QtCore.Qt.ItemDataRole.UserRole) == sid:
w = window.history_list.itemWidget(it)
if w and hasattr(w, "title_label") and w.title_label.text() == "P203新会话名":
side_ok = True
check("R4.1b confirm 后侧栏标题已更新", side_ok)
check("R4.1c 提交后 overlay 已从顶层窗口消失", find_overlay() is None)
# R4.2 空标题 confirm 不提交(只关闭)
window._rename_session(sid)
settle(200)
ovz = find_overlay()
if ovz:
ovz.input.clear()
ovz.confirm()
settle(200)
sessions = {s["id"]: s for s in window.db.get_all_sessions()}
check("R4.2 空标题 confirm 不改 DB 标题",
sessions.get(sid, {}).get("title") == "P203新会话名",
f"db_title={sessions.get(sid, {}).get('title')!r}")
check("R4.2b 空标题确认后 overlay 已消失", find_overlay() is None)
# R4.3 Esc 关闭
window._rename_session(sid)
settle(200)
ov2 = find_overlay()
check("R4.3a 重开 overlay", ov2 is not None)
if ov2:
QtWidgets.QApplication.sendEvent(
ov2, QtGui.QKeyEvent(QtCore.QEvent.Type.KeyPress, QtCore.Qt.Key.Key_Escape,
QtCore.Qt.KeyboardModifier.NoModifier, "Esc"))
QtWidgets.QApplication.sendEvent(
ov2, QtGui.QKeyEvent(QtCore.QEvent.Type.KeyRelease, QtCore.Qt.Key.Key_Escape,
QtCore.Qt.KeyboardModifier.NoModifier, "Esc"))
QtWidgets.QApplication.processEvents()
settle(200)
check("R4.3b Esc 关闭", find_overlay() is None)
# R4.4 点空白关闭
window._rename_session(sid)
settle(200)
ov3 = find_overlay()
check("R4.4a 重开 overlay", ov3 is not None)
if ov3:
QTest.mouseClick(ov3, QtCore.Qt.MouseButton.LeftButton,
QtCore.Qt.KeyboardModifier.NoModifier, QtCore.QPoint(10, 10))
settle(200)
check("R4.4b 点空白关闭", find_overlay() is None)
# R4.5 ✕ 关闭
window._rename_session(sid)
settle(200)
ov4 = find_overlay()
check("R4.5a 重开 overlay", ov4 is not None)
if ov4:
btn_x = None
for b in ov4.form.findChildren(QtWidgets.QPushButton):
if b.text() == "":
btn_x = b
break
check("R4.5b ✕ 按钮存在", btn_x is not None)
if btn_x:
btn_x.click()
settle(200)
check("R4.5c ✕ 关闭", find_overlay() is None)
# R4.6 取消按钮关闭
window._rename_session(sid)
settle(200)
ov5 = find_overlay()
if ov5:
for b in ov5.form.findChildren(QtWidgets.QPushButton):
if b.text() == "取消":
b.click()
break
settle(200)
check("R4.6 取消按钮关闭", find_overlay() is None)
# R4.7 非确认关闭(Esc/空白/✕/取消)均不改标题
sessions = {s["id"]: s for s in window.db.get_all_sessions()}
check("R4.7 非确认关闭不改 DB 标题",
sessions.get(sid, {}).get("title") == "P203新会话名",
f"db_title={sessions.get(sid, {}).get('title')!r}")
# ---------- R5 释放 ----------
print("R5 释放(无残留)", flush=True)
check("R5.1 无残留 rename_form 顶层窗口",
not [w for w in QtWidgets.QApplication.topLevelWidgets()
if w.findChild(QtWidgets.QFrame, "rename_form") is not None])
check("R5.2 主窗口仍存活可用", window.isVisible() and window.db.get_all_sessions() is not None)
# ---------- R6 焦点(软检查) ----------
print("R6 焦点回主窗口(软检查)", flush=True)
window.activateWindow()
settle(200)
print(f" [INFO] window.isActiveWindow()={window.isActiveWindow()} "
f"(offscreen 下不可靠,仅记录;真实机器人工走查确认)", flush=True)
window.close()
app.processEvents()
QTest.qWait(20)
ok = all(o for _, o in results)
print(f"\n{'='*60}", flush=True)
print(f"RESULT: {sum(o for _, o in results)}/{len(results)} -> {'ALL PASS' if ok else 'FAIL'}", flush=True)
print(f"{'='*60}", flush=True)
sys.stdout.flush()
os._exit(0 if ok else 1)
sid = w.db.create_session("聚光灯测试会话")["id"]
w.load_messages_to_web(sid)
for _ in range(20):
app.processEvents()
QTest.qWait(20)
print("=== SessionContextPopup 是否为独立顶层窗口 ===")
print(" windowFlags 含 Qt.Popup ?", bool(SessionContextPopup(sid, "", w).windowFlags() & 0x80000000))
pop = SessionContextPopup(sid, "聚光灯测试会话", w)
print(" isWindow() =", pop.isWindow(), " windowType =", pop.windowFlags())
print()
print("=== RenameOverlay 现状 ===")
w._rename_session(sid)
for _ in range(25):
app.processEvents()
QTest.qWait(20)
ovs = [c for c in w.children() if isinstance(c, RenameOverlay)]
if not ovs:
print(" ✗ 没找到 RenameOverlay")
else:
ov = ovs[0]
print(" 父对象 =", ov.parent().__class__.__name__)
print(" isWindow() =", ov.isWindow(), " (False = 主窗口的子控件,只覆盖客户区)")
print(" 几何 =", ov.geometry())
print(" 主窗口 rect =", w.rect(), " 主窗口 frameGeometry =", w.frameGeometry())
print(" WA_TranslucentBackground =", ov.testAttribute(
__import__('PyQt6.QtCore', fromlist=['Qt']).Qt.WidgetAttribute.WA_TranslucentBackground))
print(" graphicsEffect =", type(ov.graphicsEffect()).__name__ if ov.graphicsEffect() else None)
print(" 遮罩色 =", ov._overlay_color.getRgb())
print(" form 几何 =", ov.form.geometry(), " form 是否在遮罩内 =",
ov.rect().contains(ov.form.geometry()))
# 看遮罩是否盖住了标题栏 / 是否超出窗口
print(" 遮罩 top-left(全局) =", ov.mapToGlobal(ov.rect().topLeft()),
" bottom-right(全局) =", ov.mapToGlobal(ov.rect().bottomRight()))
print(" 主窗口(全局) =", w.mapToGlobal(w.rect().topLeft()), w.mapToGlobal(w.rect().bottomRight()))
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_tmp_q5_overlay.png")
ov.grab().save(out)
print(" 覆盖层截图 ->", out, ov.width(), "x", ov.height())
out2 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_tmp_q5_window.png")
w.grab().save(out2)
print(" 主窗口截图 ->", out2, w.width(), "x", w.height())
os.remove(_cfg)
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except BaseException:
import traceback
traceback.print_exc()
sys.stdout.flush()
os._exit(1)