Files
orisys-app/apps/studio/tests/test_window_surfaces.py
ykcheungab d477a4d8b7 接入 Studio 控件与菜单构建模块
将界面构建从主窗口移出,保留现有业务回调与标定路径。验证结果和完整门禁限制见 docs/architecture/studio-window-refactor-plan.zh.md。
2026-10-19 10:14:58 +08:00

237 lines
8.5 KiB
Python

"""Shell surfaces work independently and retain their behavior when wired."""
import pytest
from orisys_app_shared.i18n import get_translator, set_locale, t
from orisys_app_shared.sensors import scan_sensor_catalog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMainWindow
from orisys_studio.ui import sensor_picker as picker_module
from orisys_studio.ui.window_menus import WindowMenus
from orisys_studio.ui.window_ui import WindowUi
@pytest.fixture
def window(qt_app):
original_locale = get_translator().locale
widget = QMainWindow()
yield widget
widget.close()
widget.deleteLater()
set_locale(original_locale)
@pytest.fixture
def surface(window, tmp_path, monkeypatch):
catalog = scan_sensor_catalog(
shipped_root=tmp_path / "shipped",
user_root=tmp_path / "user",
)
monkeypatch.setattr(picker_module, "scan_sensor_catalog", lambda: catalog)
ui = WindowUi(window, theme="dark")
yield ui
# The picker is intentionally not parented until a sensor dialog adopts it.
ui.sensor_picker.close()
ui.sensor_picker.deleteLater()
def menus(window, **changes):
values = dict(
show_cal=False,
show_finger_3d=True,
show_fps=True,
show_diagnostics=False,
settings_expanded=False,
)
values.update(changes)
return WindowMenus(window, **values)
def test_central_surface_preserves_panel_order_and_ownership(window, surface):
assert window.centralWidget() is surface.central
layout = surface.central.layout()
assert [layout.itemAt(i).widget() for i in range(layout.count())] == [
surface.brand_bar,
surface.toolbar,
surface.analysis_progress,
surface.content_splitter,
]
assert layout.spacing() == 8
margins = layout.contentsMargins()
assert (margins.left(), margins.top(), margins.right(), margins.bottom()) == (
10,
) * 4
splitter = surface.content_splitter
panels = [
surface.mesh3d_panel,
surface.arrow_panel,
surface.force_plots,
surface.cal_panel,
]
assert splitter.orientation() == Qt.Orientation.Horizontal
assert [splitter.widget(i) for i in range(splitter.count())] == panels
assert all(panel.parentWidget() is splitter for panel in panels)
assert not splitter.childrenCollapsible()
assert surface.sensor_picker.parentWidget() is None
assert surface.status_chip is surface.toolbar.status_chip
def test_surface_refresh_keeps_3d_lazy_and_global_theme_unchanged(qt_app, surface):
stylesheet = qt_app.styleSheet()
plot_background = surface.force_plots.plot_widget.backgroundBrush().color()
surface.set_theme("light")
surface.retranslate()
assert surface.force_plots.plot_widget.backgroundBrush().color() != plot_background
assert qt_app.styleSheet() == stylesheet
assert surface.mesh3d_panel.view is None
assert surface.brand_bar.btn_live.text() == t("brand.nav_live")
@pytest.mark.parametrize("locale", ["zh_CN", "en_US"])
def test_menu_order_and_labels_match_the_shell(window, locale):
set_locale(locale)
ui = menus(window)
ui.retranslate()
assert [action.text() for action in window.menuBar().actions()] == [
t("menu.file"),
t("menu.session"),
t("menu.analysis"),
t("menu.view"),
t("menu.help"),
]
assert ui.session_menu.actions() == [
ui.act_run,
ui.act_stop,
ui.act_preview,
ui.act_reset,
ui.act_pause,
]
assert ui.act_analysis_setup.toolTip() == t("menu.analysis_setup_tip")
assert ui.act_recompute.toolTip() == t("menu.recompute_tip")
def test_shortcuts_are_available_without_any_business_host(window):
ui = menus(window)
assert [
action.shortcut().toString()
for action in (
ui.act_run,
ui.act_stop,
ui.act_preview,
ui.act_reset,
ui.act_pause,
ui.act_reset_view,
ui.act_play,
)
] == ["F5", "Esc", "F6", "Ctrl+R", "F7", "F8", "Space"]
assert ui.act_play.shortcutContext() == Qt.ShortcutContext.ApplicationShortcut
assert ui.act_play in window.actions()
seen = []
ui.act_run.triggered.connect(lambda: seen.append("run"))
ui.act_run.trigger()
assert seen == ["run"]
# These actions have no device, calibration or analysis handlers to invoke yet.
ui.act_reset.trigger()
ui.act_recompute.trigger()
assert not hasattr(window, "session")
def test_menu_initial_checks_and_appearance_refresh_are_separate(window):
ui = menus(window, show_cal=True, show_fps=False, settings_expanded=True)
assert ui.act_cal_panel.isChecked()
assert ui.act_finger_3d.isChecked()
assert not ui.act_show_fps.isChecked()
assert not ui.act_show_diagnostics.isChecked()
assert ui.act_settings.isChecked()
assert not ui.act_recompute.isChecked()
ui.sync_appearance(theme="light", language="en_US")
assert ui.act_theme_light.isChecked() and not ui.act_theme_dark.isChecked()
assert ui.act_lang_en.isChecked() and not ui.act_lang_zh.isChecked()
ui.act_theme_dark.trigger()
assert ui.act_theme_dark.isChecked() and not ui.act_theme_light.isChecked()
ui.act_lang_zh.trigger()
assert ui.act_lang_zh.isChecked() and not ui.act_lang_en.isChecked()
assert ui.act_cal_panel.isChecked()
def test_text_refresh_preserves_controller_owned_action_state(window):
ui = menus(window)
ui.act_reset.setText("context-dependent reset")
ui.act_pause.setChecked(True)
ui.act_run.setEnabled(False)
ui.retranslate()
ui.sync_appearance(theme="dark", language="zh_CN")
assert ui.act_reset.text() == "context-dependent reset"
assert ui.act_pause.isChecked()
assert not ui.act_run.isEnabled()
def test_shell_uses_the_surface_widgets_and_actions(make_window):
shell = make_window()
assert shell.centralWidget() is shell.ui.central
for name in (
"brand_bar", "sensor_picker", "toolbar", "status_chip", "analysis_progress",
"content_splitter", "mesh3d_panel", "arrow_panel", "force_plots", "cal_panel",
):
assert getattr(shell, name) is getattr(shell.ui, name)
assert shell.file_menu is shell.menus.file_menu
assert shell.act_play is shell.menus.act_play
assert shell.act_reset is shell.menus.act_reset
assert shell.mesh3d_panel.view is None
assert not shell.session.is_running
def test_shell_menu_commands_dispatch_once(make_window, monkeypatch):
from orisys_studio.app_window import StudioMainWindow
calls = []
def handler(name):
return lambda self: calls.append(name)
commands = {
"run": "_on_run_shortcut",
"stop": "_on_stop_shortcut",
"preview": "_on_preview_shortcut",
"reset": "_on_reset",
"reset_view": "_on_reset_view",
}
for name, method in commands.items():
monkeypatch.setattr(StudioMainWindow, method, handler(name))
shell = make_window()
calls.clear()
for name in commands:
action = getattr(shell.menus, f"act_{name}")
# No device is attached; exercise dispatch without availability policy.
action.setEnabled(True)
action.trigger()
assert calls == list(commands)
def test_shell_appearance_actions_refresh_widgets_and_menus(make_window, qt_app):
original_locale = get_translator().locale
original_stylesheet = qt_app.styleSheet()
shell = make_window()
try:
background = shell.force_plots.plot_widget.backgroundBrush().color()
shell.menus.act_theme_light.trigger()
assert shell.prefs["theme"] == "light"
assert shell.force_plots.plot_widget.backgroundBrush().color() != background
assert shell.menus.act_theme_light.isChecked()
assert not shell.menus.act_theme_dark.isChecked()
shell.menus.act_lang_en.trigger()
assert shell.prefs["language"] == "en_US"
assert shell.file_menu.title() == t("menu.file")
assert shell.brand_bar.btn_live.text() == t("brand.nav_live")
assert shell.act_reset.text() == t("menu.reset")
assert shell.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
shell.layout_ctl.apply_preset("replay")
assert shell.act_reset.text() == t("menu.reset_display")
assert shell.toolbar.btn_reset.text() == t("toolbar.reset_display")
assert shell.menus.act_lang_en.isChecked()
assert not shell.menus.act_lang_zh.isChecked()
assert shell.mesh3d_panel.view is None
finally:
set_locale(original_locale)
qt_app.setStyleSheet(original_stylesheet)