import logging import pathlib import time import typing from abc import abstractmethod import allure import configs import constants.tesseract from constants.onboarding import OnboardingScreensHeaders import driver from constants import ColorCodes from driver.objects_access import walk_children from gui.components.onboarding.keys_already_exist_popup import KeysAlreadyExistPopup from gui.components.os.open_file_dialogs import OpenFileDialog from gui.components.picture_edit_popup import PictureEditPopup from gui.components.splash_screen import SplashScreen from gui.elements.button import Button from gui.elements.object import QObject from gui.elements.text_edit import TextEdit from gui.elements.text_label import TextLabel from gui.objects_map import onboarding_names from scripts.tools.image import Image from scripts.utils.system_path import SystemPath LOG = logging.getLogger(__name__) class AllowNotificationsView(QObject): def __init__(self): super(AllowNotificationsView, self).__init__(onboarding_names.mainWindow_AllowNotificationsView) self._start_using_status_button = Button(onboarding_names.mainWindow_Start_using_Status_StatusButton) @allure.step("Start using Status") def start_using_status(self): # TODO https://github.com/status-im/status-desktop/issues/15345 self._start_using_status_button.click(timeout=15) self.wait_until_hidden() class WelcomeToStatusView(QObject): def __init__(self): super(WelcomeToStatusView, self).__init__(onboarding_names.mainWindow_WelcomeView) self._i_am_new_to_status_button = Button(onboarding_names.mainWindow_I_am_new_to_Status_StatusBaseText) self._i_already_use_status_button = Button(onboarding_names.mainWindow_I_already_use_Status_StatusFlatButton) @allure.step('Open Keys view') def get_keys(self) -> 'KeysView': self._i_am_new_to_status_button.click() time.sleep(1) return KeysView().wait_until_appears() @allure.step('Open Sign by syncing form') def sync_existing_user(self) -> 'SignBySyncingView': self._i_already_use_status_button.click() time.sleep(1) return SignBySyncingView().wait_until_appears() class OnboardingView(QObject): def __init__(self, object_name): super(OnboardingView, self).__init__(object_name) self._back_button = Button(onboarding_names.mainWindow_onboardingBackButton_StatusRoundButton) @abstractmethod def back(self): pass class KeysView(OnboardingView): def __init__(self): super(KeysView, self).__init__(onboarding_names.mainWindow_KeysMainView) self._generate_key_button = Button(onboarding_names.mainWindow_Generate_new_keys_StatusButton) self._generate_key_for_new_keycard_button = Button( onboarding_names.mainWindow_Generate_keys_for_new_Keycard_StatusBaseText) self._import_seed_phrase_button = Button(onboarding_names.mainWindow_Import_seed_phrase) @allure.step('Open Profile view') def generate_new_keys(self) -> 'YourProfileView': self._generate_key_button.click() return YourProfileView().verify_profile_view_present() @allure.step('Open Keycard Init view') def generate_key_for_new_keycard(self) -> 'KeycardInitView': self._generate_key_for_new_keycard_button.click() return KeycardInitView().wait_until_appears() @allure.step('Open Import Seed Phrase view') def open_import_seed_phrase_view(self) -> 'ImportSeedPhraseView': self._import_seed_phrase_button.click() return ImportSeedPhraseView().wait_until_appears() @allure.step('Open Enter Seed Phrase view') def open_enter_seed_phrase_view(self) -> 'ImportSeedPhraseView': self._import_seed_phrase_button.click() return SeedPhraseInputView().wait_until_appears() @allure.step('Go back') def back(self) -> WelcomeToStatusView: self._back_button.click() return WelcomeToStatusView().wait_until_appears() class ImportSeedPhraseView(OnboardingView): def __init__(self): super(ImportSeedPhraseView, self).__init__(onboarding_names.mainWindow_KeysMainView) self._import_seed_phrase_button = Button(onboarding_names.keysMainView_PrimaryAction_Button) @allure.step('Open seed phrase input view') def open_seed_phrase_input_view(self): self._import_seed_phrase_button.click() return SeedPhraseInputView().wait_until_appears() @allure.step('Go back') def back(self) -> KeysView: self._back_button.click() return KeysView().wait_until_appears() class SignBySyncingView(OnboardingView): def __init__(self): super(SignBySyncingView, self).__init__(onboarding_names.mainWindow_KeysMainView) self._scan_or_enter_sync_code_button = Button(onboarding_names.keysMainView_PrimaryAction_Button) self._i_dont_have_other_device_button = Button( onboarding_names.mainWindow_iDontHaveOtherDeviceButton_StatusBaseText) @allure.step('Open sync code view') def open_sync_code_view(self): self._scan_or_enter_sync_code_button.click() return SyncCodeView().wait_until_appears() @allure.step('Open keys view') def open_keys_view(self): self._i_dont_have_other_device_button.click() return KeysView().wait_until_appears() class SyncCodeView(OnboardingView): def __init__(self): super(SyncCodeView, self).__init__(onboarding_names.mainWindow_SyncCodeView) self._enter_sync_code_button = Button(onboarding_names.switchTabBar_Enter_sync_code_StatusSwitchTabButton) self._paste_sync_code_button = Button(onboarding_names.mainWindow_Paste_StatusButton) self._syncing_enter_code_item = QObject(onboarding_names.mainWindow_syncingEnterCode_SyncingEnterCode) @allure.step('Open enter sync code form') def open_enter_sync_code_form(self): self._enter_sync_code_button.click() return self @allure.step('Paste sync code') def click_paste_button(self): self._paste_sync_code_button.click() @property @allure.step('Get wrong sync code message') def sync_code_error_message(self) -> str: return self._syncing_enter_code_item.object.syncCodeErrorMessage class SyncDeviceFoundView(OnboardingView): def __init__(self): super(SyncDeviceFoundView, self).__init__(onboarding_names.mainWindow_SyncingDeviceView_found) self._sync_text_item = QObject(onboarding_names.sync_text_item) @property @allure.step('Get device_found_notifications') def device_found_notifications(self) -> typing.List: device_found_notifications = [] for obj in driver.findAllObjects(self._sync_text_item.real_name): device_found_notifications.append(str(obj.text)) return device_found_notifications class SyncResultView(OnboardingView): def __init__(self): super(SyncResultView, self).__init__(onboarding_names.mainWindow_SyncDeviceResult) self._sync_result = QObject(onboarding_names.mainWindow_SyncDeviceResult) self._sign_in_button = Button(onboarding_names.mainWindow_Sign_in_StatusButton) self._synced_text_item = QObject(onboarding_names.synced_StatusBaseText) @property @allure.step('Get device synced notifications') def device_synced_notifications(self) -> typing.List: device_synced_notifications = [] for obj in driver.findAllObjects(self._synced_text_item.real_name): device_synced_notifications.append(str(obj.text)) return device_synced_notifications @allure.step('Wait until appears {0}') def wait_until_appears(self, timeout_msec: int = 10000): self._sign_in_button.wait_until_appears(timeout_msec) return self @allure.step('Sign in') def sign_in(self, attempts: int = 2): self._sign_in_button.click() try: return SplashScreen().wait_until_appears() except: assert attempts > 0, f'Next button was not clicked' self.sign_in(attempts - 1) class SeedPhraseInputView(OnboardingView): def __init__(self): super(SeedPhraseInputView, self).__init__(onboarding_names.mainWindow_SeedPhraseInputView) self._12_words_tab_button = Button(onboarding_names.switchTabBar_12_words_Button) self._18_words_tab_button = Button(onboarding_names.switchTabBar_18_words_Button) self._24_words_tab_button = Button(onboarding_names.switchTabBar_24_words_Button) self._seed_phrase_input_text_edit = TextEdit(onboarding_names.mainWindow_statusSeedPhraseInputField_TextEdit) self._import_button = Button(onboarding_names.mainWindow_Import_StatusButton) @property @allure.step('Get import button enabled state') def is_import_button_enabled(self) -> bool: return driver.waitForObjectExists(self._import_button.real_name, configs.timeouts.UI_LOAD_TIMEOUT_MSEC).enabled @allure.step('Input seed phrase') def input_seed_phrase(self, seed_phrase: typing.List[str], autocomplete: bool): if len(seed_phrase) == 12: if not self._12_words_tab_button.is_checked: self._12_words_tab_button.click() elif len(seed_phrase) == 18: if not self._18_words_tab_button.is_checked: self._18_words_tab_button.click() elif len(seed_phrase) == 24: if not self._24_words_tab_button.is_checked: self._24_words_tab_button.click() else: raise RuntimeError("Wrong amount of seed words", len(seed_phrase)) for index, word in enumerate(seed_phrase, start=1): self._seed_phrase_input_text_edit.real_name['objectName'] = f'enterSeedPhraseInputField{index}' if autocomplete: word_to_put = word[:-1] self._seed_phrase_input_text_edit.text = word_to_put driver.type(self._seed_phrase_input_text_edit.object, "") else: self._seed_phrase_input_text_edit.text = word @allure.step('Import seed phrase') def import_seed_phrase(self): self._import_button.click() return YourProfileView().wait_until_appears() @allure.step('Click import button') def click_import_seed_phrase_button(self): self._import_button.click() return KeysAlreadyExistPopup().wait_until_appears() class KeycardInitView(OnboardingView): def __init__(self): super(KeycardInitView, self).__init__(onboarding_names.mainWindow_KeycardInitView) self._message = TextLabel(onboarding_names.mainWindow_Plug_in_Keycard_reader_StatusBaseText) @property def message(self) -> str: return self._message.text def back(self) -> KeysView: self._back_button.click() return KeysView().wait_until_appears() class YourProfileView(OnboardingView): def __init__(self): super(YourProfileView, self).__init__(onboarding_names.mainWindow_InsertDetailsView) self._upload_picture_button = Button(onboarding_names.updatePicButton_StatusRoundButton) self._profile_image = QObject(onboarding_names.mainWindow_statusRoundImage_StatusRoundedImage) self._display_name_text_field = TextEdit(onboarding_names.mainWindow_statusBaseInput_StatusBaseInput) self._erros_text_label = TextLabel(onboarding_names.mainWindow_errorMessage_StatusBaseText) self._next_button = Button(onboarding_names.mainWindow_Next_StatusButton) self._login_input_object = QObject(onboarding_names.mainWindow_nameInput_StatusInput) self._clear_icon = QObject(onboarding_names.mainWindow_clear_icon_StatusIcon) self._identicon_ring = QObject(onboarding_names.mainWindow_IdenticonRing) self._view_header_title = TextLabel(onboarding_names.mainWindow_Header_Title) self._image_crop_workflow = QObject(onboarding_names.profileImageCropper) def verify_profile_view_present(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC): driver.waitFor(lambda: self._view_header_title.exists, timeout_msec) assert (getattr(self._view_header_title.object, 'text') == OnboardingScreensHeaders.YOUR_PROFILE_SCREEN_TITLE.value), \ f"YourProfileView is not shown or has wrong title, \ current screen title is {getattr(self._view_header_title.object, 'text')}" return self @property @allure.step('Get next button enabled state') def is_next_button_enabled(self) -> bool: return driver.waitForObjectExists(self._next_button.real_name, configs.timeouts.UI_LOAD_TIMEOUT_MSEC).enabled @property @allure.step('Get profile image') def get_profile_image(self) -> Image: return self._profile_image.image @property @allure.step('Check identicon ring visibility') def is_identicon_ring_visible(self): return self._identicon_ring.is_visible @property @allure.step('Get error messages') def get_error_message(self) -> str: return self._erros_text_label.text if self._erros_text_label.is_visible else '' @allure.step('Set user display name') def set_display_name(self, value: str): self._display_name_text_field.clear().text = value return self @allure.step('Get user display name') def get_display_name(self) -> str: return str(self._display_name_text_field.object.text) @allure.step('Click clear button') def clear_field(self): self._clear_icon.click() return self @allure.step('Set profile picture without file upload dialog') def set_profile_picture(self, path) -> PictureEditPopup: image_cropper = driver.waitForObjectExists(self._image_crop_workflow.real_name) fileuri = pathlib.Path(str(path)).as_uri() image_cropper.cropImage(fileuri) return PictureEditPopup() @allure.step('Set profile picture with file dialog upload') def set_user_image(self, fp: SystemPath) -> PictureEditPopup: allure.attach(name='User image', body=fp.read_bytes(), attachment_type=allure.attachment_type.PNG) self._upload_picture_button.hover() self._upload_picture_button.click() file_dialog = OpenFileDialog().wait_until_appears() file_dialog.open_file(fp) return PictureEditPopup().wait_until_appears() @allure.step('Open Create Password View') def next(self, attempts: int = 2) -> 'CreatePasswordView': self._next_button.click() try: return CreatePasswordView() except Exception as err: if attempts: return self.next(attempts - 1) else: raise err @allure.step('Go back') def back(self): self._back_button.click() return KeysView().wait_until_appears() class YourEmojihashAndIdenticonRingView(OnboardingView): def __init__(self): super(YourEmojihashAndIdenticonRingView, self).__init__(onboarding_names.mainWindow_InsertDetailsView) self._profile_image = QObject(onboarding_names.mainWindow_welcomeScreenUserProfileImage_StatusSmartIdenticon) self._chat_key_text_label = TextLabel(onboarding_names.mainWindow_insertDetailsViewChatKeyTxt_StyledText) self._next_button = Button(onboarding_names.mainWindow_Next_StatusButton) self._emoji_hash = QObject(onboarding_names.mainWindow_EmojiHash) self._identicon_ring = QObject(onboarding_names.mainWindow_userImageCopy_StatusSmartIdenticon) self._view_header_title = TextLabel(onboarding_names.mainWindow_Header_Title) def verify_emojihash_view_present(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC): driver.waitFor(lambda: self._view_header_title.exists, timeout_msec) assert (getattr(self._view_header_title.object, 'text') == OnboardingScreensHeaders.YOUR_EMOJIHASH_AND_IDENTICON_RING_SCREEN_TITLE.value), \ f"YourEmojihashAndIdenticonRingView is not shown or has wrong title, \ current screen title is {getattr(self._view_header_title.object, 'text')}" return self @property @allure.step('Get profile image icon') def profile_image(self) -> Image: self._profile_image.image.update_view() return self._profile_image.image @property @allure.step('Get profile image icon without identicon ring') def cropped_profile_image(self) -> Image: # Profile image without identicon_ring self._profile_image.image.update_view() self._profile_image.image.crop( driver.UiTypes.ScreenRectangle( 20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40 )) return self._profile_image.image @property @allure.step('Get chat key') def get_chat_key(self) -> str: return self._chat_key_text_label.text.split(':')[1].strip() @property @allure.step('Get emoji hash image') def get_emoji_hash(self) -> str: return str(getattr(self._emoji_hash.object, 'publicKey')) @property @allure.step('Verify: Identicon ring visible') def is_identicon_ring_visible(self): return self._identicon_ring.is_visible @allure.step('Click next in your emojihash and identicon ring view') def next(self): # TODO https://github.com/status-im/status-desktop/issues/15345 self._next_button.click(timeout=15) time.sleep(1) if configs.system.get_platform() == "Darwin": return AllowNotificationsView().wait_until_appears() @allure.step('Go back') def back(self): self._back_button.click() return YourProfileView().wait_until_appears() @allure.step @allure.step('Verify: User image contains text') def is_user_image_contains(self, text: str): crop = driver.UiTypes.ScreenRectangle( 20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40 ) return self.profile_image.has_text(text, constants.tesseract.text_on_profile_image, crop=crop) @allure.step @allure.step('Verify: User image background color') def is_user_image_background_white(self): crop = driver.UiTypes.ScreenRectangle( 20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40 ) return self.profile_image.has_color(constants.Color.WHITE, crop=crop) class CreatePasswordView(OnboardingView): def __init__(self): super(CreatePasswordView, self).__init__(onboarding_names.mainWindow_CreatePasswordView) self._new_password_text_field = TextEdit(onboarding_names.mainWindow_passwordViewNewPassword) self._confirm_password_text_field = TextEdit(onboarding_names.mainWindow_passwordViewNewPasswordConfirm) self._create_button = Button(onboarding_names.mainWindow_Create_password_StatusButton) self._password_view_object = QObject(onboarding_names.mainWindow_view_PasswordView) self._strength_indicator = QObject( onboarding_names.mainWindow_strengthInditactor_StatusPasswordStrengthIndicator) self._indicator_panel_object = QObject(onboarding_names.mainWindow_RowLayout) self._show_icon = QObject(onboarding_names.mainWindow_show_icon_StatusIcon) self._hide_icon = QObject(onboarding_names.mainWindow_hide_icon_StatusIcon) @allure.step('Get password content from first field') def get_password_from_first_field(self) -> str: return str(self._new_password_text_field.object.displayText) @allure.step('Get password content from confirmation field') def get_password_from_confirmation_field(self) -> str: return str(self._confirm_password_text_field.object.displayText) @property @allure.step('Verify: Create password button enabled') def is_create_password_button_enabled(self) -> bool: return driver.waitForObjectExists(self._create_button.real_name, configs.timeouts.UI_LOAD_TIMEOUT_MSEC).enabled @property @allure.step('Get strength indicator color') def strength_indicator_color(self) -> str: return self._strength_indicator.object.fillColor['name'] @property @allure.step('Get strength indicator text') def strength_indicator_text(self) -> str: return self._strength_indicator.object.text @property @allure.step('Get indicator panel green messages') def green_indicator_messages(self) -> typing.List[str]: messages = [] color = ColorCodes.GREEN.value for child in walk_children(self._indicator_panel_object.object): if getattr(child, 'id', '') == 'lowerCaseTxt' and child.color['name'] == color: messages.append(str(child.text)) elif getattr(child, 'id', '') == 'upperCaseTxt' and child.color['name'] == color: messages.append(str(child.text)) elif getattr(child, 'id', '') == 'numbersTxt' and child.color['name'] == color: messages.append(str(child.text)) elif getattr(child, 'id', '') == 'symbolsTxt' and child.color['name'] == color: messages.append(str(child.text)) return messages @property @allure.step('Get password error message') def password_error_message(self) -> str: return self._password_view_object.object.errorMsgText @allure.step('Click show icon by index') def click_show_icon(self, index): show_icons = driver.findAllObjects(self._show_icon.real_name) driver.mouseClick(show_icons[index]) @allure.step('Click hide icon by index') def click_hide_icon(self, index): hide_icons = driver.findAllObjects(self._hide_icon.real_name) driver.mouseClick(hide_icons[index]) @allure.step('Set password in first field') def set_password_in_first_field(self, value: str): self._new_password_text_field.clear().text = value @allure.step('Set password in confirmation field') def set_password_in_confirmation_field(self, value: str): self._confirm_password_text_field.clear().text = value @allure.step('Set password and open Confirmation password view') def create_password(self, value: str) -> 'ConfirmPasswordView': self.set_password_in_first_field(value) self.set_password_in_confirmation_field(value) self.click_create_password() return ConfirmPasswordView().wait_until_appears() def click_create_password(self): self._create_button.click() time.sleep(1) return ConfirmPasswordView().wait_until_appears() @allure.step('Go back') def back(self): self._back_button.click() return YourEmojihashAndIdenticonRingView().wait_until_appears() class ConfirmPasswordView(OnboardingView): def __init__(self): super(ConfirmPasswordView, self).__init__(onboarding_names.mainWindow_ConfirmPasswordView) self._confirm_password_text_field = TextEdit(onboarding_names.mainWindow_confirmAgainPasswordInput) self._confirm_button = Button(onboarding_names.mainWindow_Finalise_Status_Password_Creation_StatusButton) self._confirmation_password_view_object = QObject( onboarding_names.mainWindow_passwordView_PasswordConfirmationView) @property @allure.step('Get finalise password creation button enabled state') def is_confirm_password_button_enabled(self) -> bool: return driver.waitForObjectExists(self._confirm_button.real_name, configs.timeouts.UI_LOAD_TIMEOUT_MSEC).enabled @property @allure.step('Get confirmation error message') def confirmation_error_message(self) -> str: for child in walk_children(self._confirmation_password_view_object.object): if getattr(child, 'id', '') == 'errorTxt': return str(child.text) @allure.step('Set password') def set_password(self, value: str): self._confirm_password_text_field.text = value @allure.step('Click confirm password') def click_confirm_password(self): self._confirm_button.click() @allure.step('Confirm password') def confirm_password(self, value: str): self.set_password(value) self._confirm_button.click() @allure.step('Go back') def back(self): self._back_button.click() return CreatePasswordView().wait_until_appears() @allure.step('Get password content from confirmation again field') def get_password_from_confirmation_again_field(self) -> str: return str(self._confirm_password_text_field.object.displayText) class BiometricsView(OnboardingView): def __init__(self): super(BiometricsView, self).__init__(onboarding_names.mainWindow_TouchIDAuthView) self._yes_use_touch_id_button = Button(onboarding_names.mainWindow_touchIdYesUseTouchIDButton) self._prefer_password_button = Button(onboarding_names.mainWindow_touchIdIPreferToUseMyPasswordText) @allure.step('Select prefer password') def prefer_password(self): self._prefer_password_button.click() self.wait_until_hidden() @allure.step('Verify TouchID button') def is_touch_id_button_visible(self): return self._yes_use_touch_id_button.is_visible class LoginView(QObject): def __init__(self): super(LoginView, self).__init__(onboarding_names.mainWindow_LoginView) self._password_text_edit = TextEdit(onboarding_names.loginView_passwordInput) self._arrow_right_button = Button(onboarding_names.loginView_submitBtn) self._current_user_name_label = TextLabel(onboarding_names.loginView_currentUserNameLabel) self._change_account_button = Button(onboarding_names.loginView_changeAccountBtn) self._accounts_combobox = QObject(onboarding_names.accountsView_accountListPanel) self._password_object = QObject(onboarding_names.mainWindow_txtPassword_Input) self._add_new_user_item = QObject(onboarding_names.loginView_addNewUserItem_AccountMenuItemPanel) self._add_existing_user_item = QObject(onboarding_names.o_AccountMenuItemPanel) self._use_password_instead = QObject(onboarding_names.mainWindowUsePasswordInsteadStatusBaseText) @property @allure.step('Get login error message') def login_error_message(self) -> str: return str(self._password_object.object.validationError) @allure.step('Log in user') def log_in(self, account): if self._current_user_name_label.text != account.name: self._change_account_button.hover() self._change_account_button.click() self.select_user_name(account.name) if self._use_password_instead.is_visible: self._use_password_instead.click() self._password_text_edit.text = account.password self._arrow_right_button.click() @allure.step('Add existing user') def add_existing_status_user(self): self._current_user_name_label.click() self._add_existing_user_item.click() return SignBySyncingView().wait_until_appears() @allure.step('Select user') def select_user_name(self, user_name, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC): onboarding_names = set() def _select_user() -> bool: for index in range(self._accounts_combobox.object.count): name_object = self._accounts_combobox.object.itemAt(index) name_label = str(name_object.label) onboarding_names.add(name_label) if name_label == user_name: try: driver.mouseClick(name_object) except RuntimeError: continue return True return False assert driver.waitFor(lambda: _select_user(), timeout_msec), f'User name: "{user_name}" not found in {onboarding_names}'