214 lines
7.3 KiB
Python
214 lines
7.3 KiB
Python
#!/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())
|