diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b07e5e7..89dbc111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/ragger/navigator/navigation_scenario.py b/src/ragger/navigator/navigation_scenario.py index f03e7a3d..d060d7a2 100644 --- a/src/ragger/navigator/navigation_scenario.py +++ b/src/ragger/navigator/navigation_scenario.py @@ -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] @@ -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: @@ -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, @@ -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, @@ -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) diff --git a/tests/unit/navigator/test_navigation_scenario.py b/tests/unit/navigator/test_navigation_scenario.py index 0c066404..2f6b92f2 100644 --- a/tests/unit/navigator/test_navigation_scenario.py +++ b/tests/unit/navigator/test_navigation_scenario.py @@ -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)