diff --git a/test/gui/features/spaces/spaces.feature b/test/gui/features/spaces/spaces.feature index 4f2bb3267..6aaeaf2b6 100644 --- a/test/gui/features/spaces/spaces.feature +++ b/test/gui/features/spaces/spaces.feature @@ -79,7 +79,8 @@ Feature: Project spaces """ When the user opens the activity tab And the user selects "Not Synced" tab in the activity - Then the following activities should be displayed in not synced table + Then the folder "simple-folder" should be blacklisted + And the following activities should be displayed in not synced table | resource | status | account | | simple-folder | Blacklisted | Alice Hansen@%local_server_hostname% | diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 619b67507..10eb8fd36 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -94,7 +94,7 @@ Feature: Syncing files When the user selects manual sync folder option in advanced section And the user sets the sync path in sync connection wizard And the user selects "Personal" space in sync connection wizard - And the user selects the following folders to sync: + And the user selects only the following folders to sync: | folder | | simple-folder | Then the folder "simple-folder" should exist on the file system @@ -473,7 +473,7 @@ Feature: Syncing files When the user selects manual sync folder option in advanced section And the user sets the temp folder "~`!@#$^&()-_=+{[}];',)PRN%" as local sync path in sync connection wizard And the user selects "Personal" space in sync connection wizard - And the user selects the following folders to sync: + And the user selects only the following folders to sync: | folder | | ~`!@#$^&()-_=+{[}];',) | | simple-folder | diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index ad5d95a81..910f4c0da 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -82,6 +82,7 @@ def get_app_env(): 'max_timeout': 60, 'min_timeout': 5, 'lowest_timeout': 1, + 'min_sync_timeout': 10, 'files_for_upload': os.path.join(CURRENT_DIR.parent, 'files-for-upload'), } diff --git a/test/gui/helpers/ScreenRecorder.py b/test/gui/helpers/ScreenRecorder.py index 88bdbdda4..a703cec20 100644 --- a/test/gui/helpers/ScreenRecorder.py +++ b/test/gui/helpers/ScreenRecorder.py @@ -22,7 +22,8 @@ def _record_loop(video_path): size=(width, height), fps=24, codec="libx264", - output_params=["-crf", "23", "-pix_fmt", "yuv420p"], + pix_fmt_out="yuv420p", + output_params=["-crf", "23"], ) writer.send(None) diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 750aa14ff..9b1997935 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -242,7 +242,7 @@ def get_current_sync_status(resource, resource_type): def wait_for_resource_to_sync( - resource, resource_type='FOLDER', patterns=None, force_sync=False + resource, resource_type='FOLDER', patterns=None, force_sync=False, check_queued=True ): listen_sync_status_for_item(resource, resource_type) @@ -252,32 +252,48 @@ def wait_for_resource_to_sync( if patterns is None: patterns = get_synced_pattern(resource) + sync_info = [] + synced = False if force_sync: - initial_timeout = get_config('min_timeout') + initial_timeout = get_config('min_sync_timeout') # first try with 5 seconds timeout synced = wait_for( lambda: has_sync_pattern(patterns, resource), initial_timeout, ) if not synced: - # trigger force sync if the current status is OK - status = get_current_sync_status(resource, resource_type) - if status.startswith(SYNC_STATUS['OK']): - print('[WARN] Retrying sync pattern check with force sync') + # do not trigger force sync if the sync is still in progress + if check_queued and SyncConnection.is_sync_in_progress( + get_config('syncConnectionName') + ): + print('[INFO] Sync is in progress. Waiting...') + else: + sync_info.append('Force synced: True') + print('[INFO] Retrying sync pattern check with force sync') SyncConnection.force_sync() - else: - clear_socket_messages(resource) - return - synced = wait_for( - lambda: has_sync_pattern(patterns, resource), - timeout - initial_timeout, - ) - - messages = read_and_update_socket_messages() - messages = filter_messages_for_item(messages, resource) + if not synced: + synced = wait_for( + lambda: has_sync_pattern(patterns, resource), + timeout - initial_timeout, + ) + # clear stored socket messages clear_socket_messages(resource) + sync_info.append('Sync complete (socket): %s' % synced) if synced: + if check_queued: + loaded = wait_for( + lambda: not SyncConnection.is_sync_in_progress( + get_config('syncConnectionName') + ), + get_config('sync_timeout'), + ) + sync_info.append('Sync complete (UI): %s' % loaded) + if not loaded: + raise TimeoutError( + '[ERROR] Sync is still in progress after matching the sync pattern.' + + '\n'.join(sync_info) + ) return elif not force_sync: # if the sync pattern doesn't match then check the last sync status @@ -291,17 +307,21 @@ def wait_for_resource_to_sync( + '. So passing the step.' ) return + print('[ERROR] Sync patterns: %s' % patterns) + print('[ERROR] Sync messages: %s' % read_socket_messages()) raise TimeoutError( - 'Timeout while waiting for sync to complete for ' + str(timeout) + ' seconds' + 'Timeout while waiting for sync to complete for %s seconds.\n' % timeout + + '\n'.join(sync_info) ) -def wait_for_initial_sync_to_complete(path): +def wait_for_initial_sync_to_complete(path, check_queued=True): wait_for_resource_to_sync( path, 'FOLDER', get_initial_sync_patterns(), True, + check_queued, ) diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 442e49348..caa5f8fe7 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -2,7 +2,7 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys -from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchElementException, WebDriverException from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config @@ -13,8 +13,7 @@ class Activity: TAB_CONTAINER = SimpleNamespace(by=None, selector=None) SUBTAB_CONTAINER = SimpleNamespace( - by=By.XPATH, - selector="//page_tab[starts-with(@name, '{tab_name}')]" + by=By.XPATH, selector="//page_tab[starts-with(@name, '{tab_name}')]" ) NOT_SYNCED_TABLE = SimpleNamespace(by=None, selector=None) LOCAL_ACTIVITY_FILTER_BUTTON = SimpleNamespace(by=By.NAME, selector="Filter") @@ -32,7 +31,6 @@ class Activity: NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_STATUS = SimpleNamespace(by=By.NAME, selector=None) - @staticmethod def get_not_synced_file_selector(resource): return { @@ -93,10 +91,20 @@ def is_resource_excluded(filename): @staticmethod def has_sync_status(filename, status): try: - app().find_element(Activity.SYNCED_ACTIVITY_STATUS.by, status) - return True + row = app().find_element(By.NAME, filename) + row_y = row.rect['y'] + status_cells = app().find_elements( + Activity.SYNCED_ACTIVITY_STATUS.by, status + ) + for status_el in status_cells: + if status_el.rect['y'] == row_y: + return True + return False except NoSuchElementException: return False + except WebDriverException as e: + if "NoneType" in str(e): + return False @staticmethod def select_synced_filter(sync_filter): diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index a5990378c..6ae2e1575 100644 --- a/test/gui/pageObjects/SyncConnection.py +++ b/test/gui/pageObjects/SyncConnection.py @@ -1,6 +1,6 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By -from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchElementException, WebDriverException from helpers.ConfigHelper import get_config from helpers.AppHelper import app @@ -22,8 +22,7 @@ class SyncConnection: by=By.NAME, selector="Remove Space" ) PERMISSION_ERROR_LABEL = SimpleNamespace( - by=By.XPATH, - selector="//label[contains(@name, 'permission')]" + by=By.XPATH, selector="//label[contains(@name, 'permission')]" ) @staticmethod @@ -100,6 +99,24 @@ def has_sync_connection(sync_folder): except NoSuchElementException: return False + @staticmethod + def is_sync_in_progress(sync_folder): + connection = SyncConnection.get_current_account_connection() + try: + connection.find_element( + By.NAME, + "{sync_folder},Queued,Local folder: {sync_path}{sync_folder}".format( + sync_folder=sync_folder, + sync_path=get_config('currentUserSyncPath'), + ), + ) + return True + except NoSuchElementException: + return False + except WebDriverException as e: + if "NoneType" in str(e): + return False + @staticmethod def remove_folder_sync_connection(): SyncConnection.perform_action("Remove Space") @@ -122,10 +139,15 @@ def wait_for_error_label(to_exist=True): """Wait for permission error label to appear or disappear""" status = wait_for( - lambda: (bool(app().find_elements( - SyncConnection.PERMISSION_ERROR_LABEL.by, - SyncConnection.PERMISSION_ERROR_LABEL.selector - ))) == to_exist, + lambda: ( + bool( + app().find_elements( + SyncConnection.PERMISSION_ERROR_LABEL.by, + SyncConnection.PERMISSION_ERROR_LABEL.selector, + ) + ) + ) + == to_exist, get_config("max_timeout"), ) if not status: @@ -138,7 +160,6 @@ def get_permission_error_message(): SyncConnection.wait_for_error_label(True) # Wait for label to appear element = app().find_element( SyncConnection.PERMISSION_ERROR_LABEL.by, - SyncConnection.PERMISSION_ERROR_LABEL.selector + SyncConnection.PERMISSION_ERROR_LABEL.selector, ) return str(element.text) - diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index e58917a7b..70d5b1d52 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -4,6 +4,7 @@ from helpers.SetupClientHelper import get_current_user_sync_path from helpers.AppHelper import app +from helpers.ConfigHelper import get_config class SyncConnectionWizard: @@ -12,7 +13,7 @@ class SyncConnectionWizard: ) BACK_BUTTON = SimpleNamespace(by=By.NAME, selector="< Back") NEXT_BUTTON = SimpleNamespace(by=By.NAME, selector="Next >") - SELECTIVE_SYNC_ROOT_FOLDER = SimpleNamespace(by=None, selector=None) + SELECTIVE_SYNC_ROOT_FOLDER = SimpleNamespace(by=By.NAME, selector=None) ADD_SYNC_CONNECTION_BUTTON = SimpleNamespace( by=By.XPATH, selector="//dialog[@name='Add Space']//*[@name='Add Space']" ) @@ -71,12 +72,12 @@ def select_remote_destination_folder(folder): @staticmethod def deselect_all_remote_folders(): - element = app().find_element( - SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.by, - SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.selector, + root = app().find_element( + SyncConnectionWizard.SELECTIVE_SYNC_ROOT_FOLDER.by, + get_config('syncConnectionName'), ) - element.send_keys(Keys.ARROW_DOWN) - element.native_send_keys(Keys.SPACE) # uncheck the root folder + root.native_click() + root.native_send_keys(Keys.SPACE) # uncheck the root folder @staticmethod def sort_by(header_text): @@ -197,11 +198,15 @@ def is_add_sync_folder_button_enabled(): ).enabled @staticmethod - def select_or_unselect_folders_to_sync(folders, select=True): - expected_state = "true" if select else "false" + def get_relative_folder_element(target_folder, parent_row): + possible_els = app().find_elements(By.NAME, target_folder) + for folder in possible_els: + if folder.rect["x"] > parent_row: + return folder - if select: - SyncConnectionWizard.deselect_all_remote_folders() + @staticmethod + def toggle_folder_selection(folders, select=True): + expected_state = "true" if select else "false" for folder_path in folders: parents = folder_path.strip("/").split("/") @@ -209,8 +214,11 @@ def select_or_unselect_folders_to_sync(folders, select=True): parent_element = None parent_position = 0 - for parent in parents: + target_element = None + for idx, parent in enumerate(parents): p_elements = app().find_elements(By.NAME, parent) + next_item = idx + 1 < len(parents) and parents[idx + 1] or target_folder + # select nested folders based on the position of the parent folder for p_element in p_elements: if ( @@ -220,34 +228,40 @@ def select_or_unselect_folders_to_sync(folders, select=True): parent_element = p_element parent_position = p_element.rect["x"] break + parent_element.native_double_click() # expand the folder + target_element = SyncConnectionWizard.get_relative_folder_element( + next_item, parent_position + ) + # retry once if the folder is not expanded - if parent_element.is_selected(): + if not target_element or not target_element.is_displayed(): print('[WARN] Folder was not expanded, retrying with space key') # expand using space key parent_element.native_click() parent_element.native_send_keys(Keys.SPACE) - if parent_element.is_selected(): + # try to get the next target again + target_element = SyncConnectionWizard.get_relative_folder_element( + next_item, parent_position + ) + if not target_element or not target_element.is_displayed(): raise AssertionError(f'Failed to expand folder: {parent}') - folder_element = None - target_folders = app().find_elements(By.NAME, target_folder) - # select the folder that is inside the current parent position - for folder in target_folders: - if folder.rect["x"] > parent_position: - folder_element = folder - break - is_checked = folder_element.get_attribute("checked") + if not target_element: + target_element = SyncConnectionWizard.get_relative_folder_element( + target_folder, parent_position + ) + is_checked = target_element.get_attribute("checked") # return early if the folder is already in the expected state. if is_checked == expected_state: return - folder_element.native_click() - if not folder_element.is_selected(): + target_element.native_click() + if not target_element.is_selected(): raise AssertionError(f"Failed to focus folder: {target_folder}") - folder_element.native_send_keys(Keys.SPACE) # toggle the folder selection + target_element.native_send_keys(Keys.SPACE) # toggle the folder selection - is_checked = folder_element.get_attribute("checked") + is_checked = target_element.get_attribute("checked") if is_checked != expected_state: raise AssertionError( f"Failed to {'select' if select else 'unselect'} folder: {folder_path}" @@ -259,7 +273,7 @@ def confirm_choose_what_to_sync_selection(): @staticmethod def __handle_folder_selection(folders, should_select, new_sync_connection_wizard): - SyncConnectionWizard.select_or_unselect_folders_to_sync(folders, should_select) + SyncConnectionWizard.toggle_folder_selection(folders, should_select) if new_sync_connection_wizard: SyncConnectionWizard.add_sync_connection() diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 18ebb662e..f5d2f6f91 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -74,11 +74,11 @@ def step(context): sync_paths = generate_account_config(users) start_client() # accept certificate for each user - for idx, _ in enumerate(users): + for _ in users: enter_password = EnterPassword() enter_password.accept_certificate() - for idx, _ in enumerate(sync_paths.values()): + for _ in sync_paths.values(): # login from last dialog enter_password = EnterPassword() username = enter_password.get_username() @@ -86,7 +86,7 @@ def step(context): listen_sync_status_for_item(sync_paths[username]) enter_password.login_after_setup(username, password) # wait for files to sync - wait_for_initial_sync_to_complete(sync_paths[username]) + wait_for_initial_sync_to_complete(sync_paths[username], False) Toolbar.wait_toolbar_enabled() @@ -193,11 +193,10 @@ def step(context): @Then('credentials wizard should be visible') def step(context): - with ensure( - 'Credentials wizard is not be visible' - ): + with ensure('Credentials wizard is not be visible'): AccountConnectionWizard.is_credential_window_visible().should.be.true + @When('the user selects download everything option in advanced section') def step(context): AccountConnectionWizard.select_download_everything_option() diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 421ad481b..9c35305ce 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -113,10 +113,10 @@ def step(context, filename): Activity.check_file_exist(filename) -@Then('the file "{filename}" should be blacklisted') -def step(context, filename): - with ensure('File is Blacklisted'): - Activity.is_resource_blacklisted(filename).should.be.true +@Then('the {resource_type:ResourceType} "{resourceName}" should be blacklisted') +def step(context, resource_type, resourceName): + with ensure(f'{resource_type.capitalize()} is blacklisted'): + Activity.is_resource_blacklisted(resourceName).should.be.true @Then('the file "|any|" should be ignored') @@ -144,11 +144,12 @@ def step(context): Toolbar.has_tab(tab_name).should.be.true -@When('the user selects the following folders to sync:') +@When('the user selects only the following folders to sync:') def step(context): folders = [] for row in context.table: folders.append(row[0]) + SyncConnectionWizard.deselect_all_remote_folders() SyncConnectionWizard.select_folders_to_sync( folders, new_sync_connection_wizard=True ) @@ -341,7 +342,9 @@ def step(context): # wait for error message to disappear SyncConnection.wait_for_error_label(False) - with ensure(f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"'): + with ensure( + f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"' + ): expected_error_message.should.equal(actual_error_message)