From 530b3463b05f8ad9c857b14887c8f2e7afd4c1fe Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Tue, 21 Apr 2026 18:42:48 +0545 Subject: [PATCH 1/5] test(gui): enable manual folder sync scenario Signed-off-by: Saw-jan --- test/gui/environment.py | 1 + test/gui/features/add-account/account.feature | 2 +- test/gui/helpers/SetupClientHelper.py | 1 - test/gui/helpers/TableParser.py | 102 +++++++++ .../pageObjects/AccountConnectionWizard.py | 13 +- test/gui/pageObjects/SyncConnectionWizard.py | 205 ++++++------------ test/gui/step_types/types.py | 10 + test/gui/steps/account_context.py | 12 +- test/gui/steps/file_context.py | 111 +++++----- test/gui/steps/server_context.py | 7 +- test/gui/steps/sync_context.py | 12 +- 11 files changed, 259 insertions(+), 217 deletions(-) create mode 100644 test/gui/helpers/TableParser.py create mode 100644 test/gui/step_types/types.py diff --git a/test/gui/environment.py b/test/gui/environment.py index dcf2d62c55..078bc37e6c 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -7,6 +7,7 @@ from helpers.ConfigHelper import set_config, get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths from helpers.SetupClientHelper import app +from step_types.types import * # register all step types def before_feature(context, feature): diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index b42556844b..5064ceee8e 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -50,7 +50,7 @@ Feature: adding accounts | password | 1234 | Then "Alice Hansen" account should be opened - @smoke @skip + @smoke Scenario: Add space manually from sync connection window Given user "Alice" has created folder "simple-folder" in the server And the user has started the client diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index ec8863708b..d8e935969d 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -43,7 +43,6 @@ def get_client_details(table): 'user': '', 'password': '', 'sync_folder': '', - 'oauth': False, } for key, value in table.items(): value = substitute_inline_codes(value) diff --git a/test/gui/helpers/TableParser.py b/test/gui/helpers/TableParser.py new file mode 100644 index 0000000000..cf262f1499 --- /dev/null +++ b/test/gui/helpers/TableParser.py @@ -0,0 +1,102 @@ +from behave.model import Table + + +def table_raw(table: Table): + """ + Args: + table (Table): Behave Table object. + Returns: + list: List of lists (including header row) - each row is a list of cells. + + Example: + | header1 | header2 | header3 | + | value1 | value2 | value3 | + Output: + [ + ['header1', 'header2', 'header3'], + ['value1', 'value2', 'value3'], + ] + """ + data_table = [table.headings] + data_table.extend(table_rows(table)) + return data_table + + +def table_rows(table: Table): + """ + Args: + table (Table): Behave Table object. + Returns: + list: List of lists (excluding header row) - each row is a list of cells. + + Example: + | header1 | header2 | header3 | + | value1 | value2 | value3 | + Output: + [ + ['value1', 'value2', 'value3'], + ] + """ + data_table = [] + for row in table: + data_table.append(row.cells) + return data_table + + +def table_rows_hash(table: Table): + """ + Args: + table (Table): Behave Table object. Table MUST have exactly 2 columns. + Returns: + dict: Dictionary where keys are from the first column and values are from the second column. + Raises: + ValueError: If the table does not have exactly 2 columns. + + Example: + | key1 | value1 | + | key2 | value2 | + | key3 | value3 | + Output: + { + 'key1': 'value1', + 'key2': 'value2', + 'key3': 'value3', + } + """ + if len(table.headings) != 2: + raise ValueError( + "table_rows_hash() can only be called on a data table where all rows have exactly two columns." + ) + + data_table = { + table.headings[0]: table.headings[1], + } + for row in table: + data_table[row[0]] = row[1] + return data_table + + +def table_hashes(table: Table): + """ + Args: + table (Table): Behave Table object. + Returns: + list: List of dictionaries, where each dictionary represents a row with keys from the header and values from the corresponding cells. + + Example: + | key1 | key2 | key3 | + | value1 | value2 | value3 | + | value4 | value5 | value6 | + Output: + [ + {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}, + {'key1': 'value4', 'key2': 'value5', 'key3': 'value6'}, + ] + """ + data_table = [] + for row in table: + row_dict = {} + for idx, heading in enumerate(table.headings): + row_dict[heading] = row.cells[idx] + data_table.append(row_dict) + return data_table diff --git a/test/gui/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py index e52d169639..5ee90cb6a7 100644 --- a/test/gui/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -37,7 +37,9 @@ class AccountConnectionWizard: by=By.NAME, selector="Copy URL", ) - CONF_SYNC_MANUALLY_RADIO_BUTTON = SimpleNamespace(by=None, selector=None) + CONF_SYNC_MANUALLY_RADIO_BUTTON = SimpleNamespace( + by=By.NAME, selector="Configure synchronization manually" + ) ADVANCED_CONFIGURATION_CHECKBOX = SimpleNamespace( by=By.NAME, selector="Advanced configuration", @@ -164,11 +166,10 @@ def add_account_information(account_details): @staticmethod def select_manual_sync_folder_option(): - squish.clickButton( - squish.waitForObject( - AccountConnectionWizard.CONF_SYNC_MANUALLY_RADIO_BUTTON - ) - ) + app().find_element( + AccountConnectionWizard.CONF_SYNC_MANUALLY_RADIO_BUTTON.by, + AccountConnectionWizard.CONF_SYNC_MANUALLY_RADIO_BUTTON.selector, + ).click() @staticmethod def select_download_everything_option(): diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index f876342d36..f0c9715ea1 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -1,138 +1,46 @@ -from os import path -import names -import squish +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By +import time -from helpers.SetupClientHelper import ( - get_current_user_sync_path, - set_current_user_sync_path, -) -from helpers.ConfigHelper import get_config +from helpers.SetupClientHelper import get_current_user_sync_path +from helpers.SetupClientHelper import app class SyncConnectionWizard: - CHOOSE_LOCAL_SYNC_FOLDER = { - "buddy": names.add_Space_label_QLabel, - "name": "localFolderLineEdit", - "type": "QLineEdit", - "visible": 1 - } - BACK_BUTTON = { - "window": names.stackedWidget_Add_Space_QGroupBox, - "name": "__qt__passive_wizardbutton0", - "type": "QPushButton", - "visible": 1, - } - NEXT_BUTTON = { - "window": names.stackedWidget_Add_Space_QGroupBox, - "name": "__qt__passive_wizardbutton1", - "type": "QPushButton", - "visible": 1, - } - SELECTIVE_SYNC_ROOT_FOLDER = { - "column": 0, - "container": names.add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget, - "text": "Personal", - "type": "QModelIndex", - } - ADD_SPACE_FOLDER_TREE = { - "column": 0, - "container": names.deselect_remote_folders_you_do_not_wish_to_synchronize_OpenCloud_QModelIndex, - "type": "QModelIndex", - } - ADD_SYNC_CONNECTION_BUTTON = { - "name": "qt_wizard_finish", - "type": "QPushButton", - "visible": 1, - "window": names.stackedWidget_Add_Space_QGroupBox, - } - REMOTE_FOLDER_TREE = { - "container": names.add_Folder_Sync_Connection_groupBox_QGroupBox, - "name": "folderTreeWidget", - "type": "QTreeWidget", - "visible": 1, - } - SELECTIVE_SYNC_TREE_HEADER = { - "container": names.add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget, - "orientation": 1, - "type": "QHeaderView", - "unnamed": 1, - "visible": 1, - } - CANCEL_FOLDER_SYNC_CONNECTION_WIZARD = { - "window": names.stackedWidget_Add_Space_QGroupBox, - "name": "qt_wizard_cancel", - "type": "QPushButton", - "visible": 1, - } - SPACE_NAME_SELECTOR = { - "container": names.quickWidget_scrollView_ScrollView, - "type": "Label", - "visible": True, - } - CREATE_REMOTE_FOLDER_BUTTON = { - "container": names.add_Folder_Sync_Connection_groupBox_QGroupBox, - "name": "addFolderButton", - "type": "QPushButton", - "visible": 1, - } - CREATE_REMOTE_FOLDER_INPUT = { - "buddy": names.create_Remote_Folder_Enter_the_name_of_the_new_folder_to_be_created_below_QLabel, - "type": "QLineEdit", - "unnamed": 1, - "visible": 1, - } - CREATE_REMOTE_FOLDER_CONFIRM_BUTTON = { - "text": "OK", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.create_Remote_Folder_QInputDialog, - } - REFRESH_BUTTON = { - "container": names.add_Folder_Sync_Connection_groupBox_QGroupBox, - "name": "refreshButton", - "type": "QPushButton", - "visible": 1, - } - REMOTE_FOLDER_SELECTION_INPUT = { - "name": "folderEntry", - "type": "QLineEdit", - "visible": 1, - "window": names.add_Folder_Sync_Connection_OCC_FolderWizard, - } - ADD_FOLDER_SYNC_BUTTON = { - "checkable": False, - "container": names.stackedWidget_quickWidget_OCC_QmlUtils_OCQuickWidget, - "id": "addSyncButton", - "type": "Button", - "unnamed": 1, - "visible": True, - } - WARN_LABEL = { - "window": names.add_Folder_Sync_Connection_OCC_FolderWizard, - "name": "warnLabel", - "type": "QLabel", - "visible": 1, - } - - CHOOSE_WHAT_TO_SYNC_FOLDER_TREE = { - "column": 0, - "container": names.deselect_remote_folders_you_do_not_wish_to_synchronize_Personal_QModelIndex, - "type": "QModelIndex", - } + CHOOSE_LOCAL_SYNC_FOLDER = SimpleNamespace( + by=By.ACCESSIBILITY_ID, selector="localFolderLineEdit" + ) + 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) + ADD_SPACE_FOLDER_TREE = SimpleNamespace(by=None, selector=None) + ADD_SYNC_CONNECTION_BUTTON = SimpleNamespace( + by=By.XPATH, selector="//dialog[@name='Add Space']//*[@name='Add Space']" + ) + REMOTE_FOLDER_TREE = SimpleNamespace(by=None, selector=None) + SELECTIVE_SYNC_TREE_HEADER = SimpleNamespace(by=None, selector=None) + CANCEL_FOLDER_SYNC_CONNECTION_WIZARD = SimpleNamespace(by=None, selector=None) + SPACES_LIST = SimpleNamespace(by=By.NAME, selector="Spaces list") + SPACE_NAME_SELECTOR = SimpleNamespace(by=By.NAME, selector="{space_name},") + CREATE_REMOTE_FOLDER_BUTTON = SimpleNamespace(by=None, selector=None) + CREATE_REMOTE_FOLDER_INPUT = SimpleNamespace(by=None, selector=None) + CREATE_REMOTE_FOLDER_CONFIRM_BUTTON = SimpleNamespace(by=None, selector=None) + REFRESH_BUTTON = SimpleNamespace(by=None, selector=None) + REMOTE_FOLDER_SELECTION_INPUT = SimpleNamespace(by=None, selector=None) + ADD_FOLDER_SYNC_BUTTON = SimpleNamespace(by=None, selector=None) + WARN_LABEL = SimpleNamespace(by=None, selector=None) + CHOOSE_WHAT_TO_SYNC_FOLDER_TREE = SimpleNamespace(by=None, selector=None) @staticmethod def set_sync_path_oc(sync_path): if not sync_path: sync_path = get_current_user_sync_path() - squish.type( - squish.waitForObject(SyncConnectionWizard.CHOOSE_LOCAL_SYNC_FOLDER), - "", - ) - squish.type( - SyncConnectionWizard.CHOOSE_LOCAL_SYNC_FOLDER, - sync_path, + sync_path_input = app().find_element( + SyncConnectionWizard.CHOOSE_LOCAL_SYNC_FOLDER.by, + SyncConnectionWizard.CHOOSE_LOCAL_SYNC_FOLDER.selector, ) + sync_path_input.clear() + sync_path_input.send_keys(sync_path) SyncConnectionWizard.next_step() @staticmethod @@ -141,7 +49,13 @@ def set_sync_path(sync_path=""): @staticmethod def next_step(): - squish.clickButton(squish.waitForObject(SyncConnectionWizard.NEXT_BUTTON)) + next_button = app().find_element( + SyncConnectionWizard.NEXT_BUTTON.by, + SyncConnectionWizard.NEXT_BUTTON.selector, + ) + if not next_button.is_enabled(): + raise AssertionError("Next button is not enabled") + next_button.click() @staticmethod def back(): @@ -166,7 +80,6 @@ def deselect_all_remote_folders(): squish.Qt.LeftButton, ) - @staticmethod def sort_by(header_text): squish.mouseClick( @@ -182,9 +95,10 @@ def sort_by(header_text): @staticmethod def add_sync_connection(): - squish.clickButton( - squish.waitForObject(SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON) - ) + app().find_element( + SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.by, + SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.selector, + ).click() @staticmethod def get_item_name_from_row(row_index): @@ -212,9 +126,16 @@ def cancel_folder_sync_connection_wizard(): @staticmethod def select_space(space_name): - selector = SyncConnectionWizard.SPACE_NAME_SELECTOR.copy() - selector["text"] = space_name - squish.mouseClick(squish.waitForObject(selector)) + spaces_list = app().find_element( + SyncConnectionWizard.SPACES_LIST.by, + SyncConnectionWizard.SPACES_LIST.selector, + ) + spaces_list.find_element( + SyncConnectionWizard.SPACE_NAME_SELECTOR.by, + SyncConnectionWizard.SPACE_NAME_SELECTOR.selector.format( + space_name=space_name + ), + ).click() @staticmethod def sync_space(space_name): @@ -271,11 +192,15 @@ def is_add_sync_folder_button_enabled(): ).enabled @staticmethod - def select_or_unselect_folders_to_sync(folders, should_select=True, new_sync_connection_wizard=False): + def select_or_unselect_folders_to_sync( + folders, should_select=True, new_sync_connection_wizard=False + ): if should_select: # First deselect all SyncConnectionWizard.deselect_all_remote_folders() - folder_tree_locator = SyncConnectionWizard.get_folder_tree_locator(new_sync_connection_wizard) + folder_tree_locator = SyncConnectionWizard.get_folder_tree_locator( + new_sync_connection_wizard + ) for folder in folders: folder_levels = folder.strip("/").split("/") parent_selector = None @@ -316,7 +241,7 @@ def __handle_folder_selection(folders, should_select, new_sync_connection_wizard SyncConnectionWizard.select_or_unselect_folders_to_sync( folders, should_select=should_select, - new_sync_connection_wizard=new_sync_connection_wizard + new_sync_connection_wizard=new_sync_connection_wizard, ) if new_sync_connection_wizard: @@ -327,13 +252,17 @@ def __handle_folder_selection(folders, should_select, new_sync_connection_wizard @staticmethod def unselect_folders_to_sync(folders, new_sync_connection_wizard=False): SyncConnectionWizard.__handle_folder_selection( - folders, should_select=False, new_sync_connection_wizard=new_sync_connection_wizard + folders, + should_select=False, + new_sync_connection_wizard=new_sync_connection_wizard, ) @staticmethod def select_folders_to_sync(folders, new_sync_connection_wizard=False): SyncConnectionWizard.__handle_folder_selection( - folders, should_select=True, new_sync_connection_wizard=new_sync_connection_wizard + folders, + should_select=True, + new_sync_connection_wizard=new_sync_connection_wizard, ) @staticmethod diff --git a/test/gui/step_types/types.py b/test/gui/step_types/types.py new file mode 100644 index 0000000000..db8cd0c503 --- /dev/null +++ b/test/gui/step_types/types.py @@ -0,0 +1,10 @@ +from behave import register_type +from parse import with_pattern + + +@with_pattern(r"file|folder") +def resource_type(text): + return text + + +register_type(ResourceType=resource_type) diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index ae127d7e7a..97eaa4eb01 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -14,6 +14,7 @@ ) from helpers.SyncHelper import wait_for_initial_sync_to_complete, listen_sync_status_for_item from helpers.UserHelper import get_displayname_for_user, get_password_for_user +from helpers.TableParser import table_rows_hash @Given('the user has started the client') @@ -96,12 +97,8 @@ def step(context): @When('the user adds the following account:') def step(context): - data_table = {} - for row in context.table: - if row.headings[0] not in data_table: - data_table[row.headings[0]] = row.headings[1] - data_table[row[0]] = row[1] - account_details = get_client_details(data_table) + data = table_rows_hash(context.table) + account_details = get_client_details(data) AccountConnectionWizard.add_account(account_details) # # wait for files to sync wait_for_initial_sync_to_complete(get_resource_path('/', account_details['user'])) @@ -109,7 +106,8 @@ def step(context): @Given('the user has entered the following account information:') def step(context): - account_details = get_client_details(context) + data = table_rows_hash(context.table) + account_details = get_client_details(data) AccountConnectionWizard.add_account_information(account_details) diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index a0718837df..5b23c952dd 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -1,22 +1,19 @@ -# # -*- coding: utf-8 -*- import os -# import re -# import builtins +import re +import builtins import shutil -# import zipfile -# from os.path import isfile, join, isdir -import parse -from behave import when as When, register_type - -from helpers.SetupClientHelper import ( - get_resource_path, - # get_temp_resource_path -) +import zipfile +from os.path import isfile, join, isdir +from behave import when as When, then as Then +from sure import ensure + +from helpers.SetupClientHelper import get_resource_path, get_temp_resource_path from helpers.SyncHelper import ( - wait_for_client_to_be_ready, - # listen_sync_status_for_item + wait_for_client_to_be_ready, + listen_sync_status_for_item, + wait_for, ) -# from helpers.ConfigHelper import get_config, is_windows +from helpers.ConfigHelper import get_config, is_windows from helpers.FilesHelper import ( # build_conflicted_regex, sanitize_path, @@ -30,25 +27,19 @@ # get_file_for_upload, ) -@parse.with_pattern(r"file|folder") -def parse_resource_type(text): - return text - -register_type(ResourceType=parse_resource_type) - -# def folder_exists(folder_path, timeout=1000): -# return squish.waitFor( -# lambda: isdir(sanitize_path(folder_path)), -# timeout, -# ) +def folder_exists(folder_path, timeout=1000): + return wait_for( + lambda: isdir(sanitize_path(folder_path)), + timeout, + ) -# def file_exists(file_path, timeout=1000): -# return squish.waitFor( -# lambda: isfile(sanitize_path(file_path)), -# timeout, -# ) +def file_exists(file_path, timeout=1000): + return wait_for( + lambda: isfile(sanitize_path(file_path)), + timeout, + ) # To create folders in a temporary directory, we set is_temp_folder True @@ -209,30 +200,42 @@ def deleteResource(resource, resource_type): # ) -# @Then( -# r'^the (file|folder) "([^"]*)" (should|should not) exist on the file system$', -# regexp=True, -# ) -# def step(context, resource_type, resource, should_or_should_not): -# resource_path = get_resource_path(resource) -# resource_exists = False -# if resource_type == 'file': -# if should_or_should_not == 'should': -# resource_exists = file_exists( -# resource_path, get_config('maxSyncTimeout') * 1000 -# ) -# else: -# if should_or_should_not == 'should': -# resource_exists = folder_exists( -# resource_path, get_config('maxSyncTimeout') * 1000 -# ) +@Then('the {resource_type:ResourceType} "{resource}" should exist on the file system') +def step(context, resource_type, resource): + resource_path = get_resource_path(resource) + resource_exists = False + timeout = get_config('maxSyncTimeout') * 1000 + if resource_type == 'file': + resource_exists = file_exists(resource_path, timeout) + else: + resource_exists = folder_exists(resource_path, timeout) -# expected = should_or_should_not == 'should' -# test.compare( -# expected, -# resource_exists, -# f'{resource_type.capitalize()} "{resource}" {"exists" if resource_exists else "does not exist"} on the system', -# ) + with ensure( + '{0} "{1}" should exist, but it does not', + resource_type.capitalize(), + resource, + ): + resource_exists.should.be.true + + +@Then( + 'the {resource_type:ResourceType} "{resource}" should not exist on the file system' +) +def step(context, resource_type, resource): + resource_path = get_resource_path(resource) + resource_exists = False + timeout = get_config('maxSyncTimeout') * 1000 + if resource_type == 'file': + resource_exists = file_exists(resource_path, timeout) + else: + resource_exists = folder_exists(resource_path, timeout) + + with ensure( + '{0} "{1}" should not exist, but it does', + resource_type.capitalize(), + resource, + ): + resource_exists.should.be.false # @Given('the user has changed the content of local file "|any|" to:') diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index 94b511b471..b31232253e 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -1,13 +1,8 @@ -from behave import given as Given, then as Then, register_type +from behave import given as Given, then as Then from sure import ensure import parse from helpers.api import provisioning, webdav_helper as webdav -@parse.with_pattern(r"file|folder") -def parse_resource_type(text): - return text - -register_type(ResourceType=parse_resource_type) @Given('user "{user}" has been created in the server with default attributes') def step(context, user): diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 5873a1d275..f49c8dad6b 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -1,8 +1,11 @@ from behave import when as When, then as Then -# from pageObjects.SyncConnectionWizard import SyncConnectionWizard + +from pageObjects.SyncConnectionWizard import SyncConnectionWizard + # from pageObjects.SyncConnection import SyncConnection from pageObjects.Toolbar import Toolbar from pageObjects.Activity import Activity + # from pageObjects.Settings import Settings # from helpers.ConfigHelper import get_config, is_windows, set_config @@ -16,6 +19,7 @@ substitute_inline_codes, get_resource_path, ) + # from helpers.FilesHelper import convert_path_separators_for_os @@ -170,9 +174,9 @@ def step(context, tab_name): # set_current_user_sync_path(sync_path) -# @When('the user syncs the "|any|" space') -# def step(context, space_name): -# SyncConnectionWizard.sync_space(space_name) +@When('the user syncs the "{space_name}" space') +def step(context, space_name): + SyncConnectionWizard.sync_space(space_name) # @Then('the settings tab should have the following options in the general section:') From ac176bfb5028cf34bd5764cf6f8f12a906fac358 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 22 Apr 2026 13:34:36 +0545 Subject: [PATCH 2/5] test(gui): enable account re-add scenario Signed-off-by: Saw-jan --- test/gui/features/add-account/account.feature | 2 +- test/gui/pageObjects/AccountSetting.py | 93 ++++++------------- test/gui/pageObjects/Toolbar.py | 7 +- test/gui/steps/account_context.py | 11 ++- 4 files changed, 43 insertions(+), 70 deletions(-) diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 5064ceee8e..93ae5b5365 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -76,7 +76,7 @@ Feature: adding accounts When the user selects download everything option in advanced section Then the button to open sync connection wizard should be disabled - @smoke @skip + @smoke Scenario: Re-add an account Given user "Alice" has created folder "large-folder" in the server And user "Alice" has uploaded file with content "test content" to "testFile.txt" in the server diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 1c197ec11c..3c91e699d6 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -1,75 +1,42 @@ -import names -import squish - -from helpers.UserHelper import get_displayname_for_user -from helpers.SetupClientHelper import substitute_inline_codes +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By from pageObjects.Toolbar import Toolbar +from helpers.UserHelper import get_displayname_for_user +from helpers.SetupClientHelper import app, substitute_inline_codes +from helpers.SyncHelper import wait_for class AccountSetting: - MANAGE_ACCOUNT_BUTTON = { - "container": names.stackedWidget_quickWidget_OCC_QmlUtils_OCQuickWidget, - "id": "manageAccountButton", - "text": "Manage Account", - "type": "Button", - "visible": 1, - } - ACCOUNT_MENU = { - "checkable": False, - "container": names.quickWidget_Overlay, - "text": "", - "enabled": True, - "type": "MenuItem", - "unnamed": 1, - "visible": True - } - CONFIRM_REMOVE_CONNECTION_BUTTON = { - "container": names.settings_dialogStack_QStackedWidget, - "text": "Remove connection", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - } - ACCOUNT_CONNECTION_LABEL = { - "container": names.stackedWidget_quickWidget_OCC_QmlUtils_OCQuickWidget, - "type": "Label", - "visible": 1 - } - LOG_BROWSER_WINDOW = { - "name": "OCC__LogBrowser", - "type": "OCC::LogBrowser", - "visible": 1, - } - ACCOUNT_LOADING = { - "window": names.settings_OCC_SettingsDialog, - "name": "loadingPage", - "type": "QWidget", - "visible": 0, - } - DIALOG_STACK = { - "name": "dialogStack", - "type": "QStackedWidget", - "visible": 1, - "window": names.settings_OCC_SettingsDialog, - } - CONFIRMATION_YES_BUTTON = {"text": "Yes", "type": "QPushButton", "visible": 1} + MANAGE_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Manage Account") + ACCOUNT_MENU = SimpleNamespace(by=By.NAME, selector="{menu_item}") + CONFIRM_REMOVE_CONNECTION_BUTTON = SimpleNamespace( + by=By.NAME, selector="Remove connection" + ) + ACCOUNT_CONNECTION_LABEL = SimpleNamespace(by=None, selector=None) + LOG_BROWSER_WINDOW = SimpleNamespace(by=None, selector=None) + ACCOUNT_LOADING = SimpleNamespace(by=None, selector=None) + DIALOG_STACK = SimpleNamespace(by=None, selector=None) + CONFIRMATION_YES_BUTTON = SimpleNamespace(by=None, selector=None) @staticmethod def account_action(action): - squish.mouseClick(squish.waitForObject(AccountSetting.MANAGE_ACCOUNT_BUTTON)) - selector = AccountSetting.ACCOUNT_MENU.copy() - selector['text'] = action - squish.mouseClick( - squish.waitForObject(selector) - ) + app().find_element( + AccountSetting.MANAGE_ACCOUNT_BUTTON.by, + AccountSetting.MANAGE_ACCOUNT_BUTTON.selector, + ).click() + app().find_element( + AccountSetting.ACCOUNT_MENU.by, + AccountSetting.ACCOUNT_MENU.selector.format(menu_item=action), + ).click() @staticmethod def remove_account_connection(): AccountSetting.account_action("Remove") - squish.clickButton( - squish.waitForObject(AccountSetting.CONFIRM_REMOVE_CONNECTION_BUTTON) - ) + app().find_element( + AccountSetting.CONFIRM_REMOVE_CONNECTION_BUTTON.by, + AccountSetting.CONFIRM_REMOVE_CONNECTION_BUTTON.selector, + ).click() @staticmethod def logout(): @@ -172,11 +139,11 @@ def wait_until_account_is_removed(username, timeout=10000): displayname = get_displayname_for_user(username) displayname = substitute_inline_codes(displayname) - def account_gone(): - account, _ = Toolbar.get_account(displayname) + def account_removed(): + account = Toolbar.get_account(displayname) return account is None - result = squish.waitFor(account_gone, timeout) + result = wait_for(account_removed, timeout) if not result: raise TimeoutError( diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 0142bec5ef..be6521e435 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -38,8 +38,7 @@ def has_item(item_name, timeout=get_config("minSyncTimeout") * 1000): @staticmethod def open_activity(): app().find_element( - Toolbar.ACTIVITY_BUTTON.by, - Toolbar.ACTIVITY_BUTTON.selector + Toolbar.ACTIVITY_BUTTON.by, Toolbar.ACTIVITY_BUTTON.selector ).click() @staticmethod @@ -51,8 +50,8 @@ def open_new_account_setup(): @staticmethod def open_account(displayname): - _, selector = Toolbar.get_account(displayname) - squish.mouseClick(squish.waitForObject(selector)) + account_tab = Toolbar.get_account(displayname) + account_tab.click() @staticmethod def get_displayed_account_text(displayname, host): diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 97eaa4eb01..48d4349f53 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -1,7 +1,10 @@ +import shutil +import os from behave import given as Given, when as When, then as Then from sure import expect from pageObjects.AccountConnectionWizard import AccountConnectionWizard +from pageObjects.AccountSetting import AccountSetting from pageObjects.Toolbar import Toolbar from pageObjects.EnterPassword import EnterPassword from helpers.SetupClientHelper import ( @@ -12,8 +15,12 @@ generate_account_config, get_resource_path, ) -from helpers.SyncHelper import wait_for_initial_sync_to_complete, listen_sync_status_for_item +from helpers.SyncHelper import ( + wait_for_initial_sync_to_complete, + listen_sync_status_for_item, +) from helpers.UserHelper import get_displayname_for_user, get_password_for_user +from helpers.ConfigHelper import get_config from helpers.TableParser import table_rows_hash @@ -277,7 +284,7 @@ def step(context, warn_message): ) -@Given('the user has removed the connection for user "|any|"') +@Given('the user has removed the connection for user "{username}"') def step(context, username): AccountSetting.remove_connection_for_user(username) AccountSetting.wait_until_account_is_removed(username) From 85a24d1973f70ab287b4067330d1a1ba5af5ab27 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 22 Apr 2026 14:13:33 +0545 Subject: [PATCH 3/5] test(gui): skip manual sync scenario Signed-off-by: Saw-jan --- test/gui/features/add-account/account.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 93ae5b5365..a941f821b6 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -50,7 +50,7 @@ Feature: adding accounts | password | 1234 | Then "Alice Hansen" account should be opened - @smoke + @smoke @skip Scenario: Add space manually from sync connection window Given user "Alice" has created folder "simple-folder" in the server And the user has started the client From cbfda2e3738b4e622777706ac57f47de866eca94 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 22 Apr 2026 16:18:34 +0545 Subject: [PATCH 4/5] test(gui): check space selection Signed-off-by: Saw-jan --- test/gui/pageObjects/SyncConnectionWizard.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index f0c9715ea1..8bab42a240 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -130,12 +130,15 @@ def select_space(space_name): SyncConnectionWizard.SPACES_LIST.by, SyncConnectionWizard.SPACES_LIST.selector, ) - spaces_list.find_element( + space_item = spaces_list.find_element( SyncConnectionWizard.SPACE_NAME_SELECTOR.by, SyncConnectionWizard.SPACE_NAME_SELECTOR.selector.format( space_name=space_name ), - ).click() + ) + space_item.click() + if space_item.get_attribute("selected") != "true": + raise AssertionError("Failed to select the space: " + space_name) @staticmethod def sync_space(space_name): From 4134b67266df386d911a1e324de9cbcc3a2f7ce8 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 22 Apr 2026 16:18:53 +0545 Subject: [PATCH 5/5] test(gui): remove obsolete step files Signed-off-by: Saw-jan --- test/gui/shared/steps/account_context.py | 290 ----------------------- test/gui/shared/steps/server_context.py | 139 ----------- 2 files changed, 429 deletions(-) delete mode 100644 test/gui/shared/steps/account_context.py delete mode 100644 test/gui/shared/steps/server_context.py diff --git a/test/gui/shared/steps/account_context.py b/test/gui/shared/steps/account_context.py deleted file mode 100644 index 1e7bffcec4..0000000000 --- a/test/gui/shared/steps/account_context.py +++ /dev/null @@ -1,290 +0,0 @@ -import shutil -import os - -from pageObjects.AccountConnectionWizard import AccountConnectionWizard -from pageObjects.SyncConnectionWizard import SyncConnectionWizard -from pageObjects.EnterPassword import EnterPassword -from pageObjects.Toolbar import Toolbar -from pageObjects.AccountSetting import AccountSetting - -from helpers.SetupClientHelper import ( - setup_client, - start_client, - substitute_inline_codes, - get_client_details, - generate_account_config, - get_resource_path, -) -from helpers.UserHelper import get_displayname_for_user, get_password_for_user -from helpers.SyncHelper import ( - wait_for_initial_sync_to_complete, - listen_sync_status_for_item, -) -from helpers.ConfigHelper import get_config, set_config, is_linux -from helpers.FilesHelper import convert_path_separators_for_os - - -@When('the user adds the following user credentials:') -def step(context): - account_details = get_client_details(context) - set_config('syncConnectionName', get_displayname_for_user(account_details['user'])) - AccountConnectionWizard.add_user_credentials( - account_details['user'], account_details['password'] - ) - - -@Then('the account with displayname "|any|" should be displayed') -def step(context, displayname): - displayname = substitute_inline_codes(displayname) - Toolbar.account_exists(displayname) - - -@Then('the account with displayname "|any|" should not be displayed') -def step(context, displayname): - displayname = substitute_inline_codes(displayname) - timeout = get_config('lowestSyncTimeout') * 1000 - - test.compare( - False, - Toolbar.has_item(displayname, timeout), - f"Expected account '{displayname}' to be removed", - ) - - -@Given('user "|any|" has set up a client with default settings') -def step(context, username): - password = get_password_for_user(username) - setup_client(username) - enter_password = EnterPassword() - enter_password.accept_certificate() - - enter_password.login_after_setup(username, password) - - # wait for files to sync - wait_for_initial_sync_to_complete(get_resource_path('/', username)) - - -@Given('the user has set up the following accounts with default settings:') -def step(context): - users = [] - for row in context.table: - users.append(row[0]) - sync_paths = generate_account_config(users) - start_client() - # accept certificate for each user - for idx, _ in enumerate(users): - enter_password = EnterPassword(len(users) - idx) - enter_password.accept_certificate() - - for idx, _ in enumerate(sync_paths.values()): - # login from last dialog - account_idx = len(sync_paths) - idx - enter_password = EnterPassword(account_idx) - username = enter_password.get_username() - password = get_password_for_user(username) - 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]) - - -@Given('the user has started the client') -def step(context): - start_client() - - -@When('the user starts the client') -def step(context): - start_client() - - -@When('the user opens the add-account dialog') -def step(context): - Toolbar.open_new_account_setup() - - -@When('the user adds the following account:') -def step(context): - account_details = get_client_details(context) - AccountConnectionWizard.add_account(account_details) - # wait for files to sync - wait_for_initial_sync_to_complete(get_resource_path('/', account_details['user'])) - - -@Given('the user has entered the following account information:') -def step(context): - account_details = get_client_details(context) - AccountConnectionWizard.add_account_information(account_details) - - -@When('the user "|any|" logs out using the client-UI') -def step(context, _): - AccountSetting.logout() - - -@Then('user "|any|" should be signed out') -def step(context, username): - test.compare( - AccountSetting.is_user_signed_out(), - True, - f'User "{username}" is signed out', - ) - - -@Given('user "|any|" has logged out from the client-UI') -def step(context, username): - AccountSetting.logout() - if not AccountSetting.is_user_signed_out(): - raise LookupError(f'Failed to logout user {username}') - - -@When('user "|any|" logs in using the client-UI') -def step(context, username): - AccountSetting.login() - password = get_password_for_user(username) - enter_password = EnterPassword() - enter_password.relogin(username, password) - - # wait for files to sync - wait_for_initial_sync_to_complete(get_resource_path('/', username)) - - -@When('user "|any|" opens login dialog') -def step(context, _): - AccountSetting.login() - - -@Then('user "|any|" should be connected to the server') -def step(context, _): - AccountSetting.wait_until_account_is_connected() - AccountSetting.wait_until_sync_folder_is_configured() - - -@When('the user removes the connection for user "|any|"') -def step(context, username): - AccountSetting.remove_connection_for_user(username) - - -@Then('connection wizard should be visible') -def step(context): - test.compare( - AccountConnectionWizard.is_new_connection_window_visible(), - True, - 'Connection window is visible', - ) - - -@When('the user accepts the certificate') -def step(context): - AccountConnectionWizard.accept_certificate() - - -@When('the user adds the server "|any|"') -def step(context, server): - server_url = substitute_inline_codes(server) - AccountConnectionWizard.add_server(server_url) - - -@When('the user selects manual sync folder option in advanced section') -def step(context): - AccountConnectionWizard.select_manual_sync_folder_option() - AccountConnectionWizard.next_step() - - -@Then('credentials wizard should be visible') -def step(context): - test.compare( - AccountConnectionWizard.is_credential_window_visible(), - True, - 'Credentials wizard is visible', - ) - - -@When('the user selects download everything option in advanced section') -def step(context): - AccountConnectionWizard.select_download_everything_option() - AccountConnectionWizard.next_step() - - -@When('the user opens the advanced configuration') -def step(context): - AccountConnectionWizard.select_advanced_config() - - -@Then('the user should be able to choose the local download directory') -def step(context): - test.compare(True, AccountConnectionWizard.can_change_local_sync_dir()) - - -@Then('the download everything option should be selected by default for Linux') -def step(context): - if is_linux(): - test.compare( - True, - AccountConnectionWizard.is_sync_everything_option_checked(), - 'Sync everything option is checked', - ) - -@When(r'^the user presses the "([^"]*)" key(?:s)?', regexp=True) -def step(context, key): - AccountSetting.press_key(key) - - -@Then('the log dialog should be opened') -def step(context): - test.compare(True, AccountSetting.is_log_dialog_visible(), 'Log dialog is opened') - - -@Step('the user cancels the sync connection wizard') -def step(context): - SyncConnectionWizard.cancel_folder_sync_connection_wizard() - - -@When('the user quits the client') -def step(context): - Toolbar.quit_opencloud() - - -@Then('"|any|" account should be opened') -def step(context, displayname): - displayname = substitute_inline_codes(displayname) - if not Toolbar.account_has_focus(displayname): - raise LookupError(f"Account '{displayname}' should be opened, but it is not") - - -@Then( - r'the default local sync path should contain "([^"]*)" in the (configuration|sync connection) wizard', - regexp=True, -) -def step(context, sync_path, wizard): - sync_path = substitute_inline_codes(sync_path) - - actual_sync_path = '' - - if wizard == 'configuration': - actual_sync_path = AccountConnectionWizard.get_local_sync_path() - else: - actual_sync_path = SyncConnectionWizard.get_local_sync_path() - - test.compare( - actual_sync_path, - convert_path_separators_for_os(sync_path), - 'Compare sync path contains the expected path', - ) - - -@Then('the warning "|any|" should appear in the sync connection wizard') -def step(context, warn_message): - actual_message = SyncConnectionWizard.get_warn_label() - test.compare( - True, - warn_message in actual_message, - 'Contains warning message', - ) - - -@Given('the user has removed the connection for user "|any|"') -def step(context, username): - AccountSetting.remove_connection_for_user(username) - AccountSetting.wait_until_account_is_removed(username) - shutil.rmtree(os.path.join(get_config("clientRootSyncPath"), username)) diff --git a/test/gui/shared/steps/server_context.py b/test/gui/shared/steps/server_context.py deleted file mode 100644 index 5fd040305e..0000000000 --- a/test/gui/shared/steps/server_context.py +++ /dev/null @@ -1,139 +0,0 @@ -import tempfile -from pathlib import Path - -from helpers.api import provisioning, webdav_helper as webdav -from helpers.FilesHelper import get_document_content, get_file_for_upload - -from pageObjects.Toolbar import Toolbar - - -@Then( - r'^as "([^"].*)" (?:file|folder) "([^"].*)" should not exist in the server', - regexp=True, -) -def step(context, user_name, resource_name): - test.compare( - webdav.resource_exists(user_name, resource_name), - False, - f"Resource '{resource_name}' should not exist, but does", - ) - - -@Then( - r'^as "([^"].*)" (?:file|folder) "([^"].*)" should exist in the server', regexp=True -) -def step(context, user_name, resource_name): - test.compare( - webdav.resource_exists(user_name, resource_name), - True, - f"Resource '{resource_name}' should exist, but does not", - ) - - -@Then('as "|any|" the file "|any|" should have the content "|any|" in the server') -def step(context, user_name, file_name, content): - text_content = webdav.get_file_content(user_name, file_name) - test.compare( - text_content, - content, - f"File '{file_name}' should have content '{content}' but found '{text_content}'", - ) - - -@Then( - r'as user "([^"].*)" folder "([^"].*)" should contain "([^"].*)" items in the server', - regexp=True, -) -def step(context, user_name, folder_name, items_number): - total_items = webdav.get_folder_items_count(user_name, folder_name) - test.compare( - total_items, items_number, f'Folder should contain {items_number} items' - ) - - -@Given('user "|any|" has created folder "|any|" in the server') -def step(context, user, folder_name): - webdav.create_folder(user, folder_name) - - -@Given('user "|any|" has uploaded file with content "|any|" to "|any|" in the server') -def step(context, user, file_content, file_name): - webdav.create_file(user, file_name, file_content) - - -@When('the user clicks on the settings tab') -def step(context): - Toolbar.open_settings_tab() - - -@When('user "|any|" uploads file with content "|any|" to "|any|" in the server') -def step(context, user, file_content, file_name): - webdav.create_file(user, file_name, file_content) - - -@When('user "|any|" deletes the folder "|any|" in the server') -def step(context, user, folder_name): - webdav.delete_resource(user, folder_name) - - -@Given('user "|any|" has been created in the server with default attributes') -def step(context, user): - provisioning.create_user(user) - - -@Given('user "|any|" has uploaded file "|any|" to "|any|" in the server') -def step(context, user, file_name, destination): - webdav.upload_file(user, file_name, destination) - - -@Then( - 'as "|any|" the content of file "|any|" in the server should match the content of local file "|any|"' -) -def step(context, user_name, server_file_name, local_file_name): - raw_server_content = webdav.get_file_content(user_name, server_file_name) - with tempfile.NamedTemporaryFile(suffix=Path(server_file_name).suffix) as tmp_file: - if isinstance(raw_server_content, str): - tmp_file.write(raw_server_content.encode('utf-8')) - else: - tmp_file.write(raw_server_content) - server_content = get_document_content(tmp_file.name) - local_content = get_document_content(get_file_for_upload(local_file_name)) - - test.compare( - server_content, - local_content, - f"Server file '{server_file_name}' differs from local file '{local_file_name}'", - ) - - -@Then( - r'as "([^"].*)" following files should not exist in the server', - regexp=True, -) -def step(context, user_name): - for row in context.table[1:]: - resource_name = row[0] - test.compare( - webdav.resource_exists(user_name, resource_name), - False, - f"Resource '{resource_name}' should not exist, but does", - ) - - -@Given('user "|any|" has uploaded the following files to the server') -def step(context, user): - for row in context.table[1:]: - file_name = row[0] - file_content = row[1] - webdav.create_file(user, file_name, file_content) - - -@Given('user "|any|" has sent the following resource share invitation:') -def step(context, user): - resource_details = {row[0]: row[1] for row in context.table} - webdav.send_resource_share_invitation( - user, - resource_details['resource'], - resource_details['sharee'], - resource_details['permissionsRole'], - )