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.
263 lines
11 KiB
Python
263 lines
11 KiB
Python
# -*- 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 焦点(软检查,仅打印):关闭后焦点回主窗口
|
||
|
||
隔离 + offscreen;os._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)
|