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.
This commit is contained in:
kdmukai
2023-03-05 19:48:50 -06:00
parent 4f1df7ac42
commit 25cb92a31f
9 changed files with 254 additions and 218 deletions
+46 -48
View File
@@ -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:
+52 -61
View File
@@ -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)
+41 -35
View File
@@ -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))
+35 -21
View File
@@ -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)
+27 -5
View File
@@ -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()
+9 -9
View File
@@ -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
),
]
+16 -27
View File
@@ -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
),
]
)
+12 -12
View File
@@ -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,
+16
View File
@@ -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