From 25cb92a31fd8f42d22fcc3b4931c1ab7975a996e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 5 Mar 2023 19:48:50 -0600 Subject: [PATCH] Improved integration w/View.button_data options * Modifies Views to make button_data and its options available to external caller (FlowTests). * Adds button_data_selection for tighter integration with Views and better human-readability of FlowStep sequences. * Removes the unique recursive run() call in SettingsEntryUpdateSelectionView. --- src/seedsigner/views/seed_views.py | 94 ++++++++++---------- src/seedsigner/views/settings_views.py | 113 ++++++++++++------------- src/seedsigner/views/tools_views.py | 76 +++++++++-------- src/seedsigner/views/view.py | 56 +++++++----- tests/base.py | 32 +++++-- tests/test_flows_seed.py | 18 ++-- tests/test_flows_settings.py | 43 ++++------ tests/test_flows_tools.py | 24 +++--- tests/test_settings.py | 16 ++++ 9 files changed, 254 insertions(+), 218 deletions(-) create mode 100644 tests/test_settings.py diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 8f16bfac..9e45a1f7 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -71,41 +71,41 @@ class SeedsMenuView(View): Loading seeds, passphrases, etc ****************************************************************************""" class LoadSeedView(View): - def run(self): - SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS) - button_data=[ - SEED_QR, - TYPE_12WORD, - TYPE_24WORD, - CREATE, - ] + SEED_QR = (" Scan a SeedQR", FontAwesomeIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + CREATE = (" Create a seed", FontAwesomeIconConstants.PLUS) + button_data=[ + SEED_QR, + TYPE_12WORD, + TYPE_24WORD, + CREATE, + ] + def run(self): selected_menu_num = self.run_screen( ButtonListScreen, title="Load A Seed", is_button_text_centered=False, - button_data=button_data + button_data=self.button_data ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == SEED_QR: + if self.button_data[selected_menu_num] == self.SEED_QR: from .scan_views import ScanView return Destination(ScanView) - elif button_data[selected_menu_num] == TYPE_12WORD: + elif self.button_data[selected_menu_num] == self.TYPE_12WORD: self.controller.storage.init_pending_mnemonic(num_words=12) return Destination(SeedMnemonicEntryView) - elif button_data[selected_menu_num] == TYPE_24WORD: + elif self.button_data[selected_menu_num] == self.TYPE_24WORD: self.controller.storage.init_pending_mnemonic(num_words=24) return Destination(SeedMnemonicEntryView) - elif button_data[selected_menu_num] == CREATE: + elif self.button_data[selected_menu_num] == self.CREATE: from .tools_views import ToolsMenuView return Destination(ToolsMenuView) @@ -169,62 +169,60 @@ class SeedMnemonicEntryView(View): class SeedMnemonicInvalidView(View): + EDIT = "Review & Edit" + DISCARD = ("Discard", None, None, "red") + button_data = [EDIT, DISCARD] + def __init__(self): super().__init__() self.mnemonic: List[str] = self.controller.storage.pending_mnemonic def run(self): - EDIT = "Review & Edit" - DISCARD = ("Discard", None, None, "red") - button_data = [EDIT, DISCARD] - selected_menu_num = self.run_screen( WarningScreen, title="Invalid Mnemonic!", status_headline=None, text=f"Checksum failure; not a valid seed phrase.", show_back_button=False, - button_data=button_data, + button_data=self.button_data, ) - if button_data[selected_menu_num] == EDIT: + if self.button_data[selected_menu_num] == self.EDIT: return Destination(SeedMnemonicEntryView, view_args={"cur_word_index": 0}) - elif button_data[selected_menu_num] == DISCARD: + elif self.button_data[selected_menu_num] == self.DISCARD: self.controller.storage.discard_pending_mnemonic() return Destination(MainMenuView) class SeedFinalizeView(View): + FINALIZE = "Done" + PASSPHRASE = "BIP-39 Passphrase" + button_data = [FINALIZE] + def __init__(self): super().__init__() self.seed = self.controller.storage.get_pending_seed() self.fingerprint = self.seed.get_fingerprint(network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)) + if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: + self.button_data.append(self.PASSPHRASE) + def run(self): - FINALIZE = "Done" - PASSPHRASE = "BIP-39 Passphrase" - button_data = [] - - button_data.append(FINALIZE) - - if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: - button_data.append(PASSPHRASE) - selected_menu_num = self.run_screen( seed_screens.SeedFinalizeScreen, fingerprint=self.fingerprint, - button_data=button_data, + button_data=self.button_data, ) - if button_data[selected_menu_num] == FINALIZE: + if self.button_data[selected_menu_num] == self.FINALIZE: seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) - elif button_data[selected_menu_num] == PASSPHRASE: + elif self.button_data[selected_menu_num] == self.PASSPHRASE: return Destination(SeedAddPassphraseView) @@ -254,16 +252,16 @@ class SeedReviewPassphraseView(View): """ Display the completed passphrase back to the user. """ + EDIT = "Edit passphrase" + DONE = "Done" + button_data = [EDIT, DONE] + def __init__(self): super().__init__() self.seed = self.controller.storage.get_pending_seed() def run(self): - EDIT = "Edit passphrase" - DONE = "Done" - button_data = [EDIT, DONE] - # Get the before/after fingerprints network = self.settings.get_value(SettingsConstants.SETTING__NETWORK) passphrase = self.seed.passphrase @@ -279,14 +277,14 @@ class SeedReviewPassphraseView(View): fingerprint_without=fingerprint_without, fingerprint_with=fingerprint_with, passphrase=self.seed.passphrase, - button_data=button_data, + button_data=self.button_data, show_back_button=False, ) - if button_data[selected_menu_num] == EDIT: + if self.button_data[selected_menu_num] == self.EDIT: return Destination(SeedAddPassphraseView) - elif button_data[selected_menu_num] == DONE: + elif self.button_data[selected_menu_num] == self.DONE: seed_num = self.controller.storage.finalize_pending_seed() return Destination(SeedOptionsView, view_args={"seed_num": seed_num}, clear_history=True) @@ -498,6 +496,10 @@ class SeedExportXpubScriptTypeView(View): self.seed_num = seed_num self.sig_type = sig_type + self.button_data = [] + for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): + self.button_data.append(script_type) + def run(self): from .tools_views import ToolsAddressExplorerAddressTypeView @@ -511,10 +513,6 @@ class SeedExportXpubScriptTypeView(View): return Destination(ToolsAddressExplorerAddressTypeView, view_args=args, skip_current_view=True) else: return Destination(SeedExportXpubCoordinatorView, view_args=args, skip_current_view=True) - - button_data = [] - for script_type in self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES): - button_data.append(script_type) title = "Export Xpub" if self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER: @@ -524,7 +522,7 @@ class SeedExportXpubScriptTypeView(View): ButtonListScreen, title=title, is_button_text_centered=False, - button_data=button_data, + button_data=self.button_data, is_bottom_list=True, ) @@ -536,7 +534,7 @@ class SeedExportXpubScriptTypeView(View): else: script_types_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) - selected_display_name = button_data[selected_menu_num] + selected_display_name = self.button_data[selected_menu_num] args["script_type"] = script_types_settings_entry.get_selection_option_value_by_display_name(selected_display_name) if args["script_type"] == SettingsConstants.CUSTOM_DERIVATION: diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index b458ea0c..929c3c81 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -8,6 +8,9 @@ from seedsigner.models.settings import SettingsConstants, SettingsDefinition class SettingsMenuView(View): + IO_TEST = "I/O test" + DONATE = "Donate" + def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0): super().__init__() self.visibility = visibility @@ -16,51 +19,48 @@ class SettingsMenuView(View): # Used to preserve the rendering position in the list self.initial_scroll = initial_scroll - - def run(self): - IO_TEST = "I/O test" - DONATE = "Donate" - - settings_entries = SettingsDefinition.get_settings_entries( + self.settings_entries = SettingsDefinition.get_settings_entries( visibility=self.visibility ) - button_data=[e.display_name for e in settings_entries] + self.button_data=[e.display_name for e in self.settings_entries] - selected_button = 0 + self.selected_button = 0 if self.selected_attr: - for i, entry in enumerate(settings_entries): + for i, entry in enumerate(self.settings_entries): if entry.attr_name == self.selected_attr: - selected_button = i + self.selected_button = i break if self.visibility == SettingsConstants.VISIBILITY__GENERAL: - title = "Settings" + self.title = "Settings" # Set up the next nested level of menuing - button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) - next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) + self.button_data.append(("Advanced", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + self.next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) - button_data.append(IO_TEST) - button_data.append(DONATE) + self.button_data.append(self.IO_TEST) + self.button_data.append(self.DONATE) elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED: - title = "Advanced" + self.title = "Advanced" # So far there are no real Developer options; disabling for now - # button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) - # next = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) - next = None + # self.button_data.append(("Developer Options", None, None, None, SeedSignerCustomIconConstants.SMALL_CHEVRON_RIGHT)) + # self.next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__DEVELOPER}) + self.next_destination = None elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER: - title = "Dev Options" - next = None + self.title = "Dev Options" + self.next_destination = None + + def run(self): selected_menu_num = self.run_screen( ButtonListScreen, - title=title, + title=self.title, is_button_text_centered=False, - button_data=button_data, - selected_button=selected_button, + button_data=self.button_data, + selected_button=self.selected_button, scroll_y_initial_offset=self.initial_scroll, ) @@ -75,17 +75,17 @@ class SettingsMenuView(View): else: return Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED}) - elif selected_menu_num == len(settings_entries): - return next + elif selected_menu_num == len(self.settings_entries): + return self.next_destination - elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == IO_TEST: + elif len(self.button_data) > selected_menu_num and self.button_data[selected_menu_num] == self.IO_TEST: return Destination(IOTestView) - elif len(button_data) > selected_menu_num and button_data[selected_menu_num] == DONATE: + elif len(self.button_data) > selected_menu_num and self.button_data[selected_menu_num] == self.DONATE: return Destination(DonateView) else: - return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=settings_entries[selected_menu_num].attr_name, parent_initial_scroll=initial_scroll)) + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entries[selected_menu_num].attr_name, parent_initial_scroll=initial_scroll)) @@ -94,48 +94,44 @@ class SettingsEntryUpdateSelectionView(View): Handles changes to all selection-type settings (Multiselect, SELECT_1, Enabled/Disabled, etc). """ - def __init__(self, attr_name: str, parent_initial_scroll: int = 0): + def __init__(self, attr_name: str, parent_initial_scroll: int = 0, selected_button: int = None): super().__init__() self.settings_entry = SettingsDefinition.get_settings_entry(attr_name) - self.selected_button = None + self.selected_button = selected_button self.parent_initial_scroll = parent_initial_scroll - - # hook for test suite to override recursion in run() - self.is_test = False - - def run(self): - initial_value = self.settings.get_value(self.settings_entry.attr_name) - button_data = [] - checked_buttons = [] + self.button_data = [] + self.checked_buttons = [] + self.initial_value = self.settings.get_value(self.settings_entry.attr_name) for i, value in enumerate(self.settings_entry.selection_options): if type(value) == tuple: value, display_name = value else: display_name = value - button_data.append(display_name) - if (type(initial_value) == list and value in initial_value) or value == initial_value: - checked_buttons.append(i) + + self.button_data.append(display_name) + + if (type(self.initial_value) == list and value in self.initial_value) or value == self.initial_value: + self.checked_buttons.append(i) if self.selected_button is None: # Highlight the selection (for multiselect highlight the first # selected option). self.selected_button = i - if not self.selected_button: + if self.selected_button is None: self.selected_button = 0 - screen_args = dict( - display_name=self.settings_entry.display_name, - help_text=self.settings_entry.help_text, - button_data=button_data, - selected_button=self.selected_button, - checked_buttons=checked_buttons, - settings_entry_type=self.settings_entry.type, - ) + + def run(self): ret_value = self.run_screen( settings_screens.SettingsEntryUpdateSelectionScreen, - **screen_args + display_name=self.settings_entry.display_name, + help_text=self.settings_entry.help_text, + button_data=self.button_data, + selected_button=self.selected_button, + checked_buttons=self.checked_buttons, + settings_entry_type=self.settings_entry.type, ) destination = None @@ -158,8 +154,8 @@ class SettingsEntryUpdateSelectionView(View): destination = settings_menu_view_destination elif self.settings_entry.type == SettingsConstants.TYPE__MULTISELECT: - updated_value = list(initial_value) - if ret_value not in checked_buttons: + updated_value = list(self.initial_value) + if ret_value not in self.checked_buttons: # This is a new selection to add updated_value.append(value) else: @@ -168,7 +164,7 @@ class SettingsEntryUpdateSelectionView(View): else: # All other types are single selects (e.g. Enabled/Disabled, SELECT_1) - if value == initial_value: + if value == self.initial_value: # No change, return to menu return settings_menu_view_destination else: @@ -185,12 +181,7 @@ class SettingsEntryUpdateSelectionView(View): # All selects stay in place; re-initialize where in the list we left off self.selected_button = ret_value - if self.is_test: - # Test suite can't handle the recursive call. Fake it by returning us here - # via a Destination back to this View. - return Destination(self.__class__, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll)) - else: - return self.run() + return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_entry.attr_name, parent_initial_scroll=self.parent_initial_scroll, selected_button=self.selected_button), skip_current_view=True) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 20b7309b..572b996a 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -23,33 +23,38 @@ from .view import View, Destination, BackStackView class ToolsMenuView(View): + IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) + DICE = ("New seed", FontAwesomeIconConstants.DICE) + KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) + EXPLORER = "Address Explorer" + + def __init__(self): + super().__init__() + self.button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER] + + def run(self): - IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) - DICE = ("New seed", FontAwesomeIconConstants.DICE) - KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) - EXPLORER = "Address Explorer" - button_data = [IMAGE, DICE, KEYBOARD, EXPLORER] selected_menu_num = self.run_screen( ButtonListScreen, title="Tools", is_button_text_centered=False, - button_data=button_data + button_data=self.button_data ) if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == IMAGE: + elif self.button_data[selected_menu_num] == self.IMAGE: return Destination(ToolsImageEntropyLivePreviewView) - elif button_data[selected_menu_num] == DICE: + elif self.button_data[selected_menu_num] == self.DICE: return Destination(ToolsDiceEntropyMnemonicLengthView) - elif button_data[selected_menu_num] == KEYBOARD: + elif self.button_data[selected_menu_num] == self.KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) - elif button_data[selected_menu_num] == EXPLORER: + elif self.button_data[selected_menu_num] == self.EXPLORER: return Destination(ToolsAddressExplorerSelectSourceView) @@ -422,27 +427,27 @@ class ToolsCalcFinalWordDoneView(View): Address Explorer Views ****************************************************************************""" class ToolsAddressExplorerSelectSourceView(View): - def run(self): - SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) - SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - button_data = [] + SCAN_SEED = ("Scan a seed", FontAwesomeIconConstants.QRCODE) + SCAN_DESCRIPTOR = ("Scan wallet descriptor", FontAwesomeIconConstants.QRCODE) + TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) + button_data = [SCAN_SEED, SCAN_DESCRIPTOR, TYPE_12WORD, TYPE_24WORD] - seeds = self.controller.storage.seeds - for seed in seeds: + def __init__(self): + super().__init__() + self.seeds = self.controller.storage.seeds + seed_buttons =[] + for seed in self.seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) - button_data.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + seed_buttons.append((button_str, SeedSignerCustomIconConstants.FINGERPRINT, "blue")) + self.button_data = seed_buttons + self.button_data - button_data.append(SCAN_SEED) - button_data.append(SCAN_DESCRIPTOR) - button_data.append(TYPE_12WORD) - button_data.append(TYPE_24WORD) + def run(self): selected_menu_num = self.run_screen( ButtonListScreen, title="Address Explorer", - button_data=button_data, + button_data=self.button_data, is_button_text_centered=False, is_bottom_list=True, ) @@ -455,7 +460,7 @@ class ToolsAddressExplorerSelectSourceView(View): # knows to re-route us once the side flow is complete. self.controller.resume_main_flow = Controller.FLOW__ADDRESS_EXPLORER - if len(seeds) > 0 and selected_menu_num < len(seeds): + if len(self.seeds) > 0 and selected_menu_num < len(self.seeds): # User selected one of the n seeds return Destination( SeedExportXpubScriptTypeView, @@ -465,13 +470,13 @@ class ToolsAddressExplorerSelectSourceView(View): ) ) - elif button_data[selected_menu_num] in [SCAN_SEED, SCAN_DESCRIPTOR]: + elif self.button_data[selected_menu_num] in [self.SCAN_SEED, self.SCAN_DESCRIPTOR]: from seedsigner.views.scan_views import ScanView return Destination(ScanView) - elif button_data[selected_menu_num] in [TYPE_12WORD, TYPE_24WORD]: + elif self.button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]: from seedsigner.views.seed_views import SeedMnemonicEntryView - if button_data[selected_menu_num] == TYPE_12WORD: + if self.button_data[selected_menu_num] == self.TYPE_12WORD: self.controller.storage.init_pending_mnemonic(num_words=12) else: self.controller.storage.init_pending_mnemonic(num_words=24) @@ -480,6 +485,11 @@ class ToolsAddressExplorerSelectSourceView(View): class ToolsAddressExplorerAddressTypeView(View): + RECEIVE = "Receive Addresses" + CHANGE = "Change Addresses" + button_data = [RECEIVE, CHANGE] + + def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None): """ If the explorer source is a seed, `seed_num` and `script_type` must be @@ -528,10 +538,6 @@ class ToolsAddressExplorerAddressTypeView(View): def run(self): data = self.controller.address_explorer_data - RECEIVE = "Receive Addresses" - CHANGE = "Change Addresses" - button_data = [RECEIVE, CHANGE] - wallet_descriptor_display_name = None if "wallet_descriptor" in data: wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "") @@ -540,7 +546,7 @@ class ToolsAddressExplorerAddressTypeView(View): selected_menu_num = self.run_screen( ToolsAddressExplorerAddressTypeScreen, - button_data=button_data, + button_data=self.button_data, fingerprint=self.seed.get_fingerprint() if self.seed_num is not None else None, wallet_descriptor_display_name=wallet_descriptor_display_name, script_type=script_type, @@ -550,8 +556,8 @@ class ToolsAddressExplorerAddressTypeView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] in [RECEIVE, CHANGE]: - return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=button_data[selected_menu_num] == CHANGE)) + elif self.button_data[selected_menu_num] in [self.RECEIVE, self.CHANGE]: + return Destination(ToolsAddressExplorerAddressListView, view_args=dict(is_change=self.button_data[selected_menu_num] == self.CHANGE)) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 4e7e5db9..495d5c05 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -56,7 +56,6 @@ class View: self.canvas_width = self.renderer.canvas_width self.canvas_height = self.renderer.canvas_height - self.buttons = self.controller.buttons self.screen = None @@ -100,20 +99,22 @@ class Destination: return out - def run(self, run_before: Callable = None): + def _instantiate_view(self): if not self.view_args: # Can't unpack (**) None so we replace with an empty dict self.view_args = {} # Instantiate the `View_cls` with the `view_args` dict - view = self.View_cls(**self.view_args) + self.view = self.View_cls(**self.view_args) + - # Enable test suite modifications to the View before running it - if run_before: - run_before(view) + def _run_view(self): + return self.view.run() - # and run() it - return view.run() + + def run(self): + self._instantiate_view() + return self._run_view() def __eq__(self, obj): @@ -136,23 +137,22 @@ class Destination: # ######################################################################################### class MainMenuView(View): - def run(self): - from .seed_views import SeedsMenuView - from .settings_views import SettingsMenuView - from .scan_views import ScanView - from .tools_views import ToolsMenuView - menu_items = [ - (("Scan", FontAwesomeIconConstants.QRCODE), ScanView), - (("Seeds", FontAwesomeIconConstants.KEY), SeedsMenuView), - (("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView), - (("Settings", FontAwesomeIconConstants.GEAR), SettingsMenuView), - ] + SCAN = ("Scan", FontAwesomeIconConstants.QRCODE) + SEEDS = ("Seeds", FontAwesomeIconConstants.KEY) + TOOLS = ("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH) + SETTINGS = ("Settings", FontAwesomeIconConstants.GEAR) + def __init__(self): + super().__init__() + self.button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] + + + def run(self): selected_menu_num = self.run_screen( LargeButtonScreen, title="Home", title_font_size=26, - button_data=[entry[0] for entry in menu_items], + button_data=self.button_data, show_back_button=False, show_power_button=True, ) @@ -160,7 +160,21 @@ class MainMenuView(View): if selected_menu_num == RET_CODE__POWER_BUTTON: return Destination(PowerOptionsView) - return Destination(menu_items[selected_menu_num][1]) + if self.button_data[selected_menu_num] == self.SCAN: + from .scan_views import ScanView + return Destination(ScanView) + + elif self.button_data[selected_menu_num] == self.SEEDS: + from .seed_views import SeedsMenuView + return Destination(SeedsMenuView) + + elif self.button_data[selected_menu_num] == self.TOOLS: + from .tools_views import ToolsMenuView + return Destination(ToolsMenuView) + + elif self.button_data[selected_menu_num] == self.SETTINGS: + from .settings_views import SettingsMenuView + return Destination(SettingsMenuView) diff --git a/tests/base.py b/tests/base.py index 0010cef7..48e97738 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,7 +1,7 @@ import sys from dataclasses import dataclass from mock import MagicMock, patch -from typing import Callable, List, Type, Union +from typing import Callable, List, Tuple, Type, Union # Prevent importing modules w/Raspi hardware dependencies. # These must precede any SeedSigner imports. @@ -37,6 +37,7 @@ class BaseTest: def reset_settings(cls): """ Wipe and re-initialize the Settings singleton """ Settings._instance = None + BaseTest.remove_settings() @classmethod @@ -71,10 +72,22 @@ class BaseTest: @dataclass class FlowStep: - """ Trivial helper class to express FlowTest sequences below """ + """ + Trivial helper class to express FlowTest sequences below. + + * expected_view: verify that the next step in the sequence ended up at the right View. + * run_before: function that takes a View instance as an arg and modifies it before running the View. + * screen_return_value: mocked Screen interaction result: raw return value as if from the Screen. + * button_data_selection: mocked Screen interaction result: the View.button_data value of the desired option. + """ expected_view: Type[View] = None run_before: Callable = None - screen_return_value: Union[int, str] = None + screen_return_value: Union[int,str] = None + button_data_selection: Union[str,Tuple] = None + + def __post_init__(self): + if self.screen_return_value is not None and self.button_data_selection is not None: + raise Exception("Should only specify screen_return_value or button_data_selection") @@ -87,6 +100,7 @@ class FlowTest(BaseTest): * verifies that we landed on the expected_view (if provided). * mocks out the `View.run_screen()` to prevent the associated Screen class from instantiating. * patches in the FlowStep's screen_return_value (as if it came from user interaction). + * OR retrieves the index number of the specified button_data_selection and provides that as the Screen return value. * optional `run_before` method modifies the View when necessary to be compatible w/test suite limitations. * Runs the View and receives the resulting next Destination. * then repeats the process on the next Destination until the sequence is complete. @@ -103,10 +117,18 @@ class FlowTest(BaseTest): # Patch the `View.run_screen()` so we don't actually instantiate the Screen qualname = ".".join([next_destination.View_cls.__module__, next_destination.View_cls.__name__]) with patch(qualname + ".run_screen") as mock_run_screen: - mock_run_screen.return_value = flow_step.screen_return_value + next_destination._instantiate_view() + + if flow_step.run_before: + flow_step.run_before(next_destination.view) + + if flow_step.button_data_selection: + mock_run_screen.return_value = next_destination.view.button_data.index(flow_step.button_data_selection) + else: + mock_run_screen.return_value = flow_step.screen_return_value # Now we can run the View and grab its resulting next Destination - next_destination = next_destination.run(run_before=flow_step.run_before) + next_destination = next_destination._run_view() except Exception as e: import traceback traceback.print_exc() diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index 942c9710..17303cf9 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -21,16 +21,16 @@ class TestSeedFlows(FlowTest): Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=0, # ret SCAN + button_data_selection=MainMenuView.SCAN ), FlowStep( expected_view=scan_views.ScanView, run_before=load_seed_into_decoder, # simulate read SeedQR - screen_return_value=None, + # ret value is ignored ), FlowStep( expected_view=seed_views.SeedFinalizeView, - screen_return_value=0, # ret DONE + button_data_selection=seed_views.SeedFinalizeView.FINALIZE ), ] ) @@ -45,7 +45,7 @@ class TestSeedFlows(FlowTest): def test_with_mnemonic(mnemonic): sequence = [ FlowStep( - screen_return_value=1, # ret SEEDS + button_data_selection=MainMenuView.SEEDS ), FlowStep( expected_view=seed_views.SeedsMenuView, @@ -53,7 +53,7 @@ class TestSeedFlows(FlowTest): ), FlowStep( expected_view=seed_views.LoadSeedView, - screen_return_value=1 if len(mnemonic) == 12 else 2, # ret TYPE_12WORD or TYPE_24WORD + button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD, ), ] @@ -70,7 +70,7 @@ class TestSeedFlows(FlowTest): sequence.append( FlowStep( expected_view=seed_views.SeedFinalizeView, - screen_return_value=0, # ret DONE + button_data_selection=seed_views.SeedFinalizeView.FINALIZE ) ) @@ -94,7 +94,7 @@ class TestSeedFlows(FlowTest): mnemonic = "blush twice taste dawn feed second opinion lazy thumb play neglect impact".split() sequence = [ FlowStep( - screen_return_value=1 if len(mnemonic) == 12 else 2, # ret TYPE_12WORD or TYPE_24WORD + button_data_selection=seed_views.LoadSeedView.TYPE_12WORD if len(mnemonic) == 12 else seed_views.LoadSeedView.TYPE_24WORD, ), ] for word in mnemonic[:-1]: @@ -112,7 +112,7 @@ class TestSeedFlows(FlowTest): ), FlowStep( expected_view=seed_views.SeedMnemonicInvalidView, - screen_return_value=0 # ret EDIT + button_data_selection=seed_views.SeedMnemonicInvalidView.EDIT ), ] @@ -132,7 +132,7 @@ class TestSeedFlows(FlowTest): ), FlowStep( expected_view=seed_views.SeedMnemonicInvalidView, - screen_return_value=1 # ret DISCARD; this time we give up + button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD ), ] diff --git a/tests/test_flows_settings.py b/tests/test_flows_settings.py index 33c8e7a9..fd373ff1 100644 --- a/tests/test_flows_settings.py +++ b/tests/test_flows_settings.py @@ -17,30 +17,31 @@ class TestSettingsFlows(FlowTest): def test_persistent_settings(self): """ Basic flow from MainMenuView to enable/disable persistent settings """ # Which option are we testing? - settings_entries = SettingsDefinition.get_settings_entries() - nth_entry = [s.attr_name for s in settings_entries].index(SettingsConstants.SETTING__PERSISTENT_SETTINGS) - settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__PERSISTENT_SETTINGS) def mock_settingsmenuview_screen(view: Type[View]): # SettingsMenuView reaches into its `self.screen` so we need to mock it out view.screen = MagicMock() - + destination = self.run_sequence( Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=3, # ret SETTINGS + button_data_selection=MainMenuView.SETTINGS ), FlowStep( expected_view=settings_views.SettingsMenuView, run_before=mock_settingsmenuview_screen, - screen_return_value=nth_entry, # ret persistent settings + button_data_selection=settings_entry.display_name ), FlowStep( expected_view=settings_views.SettingsEntryUpdateSelectionView, - screen_return_value=[s[0] for s in settings_entry.selection_options].index(SettingsConstants.OPTION__ENABLED), + button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED), ), + FlowStep( + expected_view=settings_views.SettingsEntryUpdateSelectionView, + screen_return_value=RET_CODE__BACK_BUTTON + ) ] ) assert destination.View_cls == settings_views.SettingsMenuView @@ -50,41 +51,32 @@ class TestSettingsFlows(FlowTest): """ Multiselect Settings options should stay in-place; requires BACK to exit. """ # Which option are we testing? settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__COORDINATORS) - nth_entry = SettingsDefinition.get_settings_entries().index(settings_entry) def mock_settingsmenuview_screen(view: Type[View]): # SettingsMenuView reaches into its `self.screen` so we need to mock it out view.screen = MagicMock() - def disable_recursion(view: Type[View]): - # The recursion in SettingsEntryUpdateSelectionView for multiselect settings - # won't work with our FlowTest sequence structure. - view.is_test = True - destination = self.run_sequence( Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=3, # ret SETTINGS + button_data_selection=MainMenuView.SETTINGS ), FlowStep( expected_view=settings_views.SettingsMenuView, run_before=mock_settingsmenuview_screen, - screen_return_value=nth_entry, # ret Coordinator software + button_data_selection=settings_entry.display_name ), FlowStep( expected_view=settings_views.SettingsEntryUpdateSelectionView, - run_before=disable_recursion, screen_return_value=0, # select/deselect first option ), FlowStep( expected_view=settings_views.SettingsEntryUpdateSelectionView, - run_before=disable_recursion, screen_return_value=1, # select/deselect second option ), FlowStep( expected_view=settings_views.SettingsEntryUpdateSelectionView, - run_before=disable_recursion, screen_return_value=1, # select/deselect second option ), FlowStep( @@ -98,9 +90,6 @@ class TestSettingsFlows(FlowTest): def test_io_test(self): """ Basic flow from MainMenuView to I/O Test View """ - # I/O Test will be the nth_entry after we include the submenu to "Advanced" - nth_entry = len(SettingsDefinition.get_settings_entries()) + 1 - def mock_settingsmenuview_screen(view: Type[View]): # SettingsMenuView reaches into its `self.screen` so we need to mock it out view.screen = MagicMock() @@ -109,16 +98,16 @@ class TestSettingsFlows(FlowTest): Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=3, # ret SETTINGS + button_data_selection=MainMenuView.SETTINGS, ), FlowStep( expected_view=settings_views.SettingsMenuView, run_before=mock_settingsmenuview_screen, - screen_return_value=nth_entry, # ret I/O Test + button_data_selection=settings_views.SettingsMenuView.IO_TEST ), FlowStep( expected_view=settings_views.IOTestView, - screen_return_value=None, # ret value is ignored + # ret value is ignored ), ] ) @@ -140,16 +129,16 @@ class TestSettingsFlows(FlowTest): Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=3, # ret SETTINGS + button_data_selection=MainMenuView.SETTINGS, ), FlowStep( expected_view=settings_views.SettingsMenuView, run_before=mock_settingsmenuview_screen, - screen_return_value=nth_entry, # ret Donate + button_data_selection=settings_views.SettingsMenuView.DONATE ), FlowStep( expected_view=settings_views.DonateView, - screen_return_value=None, # ret value is ignored + # ret value is ignored ), ] ) diff --git a/tests/test_flows_tools.py b/tests/test_flows_tools.py index 4a834f2c..39f07961 100644 --- a/tests/test_flows_tools.py +++ b/tests/test_flows_tools.py @@ -2,11 +2,12 @@ # Must import this before the Controller from typing import Type -from mock import MagicMock +from mock import MagicMock, Mock from base import FlowTest, FlowStep from seedsigner.controller import Controller from seedsigner.models.seed import Seed +from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition from seedsigner.views.view import Destination, MainMenuView, View from seedsigner.views import scan_views, seed_views, tools_views @@ -31,11 +32,11 @@ class TestToolsFlows(FlowTest): Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=2 # ret TOOLS + button_data_selection=MainMenuView.TOOLS ), FlowStep( expected_view=tools_views.ToolsMenuView, - screen_return_value=3 # ret EXPLORER + button_data_selection=tools_views.ToolsMenuView.EXPLORER ), FlowStep( expected_view=tools_views.ToolsAddressExplorerSelectSourceView, @@ -43,11 +44,11 @@ class TestToolsFlows(FlowTest): ), FlowStep( expected_view=seed_views.SeedExportXpubScriptTypeView, - screen_return_value=0 # ret NATIVE_SEGWIT (1st script type option) + button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT) ), FlowStep( expected_view=tools_views.ToolsAddressExplorerAddressTypeView, - screen_return_value=0 # ret RECEIVE addrs + button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE ), FlowStep( expected_view=tools_views.ToolsAddressExplorerAddressListView, @@ -83,24 +84,23 @@ class TestToolsFlows(FlowTest): Destination(MainMenuView), sequence=[ FlowStep( - screen_return_value=2 # ret TOOLS + button_data_selection=MainMenuView.TOOLS ), FlowStep( expected_view=tools_views.ToolsMenuView, - screen_return_value=3 # ret EXPLORER + button_data_selection=tools_views.ToolsMenuView.EXPLORER ), FlowStep( expected_view=tools_views.ToolsAddressExplorerSelectSourceView, - screen_return_value=0 # ret SCAN SeedQR + button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.SCAN_SEED ), FlowStep( expected_view=scan_views.ScanView, run_before=load_seed_into_decoder, # simulate read SeedQR - screen_return_value=None, ), FlowStep( expected_view=seed_views.SeedFinalizeView, - screen_return_value=0, # ret DONE + button_data_selection=seed_views.SeedFinalizeView.FINALIZE ), FlowStep( expected_view=seed_views.SeedOptionsView, @@ -121,7 +121,7 @@ class TestToolsFlows(FlowTest): Destination(seed_views.SeedFinalizeView), sequence=[ FlowStep( - screen_return_value=1, # ret PASSPHRASE + button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE ), FlowStep( expected_view=seed_views.SeedAddPassphraseView, @@ -129,7 +129,7 @@ class TestToolsFlows(FlowTest): ), FlowStep( expected_view=seed_views.SeedReviewPassphraseView, - screen_return_value=1, # ret DONE + button_data_selection=seed_views.SeedReviewPassphraseView.DONE ), FlowStep( expected_view=seed_views.SeedOptionsView, diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 00000000..891cce1d --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,16 @@ +from base import BaseTest +from seedsigner.models.settings import Settings +from seedsigner.models.settings_definition import SettingsConstants + + +class TestSettings(BaseTest): + + def test_reset_settings(self): + """ BaseTest.reset_settings() should wipe out any previous Settings changes """ + settings = Settings.get_instance() + settings.set_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS, SettingsConstants.OPTION__ENABLED) + assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED + + BaseTest.reset_settings() + settings = Settings.get_instance() + assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED