Make the rail reset zero the sensor in live and the display in replay

The replay view disabled a control whose display reset is exactly what an
operator wants there, and the bare "Reset" hid that the live one zeroes the
sensor tracker; the label, tooltip and status line now name the target, and
Ctrl+R follows the view. Verified with the studio suite, ui_check (628
checks) and real-GL rail snapshots of both views.
This commit is contained in:
ykcheungab
2026-10-04 11:09:22 +08:00
parent aa526c62a9
commit 6fb2f72c6e
9 changed files with 525 additions and 50 deletions
+64 -8
View File
@@ -7,12 +7,15 @@ Written against the requirement rather than the implementation:
* the primary button is a play/pause toggle in **both** views, and pressing it while a
session runs pauses without stopping — the device stays connected;
* stop ends the session (live) or leaves the replay (playback);
* reset is disabled in the replay view, where there is no tracker to zero.
* reset is one control with one meaning *per view*: the sensor tracker's in the live
view (which owns a device session), the display's in the replay view (which owns
none).
"""
from __future__ import annotations
import ui_check
from orisys_app_shared.i18n import t
from tests.conftest import finish_run, running_window, show_window
@@ -77,28 +80,81 @@ def test_stop_ends_the_session(make_window):
stuck.release()
def test_reset_is_for_the_device_and_is_disabled_in_the_replay_view(make_window):
"""Reset stays a *device* control, and it is the thing the replay view cannot offer.
def test_a_paused_session_still_offers_both_stop_and_reset(make_window):
"""Pause holds the session; the device is still open, so both controls stay offered.
This is the state the two rules are easiest to get wrong in: the transport is
unchecked, which is not "stopped", so Stop must still end the session and the reset
must still reach the device it is holding.
"""
window = show_window(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
stuck = ui_check._StuckWorker()
window._ensure_workers = lambda: None
window._box_worker = stuck
window._finger_worker = stuck
window._start_run()
window._run_timer.stop()
try:
# A first frame is the application's own path to ``running``, and the moment
# the rail recomputes what the session can do (the reset keys off it), so the
# session is reached through that path rather than stated directly.
stuck.emit(ui_check._fake_packet(1))
window._on_run_tick()
assert window._session_phase == "running"
assert window.toolbar.btn_reset.isEnabled(), (
"a running session must offer the tracker reset before it is paused"
)
window.toolbar.btn_run.setChecked(False)
assert window._display_paused is True and window._running
assert window.toolbar.btn_stop.isEnabled(), "no way out of the paused session"
assert window.toolbar.btn_reset.isEnabled(), (
"pausing took the device's reset away: the device is still open"
)
assert window.toolbar.reset_target() == "sensor"
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip")
window.toolbar.btn_reset.click()
assert stuck.reset_requests == 1, (
"the paused session's reset did not reach the device"
)
window.toolbar.btn_stop.click()
assert not window._running, "Stop did not end the paused session"
finally:
stuck.release()
def test_reset_is_the_sensor_in_live_and_the_display_in_the_replay_view(make_window):
"""Reset stays one control, and each view gets the meaning it can act on.
The enabled state of reset once frames are flowing is covered by
``test_ui_smoke.py::test_a_reset_reaches_the_device_and_is_recorded`` (which drives
``_on_reset``); this test pins what the unification is about: reset and stop are two
different controls, and the replay view disables the one that needs a device.
``test_ui_smoke.py::test_a_zeroing_is_recorded_only_after_the_worker_confirms_it``
(which drives ``_on_reset``); this test pins what the unification is about: reset and
stop are two different controls, and the view decides which of the two things the
reset acts on — the sensor tracker where a session owns a device, the display where
there is nothing of the device to act on.
"""
window, stuck = running_window(make_window)
try:
assert window.toolbar.btn_reset.text() != window.toolbar.btn_stop.text(), (
"reset and stop must be distinguishable controls"
)
assert window.toolbar.reset_target() == "sensor"
assert window.toolbar.btn_stop.isEnabled(), "stop is not reachable during a session"
finally:
finish_run(window, stuck)
replay = show_window(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
replay.brand_bar.btn_playback.click()
assert not replay.toolbar.btn_reset.isEnabled(), (
"reset is offered in the replay view, where there is no tracker to zero"
assert replay.toolbar.reset_target() == "display", (
"the replay view owns no device: its reset is the display's"
)
assert replay.toolbar.btn_reset.isEnabled(), (
"the display reset is unavailable in the replay view"
)
assert replay.toolbar.btn_reset.text() == t("toolbar.reset_display")
assert hasattr(replay.toolbar, "btn_stop"), "the replay view has no stop control"
assert replay.toolbar.btn_run.text().startswith("▶"), (
f"the replay transport idle reads {replay.toolbar.btn_run.text()!r}, not play"
+26 -5
View File
@@ -14,7 +14,8 @@ elements are supposed to exist*. These tests make that claim falsifiable against
permanently skipped;
* the transport group the manifest declares is the behaviour of a *live* session: a running
session keeps the tracker reset available and reads "pause", and a paused session reads
"resume". That is the defect class this artifact exists to prevent — a state assertion
"resume"; in the replay view the same declared reset is the display reset and says so.
That is the defect class this artifact exists to prevent — a state assertion
about widgets passing while the visible result is wrong.
A deliberate asymmetry: the existence assertions are hard (a missing element fails), while
@@ -248,12 +249,18 @@ def test_the_transport_group_matches_the_live_session_states(make_window):
assert reset is window.toolbar.btn_reset
assert window._session_phase == "running", window._session_phase
# A running session: the transport reads pause and the reset is available.
# A running session: the transport reads pause and the reset is available —
# and it says which of the two things it acts on (the sensor tracker).
assert transport.isChecked(), (
"a running session must show the transport engaged"
)
assert transport.text() == t("toolbar.pause"), transport.text()
assert reset.isEnabled(), "a running session must offer the tracker reset"
assert window.toolbar.reset_target() == "sensor", (
"the live view's reset is the sensor tracker's"
)
assert reset.text() == t("toolbar.reset_sensor"), reset.text()
assert reset.toolTip() == t("toolbar.reset_sensor_tip"), reset.toolTip()
assert stop.isEnabled(), "a running session must offer stop"
# Paused: the transport reads resume, and the device action stays available.
@@ -286,7 +293,13 @@ def test_the_transport_group_matches_the_live_session_states(make_window):
def test_the_transport_of_the_replay_view_is_not_the_device_reset(make_window):
"""The same controls, the other view: an idle transport and no device to zero."""
"""The same controls, the other view: an idle transport, and a reset that resets the display.
The manifest declares one ``reset`` element for both views, so this is where the
claim that it means *one* thing per view is checked: in the replay view it is the
display reset (named as such, and there is no device for it to touch), not a
disabled control and not a tracker zero.
"""
window = _window(make_window, "box", "replay")
try:
by_id = elements.manifest().by_id()
@@ -294,8 +307,16 @@ def test_the_transport_of_the_replay_view_is_not_the_device_reset(make_window):
reset = elements.resolve(window, by_id["reset"])
assert not transport.isChecked()
assert transport.text() == t("toolbar.playback"), transport.text()
assert not reset.isEnabled(), (
"reset is offered where there is no tracker to zero"
assert window.toolbar.reset_target() == "display", (
"the replay view's reset is the display's: this view owns no device"
)
assert reset.isEnabled(), (
"the display reset is available in the replay view (the reported defect)"
)
assert reset.text() == t("toolbar.reset_display"), reset.text()
assert reset.toolTip() == t("toolbar.reset_display_tip"), reset.toolTip()
assert window.act_reset.text() == t("menu.reset_display"), (
"Ctrl+R is the same control on a second surface and must be named the same way"
)
finally:
window.close()
+126 -7
View File
@@ -1532,7 +1532,6 @@ def test_the_nav_items_switch_the_main_window_layout(make_window):
assert not window.replay.is_loaded()
assert window.force_plots.mode() == "follow", "browsing starts with no recording"
assert window.toolbar.btn_replay_video.isVisible(), "no video picker in the replay"
live_sizes = tuple(window.content_splitter.sizes())
window.brand_bar.btn_live.click()
assert window.view_preset() == "live"
@@ -2050,7 +2049,8 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
wrong. So the rail's Stop button — the same one the live view uses to end a session
— is an unconditional stop in this view, and it covers both things that can run
there: plain playback and a recompute pass. The tracker reset owns no device here,
so it is disabled rather than repurposed.
so it is the *display* reset and says so (see
``test_the_replay_reset_is_the_display_reset_and_never_the_device``).
"""
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
@@ -2061,10 +2061,10 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
assert window._load_recording(replay_recording)
QApplication.processEvents()
assert not window.toolbar.btn_reset.isEnabled(), (
"the replay view offers to zero a device tracker it has not got"
assert window.toolbar.btn_reset.isEnabled(), (
"the replay view's display reset is unavailable"
)
assert window.toolbar.btn_reset.text() == t("toolbar.reset")
assert window.toolbar.btn_reset.text() == t("toolbar.reset_display")
assert window.toolbar.btn_stop.text() == t("toolbar.stop_label")
assert not window.toolbar.btn_stop.isEnabled(), (
"a stop is offered while nothing is running"
@@ -2136,12 +2136,131 @@ def test_the_live_view_keeps_the_tracker_reset(make_window):
window = _show(make_window(), (1440, 880))
window.brand_bar.btn_live.click()
QApplication.processEvents()
assert window.toolbar.btn_reset.text() == t("toolbar.reset"), (
assert window.toolbar.reset_target() == "sensor", (
"the live view's reset is the sensor tracker's"
)
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor"), (
"the rail's reset slot is not the tracker reset in the live view"
)
# Idle, the tip explains what is missing instead of leaving a dead button.
assert not window.toolbar.btn_reset.isEnabled()
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_unavailable"), (
window.toolbar.btn_reset.toolTip()
)
window.close()
def test_the_replay_reset_is_the_display_reset_and_never_the_device(
make_window, replay_recording
):
"""The reported defect: "the reset button cannot be used in replay mode".
The replay view owns no device, so the tracker reset is meaningless there — but the
display reset is exactly what an operator wants. The control is the same one, and
these are the facts that have to hold together: it is available, it puts the display
back (curves redrawn at their default framing, playhead at the start), and it never
asks a device worker to zero anything — the window is armed with one, and its request
counter stays at zero.
"""
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
stuck = ui_check._StuckWorker()
window._ensure_workers = lambda: None
window._box_worker = stuck
window._finger_worker = stuck
window.brand_bar.btn_playback.click()
QApplication.processEvents()
assert window._load_recording(replay_recording)
QApplication.processEvents()
assert window.toolbar.reset_target() == "display"
assert window.toolbar.btn_reset.isEnabled(), (
"the replay view must offer the display reset"
)
assert window.toolbar.btn_reset.text() == t("toolbar.reset_display")
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_display_tip")
assert window.act_reset.text() == t("menu.reset_display"), (
"Ctrl+R is the same control and must be named the same way"
)
assert window.act_reset.isEnabled()
# What the reset has to undo: a moved playhead and a browsed window.
window.replay.set_position(0.9)
window.force_plots.set_window(0.4, 1.1)
QApplication.processEvents()
assert window.force_plots.mode() == "browse"
assert window.replay.position() > 0.5
window.toolbar.btn_reset.click()
QApplication.processEvents()
assert stuck.reset_requests == 0, (
"the replay's reset asked a device worker to zero the tracker"
)
assert window.replay.position() == 0.0, (
f"the reset did not start the replay over: {window.replay.position()}"
)
assert window.force_plots.cursor_time() == 0.0
assert window.force_plots.mode() == "browse", (
"the reset left the charts out of the replay's own mode"
)
sizes = [
0 if curve.getData()[0] is None else int(curve.getData()[0].size)
for curve in window.force_plots.curves
]
assert all(size > 0 for size in sizes), (
f"the reset left the replay with blank charts: {sizes}"
)
assert window.statusBar().currentMessage() == t("status.reset_display"), (
window.statusBar().currentMessage()
)
window.close()
def test_the_live_reset_zeroes_the_device_and_restarts_the_display_baseline(make_window):
"""The live half of the same control: the device is asked, and the tip's side
effect (the display baseline restarting) really happens."""
from orisys_app_shared.i18n import t
from PySide6.QtWidgets import QApplication
window = _show(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
stuck = ui_check._StuckWorker()
window._ensure_workers = lambda: None
window._box_worker = stuck
window._finger_worker = stuck
window._start_run()
window._run_timer.stop()
try:
# The first frame is the application's own path to ``running`` — and the moment
# the rail recomputes what a session can do (see ``_update_run_enabled``), which
# is why the state is reached through it rather than stated directly.
stuck.emit(ui_check._fake_packet(1))
window._on_run_tick()
QApplication.processEvents()
assert window._session_phase == "running"
assert window.toolbar.btn_reset.isEnabled()
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip")
restarts: list = []
window.force_plots.request_presentation_reestimate = lambda: restarts.append(True)
window.toolbar.btn_reset.click()
assert stuck.reset_requests == 1, "the live reset did not reach the device"
assert restarts == [True], (
"the display baseline was not restarted with the zero (the tooltip's side effect)"
)
assert window.statusBar().currentMessage() == t("status.reset_pending"), (
window.statusBar().currentMessage()
)
assert window.act_reset.text() == t("menu.reset"), (
"Ctrl+R must be named as the sensor reset in the live view"
)
finally:
window._stop_run(reason="user_stop")
stuck.release()
def test_the_tracker_reset_follows_the_session_across_a_view_switch(make_window):
"""Zeroing the tracker is a *device* action: which panel is on screen is not a fact
about the session, so switching live → calibration → live must not take the reset
@@ -2171,7 +2290,7 @@ def test_the_tracker_reset_follows_the_session_across_a_view_switch(make_window)
assert window.toolbar.btn_reset.isEnabled() and window.act_reset.isEnabled(), (
"a running session must offer the tracker reset before any view switch"
)
assert window.toolbar.btn_reset.text() == t("toolbar.reset")
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
window.brand_bar.btn_calibration.click()
QApplication.processEvents()