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

263 lines
11 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 -*-
"""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 time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
os.environ.setdefault("HAOCODE_RENDER", "software")
os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-gpu")
from tests._test_env import isolate
isolate()
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtTest import QTest
from ui.views.main_window import MainWindow, RenameOverlay
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()
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)
if __name__ == "__main__":
try:
main()
except SystemExit:
raise
except BaseException:
import traceback
traceback.print_exc()
sys.stdout.flush()
os._exit(1)