Files
Haocode/tests/diag_panel_scrollbar.py
T
sorrow404null acbf73f2c3 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.
2026-09-17 16:40:06 +08:00

210 lines
9.6 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-02 诊断:右侧 Bash 面板滚动条与横纵交汇角(离屏)
测量并断言:
S1 代码框(#bl_code)横滚动条实际厚度 = 8px、sizeHint 一致
S2 代码框竖滚动条实际厚度 = 8px、sizeHint 一致
S3 section 滚动区(#bl_scroll)竖滚动条实际厚度 = 8px
S4 面板滚动条箭头 extent = 0(箭头隐藏)
S5 交汇角像素 = 代码框背景 #fbfcfe(无原生亮色 corner 方块)
S6 无泄漏:未命名 QPlainTextEdit 的滚动条仍是原生口径(≠8px、箭头>0)
S7 无泄漏:附件预览滚动条保持自身 6px 口径
并生成局部截图(面板全貌 + 代码框角落放大)打印测量值。
运行: QT_QPA_PLATFORM=offscreen python tests/diag_panel_scrollbar.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
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# 铁律(P0-01):临时数据库 + 临时配置必须在 import MainWindow 之前完成
from tests._test_env import isolate # noqa: E402
_TMP = isolate("panelscroll", config={"providers": {}, "mode_switch": True})
_DB_TMP = _TMP["db"]
_CFG_TMP = _TMP["config"]
from PyQt6.QtWidgets import (QApplication, QPlainTextEdit, QStyle, # noqa: E402
QStyleOptionSlider) # noqa: E402
from PyQt6.QtTest import QTest # noqa: E402
from PyQt6.QtCore import Qt # noqa: E402
from ui.views.main_window import MainWindow # noqa: E402
app = QApplication(sys.argv)
ok = True
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "docs", "agent-handoff", "evidence")
def check(name, cond, extra=""):
global ok
print((" PASS " if cond else " FAIL ") + name + ("" if cond else f" {extra}"), flush=True)
if not cond:
ok = False
def settle(ms=300):
for _ in range(int(ms / 20) + 1):
app.processEvents()
QTest.qWait(20)
def arrow_extent(sb, orient=Qt.Orientation.Vertical):
"""滚动条箭头子控件(sub-line)的实际尺寸(px):QSS 把 add-line/sub-line 置 0 后应为 0。
用 sb.style()(样式表代理风格)才能反映 QSS 效果;PyQt6 参数序 = (cc, opt, sc, widget)。
返回 -2 表示无法测量(样式代理缺失等),调用方不得把 -2 当作 0。"""
try:
st = sb.style()
if orient == Qt.Orientation.Vertical:
return st.subControlRect(QStyle.ComplexControl.CC_ScrollBar,
QStyleOptionSlider(),
QStyle.SubControl.SC_ScrollBarSubLine, sb).height()
return st.subControlRect(QStyle.ComplexControl.CC_ScrollBar,
QStyleOptionSlider(),
QStyle.SubControl.SC_ScrollBarSubLine, sb).width()
except Exception as e:
print(f" (arrow_extent 测量异常: {e})", flush=True)
return -1
window = MainWindow()
window.resize(1400, 800)
window.show()
settle(400)
panel = window.bash_panel
panel.expand_btn.click()
settle(500)
# ---- 造数据:一个展开的层,out_box 同时触发横/纵滚动条 ----
panel.on_started("sc1", "bash", {"command": "python long_report.py --all --verbose"})
panel.on_finished("sc1", "bash", True,
"$ python long_report.py --all --verbose\n"
+ "\n".join(f"line-{i:03d}" for i in range(60))
+ "\n[exit 0] (1.2s)")
settle(300)
lay = panel._layers["sc1"]
lay.toggle()
settle(200)
# 超宽单行(NoWrap)→ 横向滚动条;再追加 60 行 → 同时触发纵向滚动条
panel._layers["sc1"].out_box.setPlainText("X" * 3000 + "\n"
+ "\n".join(f"tail-{i:03d}" for i in range(60)))
settle(200)
ob = lay.out_box
sbh = ob.horizontalScrollBar()
sbv = ob.verticalScrollBar()
check("S0.1 前置:横滚动条可见(超宽单行)", sbh.isVisible(), str(sbh.isVisible()))
check("S0.2 前置:竖滚动条可见(60+ 行超出 230px 上限)", sbv.isVisible(), str(sbv.isVisible()))
# ---- S1/S2 代码框滚动条厚度 ----
check("S1 代码框横滚动条实际厚度 = 8px", sbh.height() == 8, f"h={sbh.height()}")
check("S1b 代码框横滚动条 sizeHint 厚 = 8px", sbh.sizeHint().height() == 8,
f"{sbh.sizeHint().height()}")
check("S2 代码框竖滚动条实际厚度 = 8px", sbv.width() == 8, f"w={sbv.width()}")
check("S2b 代码框竖滚动条 sizeHint 宽 = 8px", sbv.sizeHint().width() == 8,
f"{sbv.sizeHint().width()}")
# ---- S3 section 滚动区竖滚动条厚度 ----
# 让运行中栏溢出:再加 12 个已完成层(复用 P11 的溢出手法不必,层数即可)
for i in range(12):
cid = f"scf{i}"
panel.on_started(cid, "bash", {"command": f"cmd-{cid}"})
panel.on_finished(cid, "bash", True, f"$ cmd-{cid}\nok\n[exit 0] (0.1s)")
settle(300)
sdv = panel.sec_done.scroll.verticalScrollBar()
check("S0.3 前置:已完成栏溢出、竖滚动条可见", sdv.isVisible(), str(sdv.isVisible()))
check("S3 section 竖滚动条实际厚度 = 8px", sdv.width() == 8, f"w={sdv.width()}")
# ---- S4 箭头 extent ----
print(f" 测量:代码框 H 箭头 extent = {arrow_extent(sbh, Qt.Orientation.Horizontal)}px, "
f"V = {arrow_extent(sbv)}px; section V = {arrow_extent(sdv)}px",
flush=True)
check("S4 代码框横滚动条箭头 extent = 0", arrow_extent(sbh, Qt.Orientation.Horizontal) == 0,
str(arrow_extent(sbh, Qt.Orientation.Horizontal)))
check("S4b 代码框竖滚动条箭头 extent = 0", arrow_extent(sbv) == 0, str(arrow_extent(sbv)))
check("S4c section 竖滚动条箭头 extent = 0", arrow_extent(sdv) == 0, str(arrow_extent(sdv)))
# ---- S5 交汇角(render 到透明 pixmap 取样;offscreen 下文档区背景不填充,
# 但样式子控件(滚动条/边框/corner)正常渲染,可直接断言)----
from PyQt6.QtGui import QPainter, QPixmap # noqa: E402
w, h = ob.width(), ob.height()
pm = QPixmap(w, h); pm.fill(Qt.GlobalColor.transparent)
pr = QPainter(pm); ob.render(pr); pr.end()
img = pm.toImage()
def px_at(x, y):
c = img.pixelColor(x, y)
return (c.red(), c.green(), c.blue())
CORNER_BG = (0xfb, 0xfc, 0xfe) # #bl_code 背景色 = QSS 里 corner 规则的目标色
n_bg = n_white = 0
for dy in range(8):
for dx in range(8):
c = px_at(w - 1 - dx, h - 1 - dy)
if c == CORNER_BG:
n_bg += 1
if c == (255, 255, 255):
n_white += 1
# 健全性:样式确已作用到该框(文本色 #243043 与 handle #d0d0d0 应在渲染图中出现)
n_text = sum(1 for y in range(0, h, 2) for x in range(0, w, 2)
if px_at(x, y) == (0x24, 0x30, 0x43))
n_handle = sum(1 for y in range(h - 10, h) for x in range(0, w - 12, 2)
if px_at(x, y) == (0xd0, 0xd0, 0xd0))
print(f" 测量:corner 8x8 内 #fbfcfe 像素 = {n_bg},亮白(255,255,255)像素 = {n_white}"
f"文本色像素 = {n_text}handle色像素 = {n_handle}", flush=True)
check("S5 健全性:样式已作用于该框(文本色出现)", n_text > 0, f"n_text={n_text}")
check("S5b 健全性:handle #d0d0d0 出现在横滚动条带", n_handle > 0, f"n_handle={n_handle}")
check("S5c 交汇角渲染出 #fbfcfe= 代码框背景,::corner 规则生效)",
n_bg >= 1, f"n_bg={n_bg}")
check("S5d 交汇角无原生亮白方块(255,255,255", n_white == 0, f"n_white={n_white}")
img.save(os.path.join(OUT_DIR, "p2-02-outbox-render.png"))
# ---- S6 无泄漏:未命名 QPlainTextEdit 仍为原生口径 ----
probe = QPlainTextEdit()
probe.setPlainText("Y" * 3000)
probe.resize(200, 120)
probe.show()
settle(150)
psb = probe.horizontalScrollBar()
probe_sb_extent = arrow_extent(psb, Qt.Orientation.Horizontal)
check("S6 未命名代码框横滚动条非面板口径(原生厚≠8 或 有箭头)",
(psb.height() != 8) or probe_sb_extent > 0,
f"h={psb.height()} extent={probe_sb_extent}")
probe.close()
# ---- S7 无泄漏:附件预览滚动条保持自身 6px 口径 ----
att_sb = window.attachment_scroll_area.horizontalScrollBar()
att_hint_h = att_sb.sizeHint().height()
check("S7 附件预览横滚动条保持 6px(自身 QSS 未被面板规则覆盖)",
att_hint_h == 6, f"sizeHint.h={att_hint_h}")
# ---- 截图(局部:面板全貌 + 代码框角落放大 4x)----
# 注意:offscreen 下 ob.grab() 的文档区不填充(黑图),角落放大图从 S5 的
# render 图(样式子控件已正常渲染)裁出,才有证据价值
os.makedirs(OUT_DIR, exist_ok=True)
panel_path = os.path.join(OUT_DIR, "p2-02-panel.png")
panel.grab().save(panel_path)
crop = img.copy(max(0, w - 60), max(0, h - 60), 60, 60)
scaled = crop.scaled(240, 240,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.FastTransformation)
corner_path = os.path.join(OUT_DIR, "p2-02-codebox-corner-4x.png")
scaled.save(corner_path)
print(f" 截图:{panel_path}", flush=True)
print(f" 截图:{corner_path}(代码框右下角 60x60 → 4x", flush=True)
# ---- 收尾测量汇总 ----
print(f"\n 汇总:代码框 H={sbh.height()}px V={sbv.width()}px | "
f"section V={sdv.width()}px | corner #fbfcfe 像素={n_bg} | "
f"未命名框 H={psb.height()}px extent={probe_sb_extent} | 附件 H hint={att_hint_h}px",
flush=True)
print("\n===== " + ("ALL PASS" if ok else "HAS FAILURES") + " =====", flush=True)
if os.path.exists(_DB_TMP):
os.remove(_DB_TMP)
if os.path.exists(_CFG_TMP):
os.remove(_CFG_TMP)
# offscreen 铁律:os._exit 强制收尾(QtWebEngine 子进程可能不回收)
os._exit(0 if ok else 1)