Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.43.0] - 2026-03-02

### Added

- Added scenario navigation for review finishing with spinner

## [1.42.1] - 2026-02-05

### Fixed
Expand Down
60 changes: 49 additions & 11 deletions src/ragger/navigator/navigation_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ class NavigationScenarioData:
validation: Sequence[InstructionType]
dismiss_warning: Sequence[InstructionType]
pattern: str = ""
post_validation_spinner: Optional[str] = None

def __init__(self,
device: Device,
backend: BackendInterface,
use_case: UseCase,
approve: bool,
nb_warnings: int = 1):
nb_warnings: int = 1,
post_validation_spinner: Optional[str] = None):

if device.is_nano:
self.navigation = NavInsID.RIGHT_CLICK
self.validation = [NavInsID.BOTH_CLICK]
Expand Down Expand Up @@ -82,12 +85,16 @@ def __init__(self,
else:
raise NotImplementedError("Unknown use case")

# Dismiss the result modal in all cases
self.validation += [NavInsID.USE_CASE_STATUS_DISMISS]
# We assume no spinner == modal
if post_validation_spinner is None:
self.validation += [NavInsID.USE_CASE_STATUS_DISMISS]

else:
raise NotImplementedError("Unknown device")

# For both Nano AND e-ink : remember if we have a final spinner to check for
self.post_validation_spinner = post_validation_spinner


class NavigateWithScenario:

Expand All @@ -108,17 +115,29 @@ def _navigate_with_scenario(self,
if custom_screen_text is not None:
scenario.pattern = custom_screen_text

# Assert post validation modal (give True) only if no spinner
screen_change_after_last_instruction = scenario.post_validation_spinner is None

if do_comparison:
self.navigator.navigate_until_text_and_compare(
navigate_instruction=scenario.navigation,
validation_instructions=scenario.validation,
text=scenario.pattern,
path=path if path else self.screenshot_path,
test_case_name=test_name if test_name else self.test_name)
test_case_name=test_name if test_name else self.test_name,
screen_change_after_last_instruction=screen_change_after_last_instruction)
else:
self.navigator.navigate_until_text(navigate_instruction=scenario.navigation,
validation_instructions=scenario.validation,
text=scenario.pattern)
self.navigator.navigate_until_text(
navigate_instruction=scenario.navigation,
validation_instructions=scenario.validation,
text=scenario.pattern,
screen_change_after_last_instruction=screen_change_after_last_instruction)

if scenario.post_validation_spinner is not None:
# If the navigation ended with a spinner, we did not assert the post validation screen in
# navigate_until_text because we do not want to assert the spinner screen to avoid race issues
# We perform a manual text_on_screen check instead
self.backend.wait_for_text_on_screen(scenario.post_validation_spinner)

def _navigate_warning(self,
scenario: NavigationScenarioData,
Expand Down Expand Up @@ -150,11 +169,27 @@ def review_approve_with_warning(self,
do_comparison: bool = True,
warning_path: str = "warning",
nb_warnings: int = 1):
scenario = NavigationScenarioData(self.device, self.backend, UseCase.TX_REVIEW, True,
nb_warnings)
scenario = NavigationScenarioData(self.device,
self.backend,
UseCase.TX_REVIEW,
True,
nb_warnings=nb_warnings)
self._navigate_warning(scenario, test_name, do_comparison, warning_path)
self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison)

def review_approve_with_spinner(self,
spinner_text: str,
path: Optional[Path] = None,
test_name: Optional[str] = None,
custom_screen_text: Optional[str] = None,
do_comparison: bool = True):
scenario = NavigationScenarioData(self.device,
self.backend,
UseCase.TX_REVIEW,
True,
post_validation_spinner=spinner_text)
self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison)

def review_reject(self,
path: Optional[Path] = None,
test_name: Optional[str] = None,
Expand All @@ -170,8 +205,11 @@ def review_reject_with_warning(self,
do_comparison: bool = True,
warning_path: str = "warning",
nb_warnings: int = 1):
scenario = NavigationScenarioData(self.device, self.backend, UseCase.TX_REVIEW, False,
nb_warnings)
scenario = NavigationScenarioData(self.device,
self.backend,
UseCase.TX_REVIEW,
False,
nb_warnings=nb_warnings)
self._navigate_warning(scenario, test_name, do_comparison, warning_path)
self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison)

Expand Down
57 changes: 57 additions & 0 deletions tests/unit/navigator/test_navigation_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,60 @@ def test_navigation_scenario(self):
self.assertIsNone(self.navigate_with_scenario.review_reject())
self.assertIsNone(self.navigate_with_scenario.address_review_approve())
self.assertIsNone(self.navigate_with_scenario.address_review_reject())

def test_review_approve_with_spinner_nano(self):
"""Test that review_approve_with_spinner passes screen_change_after_last_instruction=False
to the navigator and calls backend.wait_for_text_on_screen with the spinner pattern."""
spinner_text = "Processing..."
self.navigate_with_scenario.review_approve_with_spinner(spinner_text)

# The navigator should have been called with screen_change_after_last_instruction=False
self.navigator.navigate_until_text_and_compare.assert_called_once()
call_kwargs = self.navigator.navigate_until_text_and_compare.call_args
self.assertFalse(
call_kwargs.kwargs.get(
'screen_change_after_last_instruction',
call_kwargs[1].get('screen_change_after_last_instruction', True)
if len(call_kwargs) > 1 else True))

# The backend should have been called with the spinner text
self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text)

def test_review_approve_with_spinner_no_comparison(self):
"""Test that review_approve_with_spinner with do_comparison=False uses navigate_until_text
with screen_change_after_last_instruction=False and calls wait_for_text_on_screen."""
spinner_text = "Signing..."
self.navigate_with_scenario.review_approve_with_spinner(spinner_text, do_comparison=False)

# navigate_until_text should have been called (not navigate_until_text_and_compare)
self.navigator.navigate_until_text_and_compare.assert_not_called()
self.navigator.navigate_until_text.assert_called_once()
call_kwargs = self.navigator.navigate_until_text.call_args
self.assertFalse(
call_kwargs.kwargs.get(
'screen_change_after_last_instruction',
call_kwargs[1].get('screen_change_after_last_instruction', True)
if len(call_kwargs) > 1 else True))

# The backend should have been called with the spinner text
self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text)

def test_review_approve_with_spinner_touchable(self):
"""Test spinner behavior on a touchable device (Stax)."""
device = Devices.get_by_type(DeviceType.STAX)
navigate_with_scenario = NavigateWithScenario(self.backend, self.navigator, device,
"test_name", self.directory)
spinner_text = "Please wait..."
navigate_with_scenario.review_approve_with_spinner(spinner_text)

# The navigator should have been called with screen_change_after_last_instruction=False
self.navigator.navigate_until_text_and_compare.assert_called_once()
call_kwargs = self.navigator.navigate_until_text_and_compare.call_args
self.assertFalse(
call_kwargs.kwargs.get(
'screen_change_after_last_instruction',
call_kwargs[1].get('screen_change_after_last_instruction', True)
if len(call_kwargs) > 1 else True))

# The backend should have been called with the spinner text
self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text)
Loading