dsh UI improvement
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency-free smoke run for the customer UI.
|
||||
|
||||
``pytest`` is the canonical runner (``apps/customer/tests``), but this script
|
||||
needs nothing beyond the app's own runtime dependencies, so it can be used in
|
||||
minimal environments:
|
||||
|
||||
python tools/ui_check.py
|
||||
|
||||
It builds the real main window offscreen and asserts the layout, theming and
|
||||
status-feedback invariants that the UI work depends on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = APP_ROOT.parents[1]
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
|
||||
if _path.is_dir() and str(_path) not in sys.path:
|
||||
sys.path.insert(0, str(_path))
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ:
|
||||
_fonts = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts"
|
||||
if _fonts.is_dir():
|
||||
os.environ["QT_QPA_FONTDIR"] = str(_fonts)
|
||||
os.chdir(APP_ROOT)
|
||||
|
||||
FAILURES: list[str] = []
|
||||
CHECKS = 0
|
||||
|
||||
|
||||
def check(condition: bool, message: str) -> None:
|
||||
global CHECKS
|
||||
CHECKS += 1
|
||||
if not condition:
|
||||
FAILURES.append(message)
|
||||
|
||||
|
||||
def section(name: str) -> None:
|
||||
print(f"\n== {name} ==")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from orisys_app_shared.force.calibration_io import CalSample
|
||||
from orisys_app_shared.i18n import set_locale
|
||||
from orisys_app_shared.theme import build_qss, load_colors
|
||||
from orisys_customer.app_window import CustomerMainWindow
|
||||
|
||||
app = QApplication.instance() or QApplication(sys.argv)
|
||||
set_locale("zh_CN")
|
||||
|
||||
section("theme tokens")
|
||||
dark = set(load_colors("dark"))
|
||||
light = set(load_colors("light"))
|
||||
check(dark == light, f"token keys differ: {sorted(dark ^ light)}")
|
||||
for theme in ("dark", "light"):
|
||||
qss = build_qss(theme)
|
||||
check(len(qss) > 1000, f"{theme} qss suspiciously short")
|
||||
check(
|
||||
load_colors(theme)["arrow_canvas"] != load_colors(theme)["accent"],
|
||||
f"{theme}: arrow canvas collides with accent",
|
||||
)
|
||||
check("QProgressBar::chunk" in qss, f"{theme}: progress bar not styled")
|
||||
check("QHeaderView::section" in qss, f"{theme}: table header not styled")
|
||||
check("QRadioButton::indicator" in qss, f"{theme}: radio not styled")
|
||||
check("QScrollBar::handle" in qss, f"{theme}: scrollbar not styled")
|
||||
|
||||
scene_count = 0
|
||||
for theme in ("dark", "light"):
|
||||
for sensor in ("box", "finger"):
|
||||
section(f"window {theme}/{sensor}")
|
||||
scene_count += 1
|
||||
app.setStyleSheet(build_qss(theme))
|
||||
window = CustomerMainWindow(
|
||||
prefs={
|
||||
"language": "zh_CN",
|
||||
"theme": theme,
|
||||
"sensor_type": sensor,
|
||||
"show_cal_panel": True,
|
||||
}
|
||||
)
|
||||
window.resize(1440, 880)
|
||||
window.show()
|
||||
app.processEvents()
|
||||
|
||||
check(window.windowTitle() != "", "window title empty")
|
||||
check(
|
||||
window.status_chip.text() != "",
|
||||
"status chip has no phase text",
|
||||
)
|
||||
check(
|
||||
window.toolbar.btn_run.text().strip() not in ("", "▶"),
|
||||
f"run button is unlabelled: {window.toolbar.btn_run.text()!r}",
|
||||
)
|
||||
|
||||
panels = [
|
||||
window.mesh3d_panel,
|
||||
window.arrow_panel,
|
||||
window.force_plots,
|
||||
window.cal_panel,
|
||||
]
|
||||
for panel, width in zip(panels, window.content_splitter.sizes()):
|
||||
if panel.isVisible():
|
||||
check(
|
||||
width >= panel.minimumWidth(),
|
||||
f"{type(panel).__name__} width {width} < min {panel.minimumWidth()}",
|
||||
)
|
||||
|
||||
check(
|
||||
window.force_plots.width() >= window.force_plots.minimumWidth(),
|
||||
"force plots narrower than its minimum",
|
||||
)
|
||||
|
||||
# value cards must keep their text visible
|
||||
for card, label in zip(
|
||||
window.force_plots.value_cards, window.force_plots.value_labels
|
||||
):
|
||||
check(
|
||||
label.height() >= label.fontMetrics().height(),
|
||||
"value card text is clipped",
|
||||
)
|
||||
check(label.text() != "", "value card is empty")
|
||||
|
||||
# demo data flows through the plots and the calibration table
|
||||
for i in range(90):
|
||||
window.force_plots.push(i / 30.0, float(i % 7), -1.0, 1.0, unit="N")
|
||||
app.processEvents()
|
||||
check(
|
||||
window.force_plots.history.sample_count > 0,
|
||||
"plots did not accept samples",
|
||||
)
|
||||
|
||||
window.cal_panel.set_running(True)
|
||||
check(window.cal_panel.btn_record.isEnabled(), "record button stays disabled")
|
||||
window.cal_panel._store.upsert(
|
||||
"fn", CalSample(weight_n=1.0, raw_mean=9.7, n=120, std=0.02)
|
||||
)
|
||||
window.cal_panel._refresh_table()
|
||||
check(window.cal_panel.table.rowCount() == 1, "calibration row missing")
|
||||
check(
|
||||
not window.cal_panel.empty_hint.isVisible(),
|
||||
"empty hint still visible with rows",
|
||||
)
|
||||
|
||||
window._set_phase("running")
|
||||
check("status_running" not in window.status_chip.text(), "chip not localized")
|
||||
window._set_theme("light" if theme == "dark" else "dark")
|
||||
app.processEvents()
|
||||
|
||||
window._persist_prefs()
|
||||
check(
|
||||
len(window.prefs.get("splitter_sizes", [])) == 4,
|
||||
"splitter sizes not persisted",
|
||||
)
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
section("small window")
|
||||
app.setStyleSheet(build_qss("dark"))
|
||||
window = CustomerMainWindow(
|
||||
prefs={
|
||||
"language": "en_US",
|
||||
"theme": "dark",
|
||||
"sensor_type": "finger",
|
||||
"show_cal_panel": True,
|
||||
"show_finger_3d": True,
|
||||
}
|
||||
)
|
||||
window.resize(1280, 720)
|
||||
window.show()
|
||||
app.processEvents()
|
||||
for panel, width in zip(
|
||||
[
|
||||
window.mesh3d_panel,
|
||||
window.arrow_panel,
|
||||
window.force_plots,
|
||||
window.cal_panel,
|
||||
],
|
||||
window.content_splitter.sizes(),
|
||||
):
|
||||
if panel.isVisible():
|
||||
check(width >= panel.minimumWidth(), f"1280px: {type(panel).__name__} squeezed")
|
||||
oversize = []
|
||||
for child in window.findChildren(object):
|
||||
if not hasattr(child, "isVisible") or not child.isVisible():
|
||||
continue
|
||||
if not hasattr(child, "geometry"):
|
||||
continue
|
||||
parent = child.parentWidget()
|
||||
if parent is None or not parent.isVisible():
|
||||
continue
|
||||
geometry = child.geometry()
|
||||
if geometry.width() > parent.width() + 1 or geometry.height() > parent.height() + 1:
|
||||
oversize.append(type(child).__name__)
|
||||
check(not oversize, f"widgets overflow their parent: {oversize}")
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
print(f"\nscenes: {scene_count} checks: {CHECKS} failures: {len(FAILURES)}")
|
||||
for failure in FAILURES:
|
||||
print(f" FAIL: {failure}")
|
||||
return 1 if FAILURES else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user