From fd7d4ea4c2518c0959970b64557ac8abc7fc6ad0 Mon Sep 17 00:00:00 2001 From: pradip Date: Wed, 26 Nov 2025 10:41:11 +0545 Subject: [PATCH 01/75] test: fix PermissionError in re-add account test on Windows --- .../scripts/pageObjects/AccountSetting.py | 18 ++++++++++++++++++ test/gui/shared/steps/account_context.py | 1 + 2 files changed, 19 insertions(+) diff --git a/test/gui/shared/scripts/pageObjects/AccountSetting.py b/test/gui/shared/scripts/pageObjects/AccountSetting.py index 8071bb0de4..1c197ec11c 100644 --- a/test/gui/shared/scripts/pageObjects/AccountSetting.py +++ b/test/gui/shared/scripts/pageObjects/AccountSetting.py @@ -166,3 +166,21 @@ def remove_connection_for_user(username): displayname = substitute_inline_codes(displayname) Toolbar.open_account(displayname) AccountSetting.remove_account_connection() + + @staticmethod + 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) + return account is None + + result = squish.waitFor(account_gone, timeout) + + if not result: + raise TimeoutError( + "Timeout waiting for account to be removed for " + + str(timeout) + + " milliseconds" + ) diff --git a/test/gui/shared/steps/account_context.py b/test/gui/shared/steps/account_context.py index 1f77481571..1e7bffcec4 100644 --- a/test/gui/shared/steps/account_context.py +++ b/test/gui/shared/steps/account_context.py @@ -286,4 +286,5 @@ def step(context, warn_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)) From 6618780d2cd3d809004a52a10ce7cf3246f106d9 Mon Sep 17 00:00:00 2001 From: Pradip Subedi Date: Fri, 28 Nov 2025 16:46:23 +0545 Subject: [PATCH 02/75] test: remove skip and issue tag (#732) --- test/gui/tst_addAccount/test.feature | 2 +- test/gui/tst_deleteFilesFolders/test.feature | 2 +- test/gui/tst_moveFilesFolders/test.feature | 4 ++-- test/gui/tst_spaces/test.feature | 4 ++-- test/gui/tst_syncing/test.feature | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/gui/tst_addAccount/test.feature b/test/gui/tst_addAccount/test.feature index 13d9a6e56a..6d05242a6d 100644 --- a/test/gui/tst_addAccount/test.feature +++ b/test/gui/tst_addAccount/test.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 - @skipOnWindows @issue-435 + 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/tst_deleteFilesFolders/test.feature b/test/gui/tst_deleteFilesFolders/test.feature index 1924261f4c..7d84e15a70 100644 --- a/test/gui/tst_deleteFilesFolders/test.feature +++ b/test/gui/tst_deleteFilesFolders/test.feature @@ -65,7 +65,7 @@ Feature: deleting files and folders | textfile1.txt | And as "Alice" file "textfile2.txt" should exist in the server - @issue-435 + Scenario: Create and delete a file with special characters in the filename Given user "Alice" has set up a client with default settings When user "Alice" creates a file "~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️‍🔥.txt" with the following content inside the sync folder diff --git a/test/gui/tst_moveFilesFolders/test.feature b/test/gui/tst_moveFilesFolders/test.feature index 5ec2a8aece..ed8a78a46b 100644 --- a/test/gui/tst_moveFilesFolders/test.feature +++ b/test/gui/tst_moveFilesFolders/test.feature @@ -67,7 +67,7 @@ Feature: move file and folder And as "Alice" file "folder1/file2.txt" should not exist in the server - @issue-435 + Scenario: Move resources from different sub-levels to sync root Given user "Alice" has created folder "folder1/folder2/folder3/folder4/test-folder" in the server And user "Alice" has uploaded file with content "openCloud" to "folder1/folder2/lorem.txt" in the server @@ -80,7 +80,7 @@ Feature: move file and folder And as "Alice" file "folder1/folder2/lorem.txt" should not exist in the server And as "Alice" folder "folder1/folder2/folder3/folder4/test-folder" should not exist in the server - @issue-435 + Scenario: Syncing a 50MB file moved into the local sync folder Given user "Alice" has set up a client with default settings And user "Alice" has created a folder "Folder1" inside the sync folder diff --git a/test/gui/tst_spaces/test.feature b/test/gui/tst_spaces/test.feature index 29f2d58ab0..c3f42fe380 100644 --- a/test/gui/tst_spaces/test.feature +++ b/test/gui/tst_spaces/test.feature @@ -67,7 +67,7 @@ Feature: Project spaces Then the sync folder list should be empty But the file "testfile.txt" should exist on the file system - @issue-435 + Scenario: User with Viewer role cannot create resource Given the administrator has added user "Alice" to space "Project101" with role "viewer" And user "Alice" has set up a client with space "Project101" @@ -82,7 +82,7 @@ Feature: Project spaces | resource | status | account | | simple-folder | Blacklisted | Alice Hansen@%local_server_hostname% | - @issue-435 + Scenario: Sharee with Editor role deletes the shared resource Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index ea4251592e..384e2af6b0 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -404,7 +404,7 @@ Feature: Syncing files And the user waits for file "file with space.txt" to be synced Then as "Alice" file "file with space.txt" should exist in the server - @issue-435 + Scenario: Syncing folders each having large number of files Given the user has created a folder "folder1" in temp folder And the user has created "500" files each of size "1048576" bytes inside folder "folder1" in temp folder @@ -533,7 +533,7 @@ Feature: Syncing files | simple-folder/sub-folder | Blacklisted | Brian Murphy@%local_server_hostname% | | simple-folder/simple.pdf | Blacklisted | Brian Murphy@%local_server_hostname% | - @skipOnWindows @issue-435 + Scenario Outline: File with long multi-byte characters name can be synced (76 characters, 255 bytes including extension) Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder @@ -546,7 +546,7 @@ Feature: Syncing files | filename | | 𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺abôǣฎพฒฆ๘ตกกผพฒณญไใๅำ๊๒๔๗๘รศฬอฮ.txt | - @issue-435 + Scenario: Sync a received shared folder with Editor permission role Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server @@ -566,7 +566,7 @@ Feature: Syncing files And as "Brian" file "Shares/simple-folder/simple.pdf" should exist in the server And as "Brian" the file "Shares/simple-folder/uploaded-lorem.txt" should have the content "overwrite openCloud test text file" in the server - @issue-435 + Scenario: Unselected subfolders are excluded from local sync Given user "Alice" has created folder "test-folder" in the server And user "Alice" has created folder "test-folder/sub-folder1" in the server From eea2cfe46ecc9151a79c89fddd95b5cc79db55b7 Mon Sep 17 00:00:00 2001 From: Pradip Subedi Date: Fri, 28 Nov 2025 16:47:49 +0545 Subject: [PATCH 03/75] test: use another folder name (#731) --- test/gui/tst_moveFilesFolders/test.feature | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/gui/tst_moveFilesFolders/test.feature b/test/gui/tst_moveFilesFolders/test.feature index ed8a78a46b..8ff5e5bd8e 100644 --- a/test/gui/tst_moveFilesFolders/test.feature +++ b/test/gui/tst_moveFilesFolders/test.feature @@ -83,8 +83,8 @@ Feature: move file and folder Scenario: Syncing a 50MB file moved into the local sync folder Given user "Alice" has set up a client with default settings - And user "Alice" has created a folder "Folder1" inside the sync folder + And user "Alice" has created a folder "NewFolder" inside the sync folder And user "Alice" has created a file "newfile.txt" with size "50MB" in the sync folder - When user "Alice" moves file "newfile.txt" to "Folder1" in the sync folder - And the user waits for file "Folder1/newfile.txt" to be synced - Then as "Alice" file "Folder1/newfile.txt" should exist in the server + When user "Alice" moves file "newfile.txt" to "NewFolder" in the sync folder + And the user waits for file "NewFolder/newfile.txt" to be synced + Then as "Alice" file "NewFolder/newfile.txt" should exist in the server From 186ac8c4477fc5bffc9c52e11734a6e1bc0417e2 Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Mon, 14 Jul 2025 16:15:08 +0545 Subject: [PATCH 04/75] wait for file to exist before checking size --- test/gui/shared/scripts/helpers/FilesHelper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/gui/shared/scripts/helpers/FilesHelper.py b/test/gui/shared/scripts/helpers/FilesHelper.py index a417dc0276..f2994fde6e 100644 --- a/test/gui/shared/scripts/helpers/FilesHelper.py +++ b/test/gui/shared/scripts/helpers/FilesHelper.py @@ -3,6 +3,7 @@ import ctypes import shutil +import squish from helpers.ConfigHelper import is_windows, get_config @@ -93,8 +94,10 @@ def get_size_in_bytes(size): def get_file_size_on_disk(resource_path): - file_size_high = ctypes.c_ulonglong(0) if is_windows(): + timeout = get_config('maxSyncTimeout') * 1000 + squish.waitFor(lambda: os.path.exists(resource_path), timeout) + file_size_high = ctypes.c_ulonglong(0) return ctypes.windll.kernel32.GetCompressedFileSizeW( ctypes.c_wchar_p(resource_path), ctypes.pointer(file_size_high) ) From e68811f518e81db05259453132106804e0bb49ed Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Mon, 14 Jul 2025 16:24:29 +0545 Subject: [PATCH 05/75] enable vfs in client config --- test/gui/shared/scripts/helpers/SetupClientHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gui/shared/scripts/helpers/SetupClientHelper.py b/test/gui/shared/scripts/helpers/SetupClientHelper.py index d1c6e660f4..67d1ecb166 100644 --- a/test/gui/shared/scripts/helpers/SetupClientHelper.py +++ b/test/gui/shared/scripts/helpers/SetupClientHelper.py @@ -177,7 +177,7 @@ def generate_account_config(users, space='Personal'): settings.setValue("localPath", sync_path) settings.setValue("paused", 'false') settings.setValue("priority", '50') - settings.setValue("virtualFilesMode", 'off') + settings.setValue("virtualFilesMode", 'cfapi') settings.setValue("journalPath",".sync_journal.db") settings.endArray() settings.setValue("size", len(users)) From ac11deb6c02544de460422b7404de815cb703613 Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Wed, 16 Jul 2025 11:02:23 +0545 Subject: [PATCH 06/75] enable some VFS tests --- test/gui/tst_vfs/test.feature | 46 +++++++++++++++++++++++++++++++++++ test/gui/tst_vfs/test.py | 8 ++++++ 2 files changed, 54 insertions(+) create mode 100644 test/gui/tst_vfs/test.feature create mode 100644 test/gui/tst_vfs/test.py diff --git a/test/gui/tst_vfs/test.feature b/test/gui/tst_vfs/test.feature new file mode 100644 index 0000000000..6961f86634 --- /dev/null +++ b/test/gui/tst_vfs/test.feature @@ -0,0 +1,46 @@ +@skipOnLinux +Feature: Enable/disable virtual file support + As a user + I want to enable virtual file support + So that I can synchronize virtual files with local folder + + + Scenario: Copy and paste virtual file + Given user "Alice" has been created in the server with default attributes + And user "Alice" has uploaded file with content "sample file" to "sampleFile.txt" in the server + And user "Alice" has uploaded file with content "lorem file" to "lorem.txt" in the server + And user "Alice" has uploaded file with content "test file" to "testFile.txt" in the server + And user "Alice" has created folder "Folder" in the server + And user "Alice" has set up a client with default settings + Then the placeholder of file "lorem.txt" should exist on the file system + And the placeholder of file "sampleFile.txt" should exist on the file system + And the placeholder of file "testFile.txt" should exist on the file system + When user "Alice" copies file "sampleFile.txt" to temp folder + And the user copies the file "lorem.txt" to "Folder" + And the user copies the file "testFile.txt" to "testFile.txt" + And the user waits for file "Folder/lorem.txt" to be synced + Then the file "sampleFile.txt" should be downloaded + And the file "Folder/lorem.txt" should be downloaded + And the file "lorem.txt" should be downloaded + And the file "testFile.txt" should be downloaded + And the file "testFile - Copy.txt" should be downloaded + And as "Alice" file "Folder/lorem.txt" should exist in the server + And as "Alice" file "lorem.txt" should exist in the server + And as "Alice" file "sampleFile.txt" should exist in the server + And as "Alice" file "testFile.txt" should exist in the server + And as "Alice" file "testFile - Copy.txt" should exist in the server + + + Scenario: Move virtual file + Given user "Alice" has been created in the server with default attributes + And user "Alice" has uploaded file with content "lorem file" to "lorem.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "sampleFile.txt" in the server + And user "Alice" has created folder "Folder" in the server + And user "Alice" has set up a client with default settings + When user "Alice" moves file "lorem.txt" to "Folder" in the sync folder + And user "Alice" moves file "sampleFile.txt" to the temp folder + And the user waits for file "Folder/lorem.txt" to be synced + Then the placeholder of file "Folder/lorem.txt" should exist on the file system + And as "Alice" file "Folder/lorem.txt" should exist in the server + And as "Alice" file "lorem.txt" should not exist in the server + And as "Alice" file "sampleFile.txt" should not exist in the server diff --git a/test/gui/tst_vfs/test.py b/test/gui/tst_vfs/test.py new file mode 100644 index 0000000000..83b0a5275a --- /dev/null +++ b/test/gui/tst_vfs/test.py @@ -0,0 +1,8 @@ +source(findFile('scripts', 'python/bdd.py')) + +setupHooks('../shared/scripts/bdd_hooks.py') +collectStepDefinitions('./steps', '../shared/steps') + + +def main(): + runFeatureFile('test.feature') From d55f6e4aeacaf298f81d7fcc32ecda134ccb9725 Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Wed, 16 Jul 2025 12:01:35 +0545 Subject: [PATCH 07/75] only enable vfs on windows --- test/gui/shared/scripts/helpers/SetupClientHelper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/gui/shared/scripts/helpers/SetupClientHelper.py b/test/gui/shared/scripts/helpers/SetupClientHelper.py index 67d1ecb166..4c66aec862 100644 --- a/test/gui/shared/scripts/helpers/SetupClientHelper.py +++ b/test/gui/shared/scripts/helpers/SetupClientHelper.py @@ -177,7 +177,10 @@ def generate_account_config(users, space='Personal'): settings.setValue("localPath", sync_path) settings.setValue("paused", 'false') settings.setValue("priority", '50') - settings.setValue("virtualFilesMode", 'cfapi') + if is_windows(): + settings.setValue("virtualFilesMode", 'cfapi') + else: + settings.setValue("virtualFilesMode", 'off') settings.setValue("journalPath",".sync_journal.db") settings.endArray() settings.setValue("size", len(users)) From 3895f6fb7ae291efe313f5d01d760a379ec91128 Mon Sep 17 00:00:00 2001 From: pradip Date: Mon, 8 Sep 2025 13:58:55 +0545 Subject: [PATCH 08/75] test: selective sync for root level file --- test/gui/tst_syncing/test.feature | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index 384e2af6b0..5bf5e5f6a0 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -580,3 +580,27 @@ Feature: Syncing files When user "Alice" uploads file with content "some content" to "test-folder/sub-folder2/lorem.txt" in the server And the user waits for the files to sync Then the file "test-folder/sub-folder2/lorem.txt" should not exist on the file system + + @skipOnWindows + Scenario: Only root level files sync when all folders are unselected + Given user "Alice" has created folder "test-folder" in the server + And user "Alice" has created folder "test-folder/sub-folder1" in the server + And user "Alice" has created folder "test-folder/sub-folder2" in the server + And user "Alice" has uploaded file with content "root file content" to "root-file.txt" in the server + And user "Alice" has uploaded file with content "some subfolder content" to "test-folder/sub-folder1/lorem.txt" in the server + And the user has started the client + And the user has entered the following account information: + | server | %local_server% | + | user | Alice | + | password | 1234 | + 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 user unselects all the remote folders + And the user adds the folder sync connection + And the user waits for the files to sync + Then the folder "test-folder/sub-folder1" should not exist on the file system + And the folder "test-folder/sub-folder2" should not exist on the file system + And the file "test-folder/sub-folder1/lorem.txt" should not exist on the file system + But the file "root-file.txt" should exist on the file system + From 5626686253d250848cfdbdb541fb8b24e8b84314 Mon Sep 17 00:00:00 2001 From: pradip Date: Wed, 10 Dec 2025 12:51:16 +0545 Subject: [PATCH 09/75] test(refactor): skip selective sync tests because of VFS --- test/gui/tst_syncing/test.feature | 7 +++++-- test/manual/test_plan/testplan.md | 16 +++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index 5bf5e5f6a0..aebe81a115 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -57,6 +57,7 @@ Feature: Syncing files client content """ + @skipOnWindows Scenario: Sync all is selected by default Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -81,6 +82,7 @@ Feature: Syncing files But the folder "simple-folder" should not exist on the file system And the folder "large-folder" should not exist on the file system + @skipOnWindows Scenario: Sync only one folder from the server Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -111,7 +113,7 @@ Feature: Syncing files And the user waits for the files to sync Then as "Alice" folder "simple-folder" should not exist in the server - @issue-9733 + @issue-9733 @skipOnWindows Scenario: sort folders list by name and size Given user "Alice" has created folder "123Folder" in the server And user "Alice" has uploaded file with content "small" to "123Folder/lorem.txt" in the server @@ -456,6 +458,7 @@ Feature: Syncing files And as "Alice" the file "file2.txt" should have the content "Test file2" in the server + @skipOnWindows Scenario: sync remote folder to a local sync folder having special characters Given user "Alice" has created folder "~`!@#$^&()-_=+{[}];',)" in the server And user "Alice" has created folder "simple-folder" in the server @@ -566,7 +569,7 @@ Feature: Syncing files And as "Brian" file "Shares/simple-folder/simple.pdf" should exist in the server And as "Brian" the file "Shares/simple-folder/uploaded-lorem.txt" should have the content "overwrite openCloud test text file" in the server - + @skipOnWindows Scenario: Unselected subfolders are excluded from local sync Given user "Alice" has created folder "test-folder" in the server And user "Alice" has created folder "test-folder/sub-folder1" in the server diff --git a/test/manual/test_plan/testplan.md b/test/manual/test_plan/testplan.md index fe6e9590b1..7018d04f7c 100644 --- a/test/manual/test_plan/testplan.md +++ b/test/manual/test_plan/testplan.md @@ -177,16 +177,18 @@ Note: "Via Web" means check files on server in the web browser | 3 | Configure synchronization manually, a space | 1. Start the desktop client and fill in the server details
2. Check the advanced configuration checkbox
3. choose `Configure synchronization manually`
4. Connect the account
5. Choose "Cancel" in the next screen | - No local sync folder is created
- The setting window is opened and the account is registered | :robot: Win
:construction: macOS
:robot: Linux | tst_syncing | ### 11. Selective sync +> [!NOTE] +> Selective sync is not available on Windows due to VFS implemented by default. | ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) | |----|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------| -| 1 | sync only one folder | 1. Upload some files and folders to the server
2. add an account to the desktop client with manual sync configuration
3. Choose the personal space to be synced
4. choose a local folder
5. Select only one folder to be synced and add the connection | Only one folder is synced | :robot: Win
:construction: macOS
:robot: Linux | tst_syncing | -| 3 | unselected subfolders | 1. Upload a folder that has many subfolders to the server
2. Connect the desktop client and sync the personal space
From the `...` button for the space select "Chose what to sync" window, select the folder that has many subfolders
3. Extend that folder and unselect some subfolders
3. Click "OK" | The parent folder is synced but not the unselected subfolders | :robot: Win
:construction: macOS
:robot: Linux | tst_syncing | -| 4 | Folder without subfolder in the list | 1. From the `Deselect remote folders...` window, click on the `>` for a folder that does not have subfolders | the `>` disappears | :construction: Win
:construction: macOS
:construction: Linux | | -| 5 | sync files both ways for selected folder | 1. From the `Deselect remote folders...` window, select a folder to sync and add the connection
2. Upload some files via webUI into that folder
3. Copy some other files into the corresponding local folder
4. Wait for sync | Files are synced both ways | :construction: Win
:construction: macOS
:construction: Linux | | -| 6 | sync files for unselected folder | 1. From the `Deselect remote folders...` window, unselect a folder and add connection
2. From the server, upload some files in that unselected folder | The folder and files are not available in the sync folder
Previously synced folders are deleted | :construction: Win
:construction: macOS
:robot: Linux | tst-syncing | -| 10 | sync of files in root folder | 1. From the `Deselect remote folders...` window, unselect all the folders
2. Add the connection | files that are in the root folder are synced | :construction: Win
:construction: macOS
:construction: Linux | | -| 11 | sorting of folders | 1. In the `Deselect remote folders...` window, sort the folders by name and size | Sorting works | :robot: Win
:construction: macOS
:robot: Linux | tst_syncing | +| 1 | sync only one folder | 1. Upload some files and folders to the server
2. add an account to the desktop client with manual sync configuration
3. Choose the personal space to be synced
4. choose a local folder
5. Select only one folder to be synced and add the connection | Only one folder is synced | :construction: macOS
:robot: Linux | tst_syncing | +| 2 | unselected subfolders | 1. Upload a folder that has many subfolders to the server
2. Connect the desktop client and sync the personal space
From the `...` button for the space select "Chose what to sync" window, select the folder that has many subfolders
3. Extend that folder and unselect some subfolders
3. Click "OK" | The parent folder is synced but not the unselected subfolders | :construction: macOS
:robot: Linux | tst_syncing | +| 3 | Folder without subfolder in the list | 1. From the `Deselect remote folders...` window, click on the `>` for a folder that does not have subfolders | the `>` disappears | :construction: macOS
:construction: Linux | | +| 4 | sync files both ways for selected folder | 1. From the `Deselect remote folders...` window, select a folder to sync and add the connection
2. Upload some files via webUI into that folder
3. Copy some other files into the corresponding local folder
4. Wait for sync | Files are synced both ways | :construction: macOS
:construction: Linux | | +| 5 | sync files for unselected folder | 1. From the `Deselect remote folders...` window, unselect a folder and add connection
2. From the server, upload some files in that unselected folder | The folder and files are not available in the sync folder
Previously synced folders are deleted | :construction: macOS
:robot: Linux | tst-syncing | +| 6 | sync of files in root folder | 1. From the `Deselect remote folders...` window, unselect all the folders
2. Add the connection | files that are in the root folder are synced | :construction: macOS
:construction: Linux | | +| 7 | sorting of folders | 1. In the `Deselect remote folders...` window, sort the folders by name and size | Sorting works | :construction: macOS
:robot: Linux | tst_syncing | ### 12. Overlay icons From 36b1666444345b2a8823485271f6cb57777c4fee Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 1 Jan 2026 14:53:12 +0545 Subject: [PATCH 10/75] ci: re-arrange woodpecker configurations (#769) * ci: re-arrange woodpecker configurations Signed-off-by: Saw-jan * ci: separate steps by new line Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 44 +++++----- .woodpecker/cache-opencloud.yaml | 73 +++++++++-------- .woodpecker/cache-pnpm.yaml | 72 ++++++++-------- .woodpecker/cache-python.yaml | 58 ++++++------- .woodpecker/notification.yaml | 18 ++-- .woodpecker/purge-cache.yaml | 9 +- .woodpecker/ready-release-go.yaml | 2 +- .woodpecker/translation.yaml | 15 ++-- .woodpecker/ui-tests.yaml | 131 ++++++++++++++++-------------- 9 files changed, 223 insertions(+), 199 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index 04cc468973..e778bd5f13 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -1,4 +1,3 @@ ---- variables: - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' @@ -9,7 +8,25 @@ variables: from_secret: cache_s3_secret_key CACHE_BUCKET: from_secret: cache_s3_bucket - MC_HOST: "https://s3.ci.opencloud.eu" + MC_HOST: 'https://s3.ci.opencloud.eu' + +when: + - branch: + - main + - stable-* + event: + - push + - manual + - event: pull_request + evaluate: | + !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") + - event: tag + - event: cron + cron: nightly* + +workspace: + base: /woodpecker/ + path: desktop steps: - name: fix-permissions @@ -24,27 +41,10 @@ steps: - cmake %s -S .. -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ - ninja - name: upload-desktop-client-cache + image: *minio_image + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop-build/${CI_COMMIT_SHA}/ - mc ls --recursive s3/$CACHE_BUCKET/desktop-build - environment: - <<: *minio_environment - image: *minio_image - -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: pull_request - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: tag - - event: cron - cron: nightly* -workspace: - base: /woodpecker/ - path: desktop diff --git a/.woodpecker/cache-opencloud.yaml b/.woodpecker/cache-opencloud.yaml index 3f8eafa5e6..6c384a4264 100644 --- a/.woodpecker/cache-opencloud.yaml +++ b/.woodpecker/cache-opencloud.yaml @@ -1,4 +1,3 @@ ---- variables: - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - &minio_environment @@ -8,63 +7,69 @@ variables: from_secret: cache_s3_secret_key CACHE_BUCKET: from_secret: cache_s3_bucket - MC_HOST: "https://s3.ci.opencloud.eu" + MC_HOST: 'https://s3.ci.opencloud.eu' + +when: + - branch: + - main + - stable-* + event: + - push + - manual + - event: pull_request + evaluate: | + !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") + - event: tag + - event: cron + cron: nightly* skip_clone: true + steps: - - commands: + - name: check-for-existing-cache + image: *minio_image + environment: + <<: *minio_environment + commands: - curl -o .woodpecker.env https://raw.githubusercontent.com/opencloud-eu/desktop/$CI_COMMIT_SHA/.woodpecker.env - curl -o script.sh https://raw.githubusercontent.com/opencloud-eu/desktop/$CI_COMMIT_SHA/test/gui/woodpecker/script.sh - . ./.woodpecker.env - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc ls --recursive s3/$CACHE_BUCKET/opencloud-build - bash script.sh check_opencloud_cache - environment: - <<: *minio_environment - image: *minio_image - name: check-for-existing-cache - - commands: + + - name: clone-opencloud + image: docker.io/golang:1.24 + commands: - . ./.woodpecker.env - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - git clone -b $OPENCLOUD_BRANCH --single-branch https://github.com/opencloud-eu/opencloud.git repo_opencloud - cd repo_opencloud - git checkout $OPENCLOUD_COMMITID - image: docker.io/golang:1.24 - name: clone-opencloud - - commands: + + - name: generate-opencloud + image: owncloudci/nodejs:20 + commands: - . ./.woodpecker.env - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - cd repo_opencloud - for i in $(seq 3); do make node-generate-prod && break || sleep 1; done - image: owncloudci/nodejs:20 - name: generate-opencloud - - commands: + + - name: build-opencloud + image: docker.io/golang:1.24 + commands: - . ./.woodpecker.env - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - cd repo_opencloud - for i in $(seq 3); do make -C opencloud build && break || sleep 1; done - image: docker.io/golang:1.24 - name: build-opencloud - - commands: + + - name: upload-opencloud-cache + image: *minio_image + environment: + <<: *minio_environment + commands: - . ./.woodpecker.env - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -a repo_opencloud/opencloud/bin/opencloud s3/$CACHE_BUCKET/opencloud-build/$OPENCLOUD_COMMITID/ - mc ls --recursive s3/$CACHE_BUCKET/opencloud-build - environment: - <<: *minio_environment - image: *minio_image - name: upload-opencloud-cache -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: pull_request - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: tag - - event: cron - cron: nightly* diff --git a/.woodpecker/cache-pnpm.yaml b/.woodpecker/cache-pnpm.yaml index 48a665c816..b8747a455f 100644 --- a/.woodpecker/cache-pnpm.yaml +++ b/.woodpecker/cache-pnpm.yaml @@ -1,4 +1,3 @@ ---- variables: - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - &minio_environment @@ -8,61 +7,66 @@ variables: from_secret: cache_s3_secret_key CACHE_BUCKET: from_secret: cache_s3_bucket - MC_HOST: "https://s3.ci.opencloud.eu" + MC_HOST: 'https://s3.ci.opencloud.eu' + +when: + - branch: + - main + - stable-* + event: + - push + - manual + - event: tag + - event: pull_request + evaluate: | + !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") + - event: cron + cron: nightly* + +workspace: + base: /woodpecker/ + path: desktop steps: - - commands: + - name: check-browsers-cache + image: *minio_image + environment: + <<: *minio_environment + commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc ls --recursive s3/$CACHE_BUCKET/web - bash test/gui/woodpecker/script.sh check_browsers_cache - environment: - <<: *minio_environment - image: *minio_image - name: check-browsers-cache - - commands: + + - name: pnpm-install + image: owncloudci/nodejs:20 + commands: - . ./.woodpecker.env - if $BROWSER_CACHE_FOUND; then exit 0; fi - cd test/gui/ - npm i -s -g -f "$(jq -r ".packageManager" < webUI/package.json)" - pnpm config set store-dir ./.pnpm-store - make pnpm-install + + - name: install-browsers image: owncloudci/nodejs:20 - name: pnpm-install - - commands: + environment: + PLAYWRIGHT_BROWSERS_PATH: .playwright + commands: - . ./.woodpecker.env - if $BROWSER_CACHE_FOUND; then exit 0; fi - cd test/gui/ - make pnpm-install-chromium - cd webUI - tar -czvf /woodpecker/desktop/playwright-browsers.tar.gz .playwright + + - name: upload-browsers-cache + image: *minio_image environment: - PLAYWRIGHT_BROWSERS_PATH: .playwright - image: owncloudci/nodejs:20 - name: install-browsers - - commands: + <<: *minio_environment + commands: - . ./.woodpecker.env - if $BROWSER_CACHE_FOUND; then exit 0; fi - playwright_version=$(bash test/gui/woodpecker/script.sh get_playwright_version) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -r -a /woodpecker/desktop/playwright-browsers.tar.gz s3/$CACHE_BUCKET/web/browsers-cache/$playwright_version/ - mc ls --recursive s3/$CACHE_BUCKET/web - environment: - <<: *minio_environment - image: *minio_image - name: upload-browsers-cache -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: tag - - event: pull_request - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: cron - cron: nightly* -workspace: - base: /woodpecker/ - path: desktop diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index 9056b3b180..0069edd430 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -1,4 +1,3 @@ ---- variables: - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' @@ -9,18 +8,40 @@ variables: from_secret: cache_s3_secret_key CACHE_BUCKET: from_secret: cache_s3_bucket - MC_HOST: "https://s3.ci.opencloud.eu" + MC_HOST: 'https://s3.ci.opencloud.eu' + +when: + - branch: + - main + - stable-* + event: + - push + - manual + - event: tag + - event: pull_request + evaluate: | + !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") + - event: cron + cron: nightly* + +workspace: + base: /woodpecker/ + path: desktop steps: - name: check-python-cache + image: *minio_image + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc ls s3/$CACHE_BUCKET/desktop-build - bash test/gui/woodpecker/script.sh check_python_cache - environment: - <<: *minio_environment - image: *minio_image + - name: install-python-modules + image: *squish_image + environment: + PYTHONUSERBASE: /woodpecker/desktop commands: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi @@ -28,33 +49,14 @@ steps: - python3.10 -m pip list -v - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - tar -czvf /woodpecker/desktop/python-cache-$requirements_sha.tar.gz lib/python3.10/site-packages - image: *squish_image - environment: - PYTHONUSERBASE: /woodpecker/desktop + - name: upload-python-cache + image: *minio_image + environment: + <<: *minio_environment commands: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -r -a /woodpecker/desktop/python-cache*.tar.gz s3/$CACHE_BUCKET/desktop-build/ - mc ls s3/$CACHE_BUCKET/desktop-build - environment: - <<: *minio_environment - image: *minio_image - -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: tag - - event: pull_request - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: cron - cron: nightly* -workspace: - base: /woodpecker/ - path: desktop diff --git a/.woodpecker/notification.yaml b/.woodpecker/notification.yaml index e660034a38..285ace6066 100644 --- a/.woodpecker/notification.yaml +++ b/.woodpecker/notification.yaml @@ -6,19 +6,24 @@ variables: - &ci_woodpecker_url from_secret: oc_ci_url -depends_on: [build, ui-tests] -runs_on: [ success, failure ] when: - - branch: + - event: [push, manual] + branch: - main - stable-* - event: - - push - - manual - event: pull_request - event: tag - event: cron cron: nightly* + +depends_on: + - build + - ui-tests + +runs_on: + - success + - failure + skip_clone: true steps: @@ -41,7 +46,6 @@ steps: CI_WOODPECKER_URL: *ci_woodpecker_url CI_REPO_ID: *current_repo_id CI_WOODPECKER_TOKEN: no-auth-needed-on-this-repo - commands: - git clone --single-branch --branch $QA_REPO_BRANCH $QA_REPO /tmp/qa - cd /tmp/qa/scripts/matrix-notification/ diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index a93dd07cda..5a1ffb0688 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -1,12 +1,11 @@ ---- variables: - - &minio_image "minio/mc:RELEASE.2021-10-07T04-19-58Z" + - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - &minio_environment AWS_ACCESS_KEY_ID: from_secret: cache_s3_access_key AWS_SECRET_ACCESS_KEY: from_secret: cache_s3_secret_key - MC_HOST: "https://s3.ci.opencloud.eu" + MC_HOST: 'https://s3.ci.opencloud.eu' CACHE_BUCKET: from_secret: cache_s3_bucket PUBLIC_BUCKET: public @@ -41,11 +40,11 @@ matrix: steps: - name: ${JOB_NAME} image: *minio_image + environment: + <<: *minio_environment commands: - mc alias set s3 "$MC_HOST" "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" - if [ "$USE_PUBLIC_BUCKET" = "true" ]; then CACHE_BUCKET=$PUBLIC_BUCKET; fi - to_delete=$(mc find "s3/$CACHE_BUCKET/${PURGE_PATH}" --older-than "${TTL}") - if [ "$to_delete" = "" ]; then exit 0; fi - mc rm $to_delete - environment: - <<: *minio_environment diff --git a/.woodpecker/ready-release-go.yaml b/.woodpecker/ready-release-go.yaml index b4c5abfa13..de39dc491c 100644 --- a/.woodpecker/ready-release-go.yaml +++ b/.woodpecker/ready-release-go.yaml @@ -3,7 +3,7 @@ when: branch: stable-3.0 steps: - release-helper: + - name: release-helper image: woodpeckerci/plugin-ready-release-go:latest settings: git_email: devops@opencloud.eu diff --git a/.woodpecker/translation.yaml b/.woodpecker/translation.yaml index e0e276a4e9..6b44414195 100644 --- a/.woodpecker/translation.yaml +++ b/.woodpecker/translation.yaml @@ -1,15 +1,17 @@ ---- -when: - - event: cron - cron: translation-sync - variables: - &ubuntu_image 'ubuntu' - &git_action_plugin 'quay.io/thegeeklab/wp-git-action:2' +when: + - event: cron + cron: translation-sync + steps: - name: translation-update image: *ubuntu_image + environment: + TX_TOKEN: + from_secret: tx_token commands: - apt update - ln -fs /usr/share/zoneinfo/Europe/Berlin /etc/localtime @@ -23,9 +25,6 @@ steps: - rm -rf /tmp/tx - tx pull --force --all - rm tx LICENSE - environment: - TX_TOKEN: - from_secret: tx_token - name: translation-push image: *git_action_plugin diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index f45ad7a1d5..52d1d49943 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -1,4 +1,3 @@ ---- variables: - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' @@ -13,59 +12,79 @@ variables: MC_HOST: 'https://s3.ci.opencloud.eu' PUBLIC_BUCKET: public +when: + - branch: + - main + - stable-* + event: + - push + - manual + - event: pull_request + - event: tag + - event: cron + cron: nightly* + depends_on: - cache-opencloud - cache-pnpm - build + +workspace: + base: /woodpecker + path: desktop + steps: - - commands: + - name: pnpm-install + image: owncloudci/nodejs:20 + commands: - cd test/gui/ - npm i -s -g -f "$(jq -r ".packageManager" < webUI/package.json)" - pnpm config set store-dir ./.pnpm-store - make pnpm-install - image: owncloudci/nodejs:20 - name: pnpm-install + - name: restore-python-cache + image: *minio_image + environment: + <<: *minio_environment commands: - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -a s3/$CACHE_BUCKET/desktop-build/python-cache-$requirements_sha.tar.gz /woodpecker/desktop + + - name: install-python-modules + image: *squish_image environment: - <<: *minio_environment - image: *minio_image - - commands: + PYTHONUSERBASE: /woodpecker/desktop/ + commands: - tar -xvf python-cache-*.tar.gz -C . - make -C test/gui/ pip-install - python3.10 -m pip list -v - image: *squish_image + + - name: restore-browsers-cache + image: *minio_image environment: - PYTHONUSERBASE: /woodpecker/desktop/ - name: install-python-modules - - commands: + <<: *minio_environment + commands: - playwright_version=$(bash test/gui/woodpecker/script.sh get_playwright_version) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -r -a s3/$CACHE_BUCKET/web/browsers-cache/$playwright_version/playwright-browsers.tar.gz /woodpecker/desktop + + - name: unzip-browsers-cache + image: owncloud/ubuntu:20.04 + commands: + - tar -xvf /woodpecker/desktop/playwright-browsers.tar.gz -C . + + - name: restore-opencloud-cache + image: *minio_image environment: <<: *minio_environment - image: *minio_image - name: restore-browsers-cache - - commands: - - tar -xvf /woodpecker/desktop/playwright-browsers.tar.gz -C . - image: owncloud/ubuntu:20.04 - name: unzip-browsers-cache - - commands: + commands: - . ./.woodpecker.env - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -r -a s3/$CACHE_BUCKET/opencloud-build/$OPENCLOUD_COMMITID/opencloud /woodpecker/desktop - environment: - <<: *minio_environment - image: *minio_image - name: restore-opencloud-cache - - commands: - - mkdir -p /srv/app/tmp/opencloud/opencloud/data/ - - mkdir -p /srv/app/tmp/opencloud/storage/users/ - - ./opencloud init - - ./opencloud server + + - name: opencloud + image: docker.io/golang:1.24 detach: true environment: FRONTEND_SEARCH_MIN_LENGTH: '2' @@ -80,20 +99,25 @@ steps: OC_URL: https://opencloud:9200 PROXY_ENABLE_BASIC_AUTH: true WEB_UI_CONFIG_FILE: /woodpecker/desktop/test/gui/woodpecker/config-opencloud.json - image: docker.io/golang:1.24 - name: opencloud - - commands: - - timeout 300 bash -c 'while [ $(curl -sk -uadmin:admin https://opencloud:9200/graph/v1.0/users/admin -w %{http_code} -o /dev/null) != 200 ]; do sleep 1; done' + commands: + - mkdir -p /srv/app/tmp/opencloud/opencloud/data/ + - mkdir -p /srv/app/tmp/opencloud/storage/users/ + - ./opencloud init + - ./opencloud server + + - name: wait-for-opencloud image: owncloudci/alpine:latest - name: wait-for-opencloud + commands: + - timeout 300 bash -c 'while [ $(curl -sk -uadmin:admin https://opencloud:9200/graph/v1.0/users/admin -w %{http_code} -o /dev/null) != 200 ]; do sleep 1; done' + - name: restore-desktop-client + image: *minio_image + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -a -r s3/$CACHE_BUCKET/desktop-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build - ls -lh /woodpecker/desktop/build/bin - environment: - <<: *minio_environment - image: *minio_image - name: create-extra-directories image: *ubuntu_image @@ -131,55 +155,42 @@ steps: - name: crash-log image: *ubuntu_image when: - - status: [success, failure] + - status: + - success + - failure commands: - cat /woodpecker/desktop/test/gui/tmp/OpenCloud-crash.log 2>/dev/null || exit 0 - name: upload-test-reports - environment: - <<: *minio_environment image: *minio_image when: - - status: [failure] + - status: failure + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/ - mc cp -a -r /woodpecker/desktop/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER - name: gui-test-reports - environment: - <<: *minio_environment image: *minio_image when: - - status: [failure] + - status: failure + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - bash test/gui/woodpecker/gui_test_reports.sh - name: client-log - environment: - <<: *minio_environment image: *minio_image when: - - status: [failure] + - status: failure + environment: + <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - cd /woodpecker/desktop/test/gui/clientLog/ - echo "To download the logs, access the following links:" - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/) - "for f in $logs; do echo \"$MC_HOST/$f \n \" | cut -d '/' -f1-3,5-99; done" - -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: pull_request - - event: tag - - event: cron - cron: nightly* -workspace: - base: /woodpecker - path: desktop From 3201c35aeb359215a2d074716e964ff4d26d3cdc Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 7 Jan 2026 17:34:55 +0545 Subject: [PATCH 11/75] test(gui): listen for sync events before the file actions (#772) * test: compare document text content Signed-off-by: Saw-jan * test: handle core file info extraction errors Signed-off-by: Saw-jan * test: listen for sync events before file actions Signed-off-by: Saw-jan * test: rename variable * test: remove unnecessary var assignment Co-authored-by: Pradip Subedi * test: hanlde text files Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan Co-authored-by: Pradip Subedi --- test/gui/requirements.txt | 6 +- .../gui/shared/scripts/helpers/FilesHelper.py | 66 ++++++++++++++++++- .../scripts/helpers/StacktraceHelper.py | 8 ++- test/gui/shared/steps/file_context.py | 45 ++++++++----- test/gui/shared/steps/server_context.py | 28 +++++--- 5 files changed, 125 insertions(+), 28 deletions(-) diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 498e9a078b..7aca78c907 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -4,4 +4,8 @@ psutil==5.9.*; python_version >= "3.10" black==24.3.*; python_version >= "3.10" pylint==3.2.*; python_version >= "3.10" pywin32==305; sys_platform == 'win32' and python_version >= "3.10" -pyside6==6.9.*; python_version >= "3.10" \ No newline at end of file +pyside6==6.9.*; python_version >= "3.10" +pypdf==6.5.*; python_version >= "3.10" +python-docx==1.2.*; python_version >= "3.10" +python-pptx==1.0.*; python_version >= "3.10" +openpyxl==3.1.*; python_version >= "3.10" \ No newline at end of file diff --git a/test/gui/shared/scripts/helpers/FilesHelper.py b/test/gui/shared/scripts/helpers/FilesHelper.py index f2994fde6e..7fac7485f7 100644 --- a/test/gui/shared/scripts/helpers/FilesHelper.py +++ b/test/gui/shared/scripts/helpers/FilesHelper.py @@ -2,6 +2,12 @@ import re import ctypes import shutil +from pathlib import Path + +from pypdf import PdfReader +from docx import Document +from pptx import Presentation +from openpyxl import load_workbook import squish from helpers.ConfigHelper import is_windows, get_config @@ -139,5 +145,61 @@ def convert_path_separators_for_os(path): On other systems, returns the path unchanged. """ if is_windows(): - return path.replace('/', '\\') - return path \ No newline at end of file + return path.replace("/", "\\") + return path + + +def get_pdf_content(pdf_file): + reader = PdfReader(pdf_file) + content = "" + for page in reader.pages: + if page_text := page.extract_text(): + content += page_text + return content + + +def get_docs_content(docs_file): + doc = Document(docs_file) + content = "\n".join(p.text for p in doc.paragraphs) + return content + + +def get_presentation_content(ppt_file): + presentation = Presentation(ppt_file) + text = [] + for slide in presentation.slides: + for shape in slide.shapes: + if hasattr(shape, "text"): + text.append(shape.text) + return "\n".join(text) + + +def get_excel_content(excel_file): + # parse with read_only mode + workbook = load_workbook(excel_file, read_only=True, data_only=True) + text = [] + for sheet in workbook.worksheets: + for row in sheet.iter_rows(values_only=True): + for cell in row: + if cell is not None: + text.append(str(cell)) + return "\n".join(text) + + +def get_document_content(document): + content = "" + doc_ext = Path(document).suffix.lower().lstrip(".") + if doc_ext == "pdf": + content = get_pdf_content(document) + elif doc_ext == "docx": + content = get_docs_content(document) + elif doc_ext == "pptx": + content = get_presentation_content(document) + elif doc_ext == "xlsx": + content = get_excel_content(document) + elif doc_ext in ["txt", "md"]: + with open(document, "r", encoding="utf-8") as f: + content = f.read() + else: + raise ValueError(f"Unsupported document format: {doc_ext}") + return content diff --git a/test/gui/shared/scripts/helpers/StacktraceHelper.py b/test/gui/shared/scripts/helpers/StacktraceHelper.py index a67c009ba2..6c022ef83f 100644 --- a/test/gui/shared/scripts/helpers/StacktraceHelper.py +++ b/test/gui/shared/scripts/helpers/StacktraceHelper.py @@ -42,7 +42,13 @@ def parse_stacktrace(coredump_file): coredump_filename = os.path.basename(coredump_file) # example coredump file: core-1648445754-1001-11-!drone!src!build-GUI-tests!bin!opencloud patterns = coredump_filename.split('-') - app_binary = '-'.join(patterns[4:]).replace('!', '/') + app_binary = 'opencloud' + if len(patterns) == 1: + patterns.append('N/A') + patterns.append('N/A') + patterns.append('N/A') + else: + app_binary = '-'.join(patterns[4:]).replace('!', '/') message.append('-------------------------------------------') message.append(f'Executable: {app_binary}') diff --git a/test/gui/shared/steps/file_context.py b/test/gui/shared/steps/file_context.py index 6d3f2bf666..49d2eb0d66 100644 --- a/test/gui/shared/steps/file_context.py +++ b/test/gui/shared/steps/file_context.py @@ -8,7 +8,7 @@ import squish from helpers.SetupClientHelper import get_resource_path, get_temp_resource_path -from helpers.SyncHelper import wait_for_client_to_be_ready +from helpers.SyncHelper import wait_for_client_to_be_ready, listen_sync_status_for_item from helpers.ConfigHelper import get_config from helpers.FilesHelper import ( build_conflicted_regex, @@ -20,7 +20,7 @@ prefix_path_namespace, remember_path, convert_path_separators_for_os, - get_file_for_upload + get_file_for_upload, ) @@ -70,11 +70,13 @@ def write_file(resource, content): def wait_and_write_file(path, content): + listen_sync_status_for_item(get_resource_path(path), 'FILE') wait_for_client_to_be_ready() write_file(path, content) def wait_and_try_to_write_file(resource, content): + listen_sync_status_for_item(get_resource_path(resource), 'FILE') wait_for_client_to_be_ready() try: write_file(resource, content) @@ -102,7 +104,6 @@ def add_copy_suffix(resource_path, resource_type): def copy_resource(resource_type, source, destination, from_files_for_upload=False): - wait_for_client_to_be_ready() if from_files_for_upload: source_dir = get_file_for_upload(source) else: @@ -110,11 +111,26 @@ def copy_resource(resource_type, source, destination, from_files_for_upload=Fals destination_dir = get_resource_path(destination) if source_dir == destination_dir and destination_dir != '/': destination_dir = add_copy_suffix(source, resource_type) + + listen_sync_status_for_item(destination_dir, resource_type) + wait_for_client_to_be_ready() if resource_type == 'folder': return shutil.copytree(source_dir, destination_dir) return shutil.copy2(source_dir, destination_dir) +def move_resource(username, resource_type, source, destination, is_temp_folder=False): + if not is_temp_folder: + source = get_resource_path(source, username) + if destination == '/': + destination = '' + destination = get_resource_path(destination, username) + + listen_sync_status_for_item(destination, resource_type) + wait_for_client_to_be_ready() + shutil.move(source, destination) + + def deleteResource(resource, resource_type): resource_path = sanitize_path(get_resource_path(resource)) if resource_type == 'file': @@ -174,7 +190,10 @@ def step(context, file_path): ) -@Then(r'^the (file|folder) "([^"]*)" (should|should not) exist on the file system$', regexp=True) +@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 @@ -194,7 +213,7 @@ def step(context, resource_type, resource, should_or_should_not): expected, resource_exists, f'{resource_type.capitalize()} "{resource}" {"exists" if resource_exists else "does not exist"} on the system', -) + ) @Given('the user has changed the content of local file "|any|" to:') @@ -284,23 +303,17 @@ def step(context, file_number, file_size, folder_name): r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', regexp=True, ) -def step(context, username, _, resource_name): +def step(context, username, resource_type, resource_name): source_dir = join(get_config('tempFolderPath'), resource_name) - destination_dir = get_resource_path('/', username) - shutil.move(source_dir, destination_dir) + move_resource(username, resource_type, source_dir, '/', True) @When( - r'user "([^"]*)" moves (?:file|folder) "([^"]*)" to "([^"]*)" in the sync folder', + r'user "([^"]*)" moves (file|folder) "([^"]*)" to "([^"]*)" in the sync folder', regexp=True, ) -def step(context, username, source, destination): - wait_for_client_to_be_ready() - source_dir = get_resource_path(source, username) - if destination in (None, '/'): - destination = '' - destination_dir = get_resource_path(destination, username) - shutil.move(source_dir, destination_dir) +def step(context, username, resource_type, source, destination): + move_resource(username, resource_type, source, destination) @Then('user "|any|" should be able to open the file "|any|" on the file system') diff --git a/test/gui/shared/steps/server_context.py b/test/gui/shared/steps/server_context.py index 89ab6e2fa6..5fd040305e 100644 --- a/test/gui/shared/steps/server_context.py +++ b/test/gui/shared/steps/server_context.py @@ -1,4 +1,8 @@ +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 @@ -82,15 +86,23 @@ 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|"') +@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): - server_content = webdav.get_file_content(user_name, server_file_name) - local_content = open(get_file_for_upload(local_file_name), "rb").read() + 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}'" + f"Server file '{server_file_name}' differs from local file '{local_file_name}'", ) @@ -120,8 +132,8 @@ def step(context, user): 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'] + user, + resource_details['resource'], + resource_details['sharee'], + resource_details['permissionsRole'], ) From b2dfcd8d8ff87fad1a53dc7a2b44c3fa7d1e6507 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 8 Jan 2026 12:59:34 +0545 Subject: [PATCH 12/75] test: wait longer for error messages (#773) Signed-off-by: Saw-jan --- .../scripts/pageObjects/SyncConnection.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/gui/shared/scripts/pageObjects/SyncConnection.py b/test/gui/shared/scripts/pageObjects/SyncConnection.py index c93f6e69c0..9d0ef78314 100644 --- a/test/gui/shared/scripts/pageObjects/SyncConnection.py +++ b/test/gui/shared/scripts/pageObjects/SyncConnection.py @@ -6,8 +6,6 @@ class SyncConnection: - WAIT_ERROR_LABEL_TIMEOUT = 10 - FOLDER_SYNC_CONNECTION_LIST = { "container": names.quickWidget_scrollView_ScrollView, "type": "ListView", @@ -23,7 +21,7 @@ class SyncConnection: "container": names.quickWidget_scrollView_ScrollView, "id": "moreButton", "type": "Image", - "visible": True + "visible": True, } MENU = { "checkable": False, @@ -32,7 +30,7 @@ class SyncConnection: "text": "", "type": "MenuItem", "unnamed": 1, - "visible": True + "visible": True, } SELECTIVE_SYNC_APPLY_BUTTON = { "container": names.settings_stack_QStackedWidget, @@ -57,7 +55,7 @@ class SyncConnection: PERMISSION_ERROR_LABEL = { "container": names.folderError_Container, "type": "Label", - "visible": True + "visible": True, } @staticmethod @@ -71,10 +69,8 @@ def open_menu(): def perform_action(action): SyncConnection.open_menu() selector = SyncConnection.MENU.copy() - selector['text'] = action - squish.mouseClick( - squish.waitForObject(selector) - ) + selector["text"] = action + squish.mouseClick(squish.waitForObject(selector)) @staticmethod def force_sync(): @@ -103,7 +99,6 @@ def choose_what_to_sync(): SyncConnection.open_menu() SyncConnection.perform_action("Choose what to sync") - @staticmethod def get_folder_connection_count(): return squish.waitForObject(SyncConnection.FOLDER_SYNC_CONNECTION_LIST).count @@ -129,7 +124,7 @@ def wait_for_error_label(to_exist=True): """Wait for permission error label to appear or disappear""" status = squish.waitFor( lambda: object.exists(SyncConnection.PERMISSION_ERROR_LABEL) == to_exist, - SyncConnection.WAIT_ERROR_LABEL_TIMEOUT * 1000 + get_config("maxSyncTimeout") * 1000, ) if not status: action = "appear" if to_exist else "disappear" From 8d11db9e7b855667e76094b6f6b533537089e789 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Fri, 16 Jan 2026 12:43:15 +0545 Subject: [PATCH 13/75] test(gui): use current time if timestamp not found (#779) * test: use cuurent time if timestamp not found Signed-off-by: Saw-jan * test: wait and listen for sync events Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- test/gui/shared/scripts/helpers/StacktraceHelper.py | 6 +++++- test/gui/shared/steps/file_context.py | 9 ++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/gui/shared/scripts/helpers/StacktraceHelper.py b/test/gui/shared/scripts/helpers/StacktraceHelper.py index 6c022ef83f..d53e79eb15 100644 --- a/test/gui/shared/scripts/helpers/StacktraceHelper.py +++ b/test/gui/shared/scripts/helpers/StacktraceHelper.py @@ -2,6 +2,7 @@ import subprocess import glob import re +import time from datetime import datetime from helpers.ConfigHelper import is_windows @@ -50,9 +51,12 @@ def parse_stacktrace(coredump_file): else: app_binary = '-'.join(patterns[4:]).replace('!', '/') + timestamp = datetime.fromtimestamp( + float(patterns[1] if patterns[1] != 'N/A' else time.time()) + ) message.append('-------------------------------------------') message.append(f'Executable: {app_binary}') - message.append(f'Timestamp: {str(datetime.fromtimestamp(float(patterns[1])))}') + message.append(f'Timestamp: {str(timestamp)}') message.append(f'Process ID: {patterns[2]}') message.append(f'Signal Number: {patterns[3]}') message.append('-------------------------------------------') diff --git a/test/gui/shared/steps/file_context.py b/test/gui/shared/steps/file_context.py index 49d2eb0d66..3ca54a682a 100644 --- a/test/gui/shared/steps/file_context.py +++ b/test/gui/shared/steps/file_context.py @@ -70,14 +70,14 @@ def write_file(resource, content): def wait_and_write_file(path, content): - listen_sync_status_for_item(get_resource_path(path), 'FILE') wait_for_client_to_be_ready() + listen_sync_status_for_item(get_resource_path(path), 'FILE') write_file(path, content) def wait_and_try_to_write_file(resource, content): - listen_sync_status_for_item(get_resource_path(resource), 'FILE') wait_for_client_to_be_ready() + listen_sync_status_for_item(get_resource_path(resource), 'FILE') try: write_file(resource, content) except: @@ -112,8 +112,8 @@ def copy_resource(resource_type, source, destination, from_files_for_upload=Fals if source_dir == destination_dir and destination_dir != '/': destination_dir = add_copy_suffix(source, resource_type) - listen_sync_status_for_item(destination_dir, resource_type) wait_for_client_to_be_ready() + listen_sync_status_for_item(destination_dir, resource_type) if resource_type == 'folder': return shutil.copytree(source_dir, destination_dir) return shutil.copy2(source_dir, destination_dir) @@ -126,8 +126,8 @@ def move_resource(username, resource_type, source, destination, is_temp_folder=F destination = '' destination = get_resource_path(destination, username) - listen_sync_status_for_item(destination, resource_type) wait_for_client_to_be_ready() + listen_sync_status_for_item(destination, resource_type) shutil.move(source, destination) @@ -265,7 +265,6 @@ def step(context, user, resource, content): @When(r'the user deletes the (file|folder) "([^"]*)"', regexp=True) def step(context, item_type, resource): wait_for_client_to_be_ready() - deleteResource(resource, item_type) From 41b9d103a895b90c9800408f7ae3e4b8dca9f946 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 16 Feb 2026 19:21:13 +0545 Subject: [PATCH 14/75] test(gui): fix gui tests execution time (#808) * fix(ci): upload gui reports first Signed-off-by: Saw-jan * test(gui): update sync pattern Signed-off-by: Saw-jan * test(gui): fix checking sync pattern Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/ui-tests.yaml | 2 +- test/gui/shared/scripts/helpers/SyncHelper.py | 15 ++++++++++++--- test/gui/tst_deleteFilesFolders/test.feature | 11 +++++++---- test/gui/tst_editFiles/test.feature | 6 +++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 52d1d49943..6f350f1663 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -169,8 +169,8 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/ - mc cp -a -r /woodpecker/desktop/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER + - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/ - name: gui-test-reports image: *minio_image diff --git a/test/gui/shared/scripts/helpers/SyncHelper.py b/test/gui/shared/scripts/helpers/SyncHelper.py index cd6680b046..a9b3e92ddb 100644 --- a/test/gui/shared/scripts/helpers/SyncHelper.py +++ b/test/gui/shared/scripts/helpers/SyncHelper.py @@ -43,6 +43,8 @@ SYNC_STATUS = { 'SYNC': 'STATUS:SYNC', # sync in progress 'OK': 'STATUS:OK', # sync completed + 'OKAL': 'STATUS:OK+AL', # sync completed (Always Local) + 'OKOO': 'STATUS:OK+OO', # sync completed (Online Only) 'ERROR': 'STATUS:ERROR', # sync error 'IGNORE': 'STATUS:IGNORE', # sync ignored 'NOP': 'STATUS:NOP', # not in sync yet @@ -57,13 +59,13 @@ 'initial': [ # when adding account via New Account wizard [ + SYNC_STATUS['NOP'], SYNC_STATUS['REGISTER'], SYNC_STATUS['UPDATE'], - SYNC_STATUS['UPDATE'], - SYNC_STATUS['UPDATE'], ], # when syncing empty account (hidden files are ignored) [SYNC_STATUS['UPDATE'], SYNC_STATUS['OK']], + [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']], # when syncing an account that has some files/folders [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], ], @@ -83,6 +85,13 @@ SYNC_STATUS['OK'], SYNC_STATUS['UPDATE'], ], + # used for local resource creation and deletion + [ + SYNC_STATUS['OKAL'], + SYNC_STATUS['OK'], + SYNC_STATUS['OK'], + SYNC_STATUS['UPDATE'], + ], ], 'single_synced': [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], 'error': [SYNC_STATUS['ERROR']], @@ -194,7 +203,7 @@ def listen_sync_status_for_item(item, resource_type='FOLDER'): if (resource_type := resource_type.upper()) not in ('FILE', 'FOLDER'): raise ValueError('resource_type must be "FILE" or "FOLDER"') socket_connect = get_socket_connection() - item = item.rstrip('\\') + item = item.rstrip('\\').rstrip('/') socket_connect.sendCommand(f'RETRIEVE_{resource_type}_STATUS:{item}\n') diff --git a/test/gui/tst_deleteFilesFolders/test.feature b/test/gui/tst_deleteFilesFolders/test.feature index 7d84e15a70..24a6dd6fba 100644 --- a/test/gui/tst_deleteFilesFolders/test.feature +++ b/test/gui/tst_deleteFilesFolders/test.feature @@ -66,12 +66,15 @@ Feature: deleting files and folders And as "Alice" file "textfile2.txt" should exist in the server - Scenario: Create and delete a file with special characters in the filename + Scenario Outline: Create and delete a file with special characters Given user "Alice" has set up a client with default settings - When user "Alice" creates a file "~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️‍🔥.txt" with the following content inside the sync folder + When user "Alice" creates a file "" with the following content inside the sync folder """ special characters """ - And the user deletes the file "~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️‍🔥.txt" + And the user deletes the file "" And the user waits for the files to sync - Then as "Alice" file "~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️‍🔥.txt" should not exist in the server + Then as "Alice" file "" should not exist in the server + Examples: + | fileName | + | ~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️‍🔥.txt | diff --git a/test/gui/tst_editFiles/test.feature b/test/gui/tst_editFiles/test.feature index a735cbc97f..d76454cb4f 100644 --- a/test/gui/tst_editFiles/test.feature +++ b/test/gui/tst_editFiles/test.feature @@ -31,11 +31,11 @@ Feature: edit files Given user "Alice" has set up a client with default settings And the user has copied file "" from outside the sync folder to "/" in the sync folder When the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for file "" to be synced + And the user waits for the files to sync And the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for file "" to be synced + And the user waits for the files to sync And the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for file "" to be synced + And the user waits for the files to sync Then as "Alice" the content of file "" in the server should match the content of local file "" Examples: | initialFile | updateFile1 | updateFile2 | updateFile3 | From e72b23caaad37498e541f1df54ea1ca0b57247cb Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 19 Feb 2026 10:36:23 +0545 Subject: [PATCH 15/75] ci: separate cache locations Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 4 ++-- .woodpecker/cache-python.yaml | 6 +++--- .woodpecker/purge-cache.yaml | 7 +++++-- .woodpecker/ui-tests.yaml | 10 +++++----- test/gui/woodpecker/gui_test_reports.sh | 2 +- test/gui/woodpecker/script.sh | 2 +- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index e778bd5f13..0147b9d259 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -46,5 +46,5 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop-build/${CI_COMMIT_SHA}/ - - mc ls --recursive s3/$CACHE_BUCKET/desktop-build + - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop/bin-build/${CI_COMMIT_SHA}/ + - mc ls --recursive s3/$CACHE_BUCKET/desktop/bin-build diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index 0069edd430..16e78e9ded 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -35,7 +35,7 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc ls s3/$CACHE_BUCKET/desktop-build + - mc ls s3/$CACHE_BUCKET/desktop/python-cache/ - bash test/gui/woodpecker/script.sh check_python_cache - name: install-python-modules @@ -58,5 +58,5 @@ steps: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -r -a /woodpecker/desktop/python-cache*.tar.gz s3/$CACHE_BUCKET/desktop-build/ - - mc ls s3/$CACHE_BUCKET/desktop-build + - mc cp -r -a /woodpecker/desktop/python-cache*.tar.gz s3/$CACHE_BUCKET/desktop/python-cache/ + - mc ls s3/$CACHE_BUCKET/desktop/python-cache/ diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index 5a1ffb0688..9b9cd31266 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -24,8 +24,11 @@ skip_clone: true matrix: include: - JOB_NAME: purge-desktop-build - PURGE_PATH: desktop-build/ + PURGE_PATH: desktop/bin-build/ TTL: 1d + - JOB_NAME: purge-python-cache + PURGE_PATH: desktop/python-cache/ + TTL: 14d - JOB_NAME: purge-browsers-cache PURGE_PATH: web/browsers-cache/ TTL: 14d @@ -33,7 +36,7 @@ matrix: PURGE_PATH: opencloud-build/ TTL: 1d - JOB_NAME: purge-logs - PURGE_PATH: desktop + PURGE_PATH: desktop/testlogs/ USE_PUBLIC_BUCKET: true TTL: 7d diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 6f350f1663..e737f6949e 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -49,7 +49,7 @@ steps: commands: - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a s3/$CACHE_BUCKET/desktop-build/python-cache-$requirements_sha.tar.gz /woodpecker/desktop + - mc cp -a s3/$CACHE_BUCKET/desktop/python-cache/python-cache-$requirements_sha.tar.gz /woodpecker/desktop - name: install-python-modules image: *squish_image @@ -116,7 +116,7 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r s3/$CACHE_BUCKET/desktop-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build + - mc cp -a -r s3/$CACHE_BUCKET/desktop/bin-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build - ls -lh /woodpecker/desktop/build/bin - name: create-extra-directories @@ -169,8 +169,8 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER - - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/ + - mc cp -a -r /woodpecker/desktop/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER + - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/ - name: gui-test-reports image: *minio_image @@ -192,5 +192,5 @@ steps: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - cd /woodpecker/desktop/test/gui/clientLog/ - echo "To download the logs, access the following links:" - - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/logs/) + - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/) - "for f in $logs; do echo \"$MC_HOST/$f \n \" | cut -d '/' -f1-3,5-99; done" diff --git a/test/gui/woodpecker/gui_test_reports.sh b/test/gui/woodpecker/gui_test_reports.sh index 4af7fbe98a..e429ee2b1a 100644 --- a/test/gui/woodpecker/gui_test_reports.sh +++ b/test/gui/woodpecker/gui_test_reports.sh @@ -1,6 +1,6 @@ #!/bin/bash -REPORT_PATH="$PUBLIC_BUCKET/desktop/$CI_PIPELINE_NUMBER/guiReportUpload" +REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/guiReportUpload" REPORT_URL="$MC_HOST/$REPORT_PATH" echo "" diff --git a/test/gui/woodpecker/script.sh b/test/gui/woodpecker/script.sh index fa191257d8..1b7eaeb750 100644 --- a/test/gui/woodpecker/script.sh +++ b/test/gui/woodpecker/script.sh @@ -65,7 +65,7 @@ check_browsers_cache() { check_python_cache() { requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - python_cache=$(mc find s3/$CACHE_BUCKET/desktop-build/python-cache-$requirements_sha.tar.gz 2>&1 | grep 'Object does not exist') + python_cache=$(mc find s3/$CACHE_BUCKET/desktop/python-cache/python-cache-$requirements_sha.tar.gz 2>&1 | grep 'Object does not exist') if [[ "$python_cache" != "" ]] then From caa44385a060cc0a299e48b9dbb1e098cd7a6b94 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 19 Feb 2026 10:38:46 +0545 Subject: [PATCH 16/75] ci: purge cache after executing test workflow Signed-off-by: Saw-jan --- .woodpecker/purge-cache.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index 9b9cd31266..4314b6938d 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -19,6 +19,13 @@ when: - event: cron cron: nightly* +depends_on: + - ui-tests + +runs_on: + - success + - failure + skip_clone: true matrix: From 181cc6b5390aa26609e20883f61121c009bebb04 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Fri, 20 Feb 2026 09:39:06 +0545 Subject: [PATCH 17/75] ci: rename client build cache dir Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 4 ++-- .woodpecker/ui-tests.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index 0147b9d259..eabfac6628 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -46,5 +46,5 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop/bin-build/${CI_COMMIT_SHA}/ - - mc ls --recursive s3/$CACHE_BUCKET/desktop/bin-build + - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ + - mc ls --recursive s3/$CACHE_BUCKET/desktop/client-build diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index e737f6949e..194526e6f0 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -116,7 +116,7 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r s3/$CACHE_BUCKET/desktop/bin-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build + - mc cp -a -r s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build - ls -lh /woodpecker/desktop/build/bin - name: create-extra-directories From a472f8f1d47de9452cca6239d615f801cc233da9 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Mon, 23 Feb 2026 12:52:16 +0545 Subject: [PATCH 18/75] chore: bump opencloud commit hash Signed-off-by: Saw-jan --- .woodpecker.env | 2 +- .woodpecker/purge-cache.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.env b/.woodpecker.env index 809087a496..bc41060c04 100644 --- a/.woodpecker.env +++ b/.woodpecker.env @@ -1,3 +1,3 @@ # The version of OpenCloud to use in pipelines -OPENCLOUD_COMMITID=0016085933663e30256d0cc521f16844d84a2f71 +OPENCLOUD_COMMITID=2a2e882a59a19307dd99979c15d199f72450688f OPENCLOUD_BRANCH=main diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index 4314b6938d..f3eb3daee1 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -31,7 +31,7 @@ skip_clone: true matrix: include: - JOB_NAME: purge-desktop-build - PURGE_PATH: desktop/bin-build/ + PURGE_PATH: desktop/client-build/ TTL: 1d - JOB_NAME: purge-python-cache PURGE_PATH: desktop/python-cache/ From c54d34e3ccefd911b172dc3dcb50d7525cb9f20e Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 23 Feb 2026 17:40:01 +0545 Subject: [PATCH 19/75] ci: use latest rolling opencloud server version (#820) Signed-off-by: Saw-jan --- .woodpecker.env | 3 -- .woodpecker/cache-opencloud.yaml | 75 -------------------------------- .woodpecker/ui-tests.yaml | 18 ++------ test/gui/woodpecker/script.sh | 33 +------------- 4 files changed, 4 insertions(+), 125 deletions(-) delete mode 100644 .woodpecker.env delete mode 100644 .woodpecker/cache-opencloud.yaml diff --git a/.woodpecker.env b/.woodpecker.env deleted file mode 100644 index bc41060c04..0000000000 --- a/.woodpecker.env +++ /dev/null @@ -1,3 +0,0 @@ -# The version of OpenCloud to use in pipelines -OPENCLOUD_COMMITID=2a2e882a59a19307dd99979c15d199f72450688f -OPENCLOUD_BRANCH=main diff --git a/.woodpecker/cache-opencloud.yaml b/.woodpecker/cache-opencloud.yaml deleted file mode 100644 index 6c384a4264..0000000000 --- a/.woodpecker/cache-opencloud.yaml +++ /dev/null @@ -1,75 +0,0 @@ -variables: - - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - - &minio_environment - AWS_ACCESS_KEY_ID: - from_secret: cache_s3_access_key - AWS_SECRET_ACCESS_KEY: - from_secret: cache_s3_secret_key - CACHE_BUCKET: - from_secret: cache_s3_bucket - MC_HOST: 'https://s3.ci.opencloud.eu' - -when: - - branch: - - main - - stable-* - event: - - push - - manual - - event: pull_request - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: tag - - event: cron - cron: nightly* - -skip_clone: true - -steps: - - name: check-for-existing-cache - image: *minio_image - environment: - <<: *minio_environment - commands: - - curl -o .woodpecker.env https://raw.githubusercontent.com/opencloud-eu/desktop/$CI_COMMIT_SHA/.woodpecker.env - - curl -o script.sh https://raw.githubusercontent.com/opencloud-eu/desktop/$CI_COMMIT_SHA/test/gui/woodpecker/script.sh - - . ./.woodpecker.env - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc ls --recursive s3/$CACHE_BUCKET/opencloud-build - - bash script.sh check_opencloud_cache - - - name: clone-opencloud - image: docker.io/golang:1.24 - commands: - - . ./.woodpecker.env - - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - - git clone -b $OPENCLOUD_BRANCH --single-branch https://github.com/opencloud-eu/opencloud.git repo_opencloud - - cd repo_opencloud - - git checkout $OPENCLOUD_COMMITID - - - name: generate-opencloud - image: owncloudci/nodejs:20 - commands: - - . ./.woodpecker.env - - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - - cd repo_opencloud - - for i in $(seq 3); do make node-generate-prod && break || sleep 1; done - - - name: build-opencloud - image: docker.io/golang:1.24 - commands: - - . ./.woodpecker.env - - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - - cd repo_opencloud - - for i in $(seq 3); do make -C opencloud build && break || sleep 1; done - - - name: upload-opencloud-cache - image: *minio_image - environment: - <<: *minio_environment - commands: - - . ./.woodpecker.env - - if $OPENCLOUD_CACHE_FOUND; then exit 0; fi - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a repo_opencloud/opencloud/bin/opencloud s3/$CACHE_BUCKET/opencloud-build/$OPENCLOUD_COMMITID/ - - mc ls --recursive s3/$CACHE_BUCKET/opencloud-build diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 194526e6f0..50d866fd6d 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -25,7 +25,6 @@ when: cron: nightly* depends_on: - - cache-opencloud - cache-pnpm - build @@ -74,17 +73,8 @@ steps: commands: - tar -xvf /woodpecker/desktop/playwright-browsers.tar.gz -C . - - name: restore-opencloud-cache - image: *minio_image - environment: - <<: *minio_environment - commands: - - . ./.woodpecker.env - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -r -a s3/$CACHE_BUCKET/opencloud-build/$OPENCLOUD_COMMITID/opencloud /woodpecker/desktop - - name: opencloud - image: docker.io/golang:1.24 + image: quay.io/opencloudeu/opencloud-rolling:latest detach: true environment: FRONTEND_SEARCH_MIN_LENGTH: '2' @@ -100,10 +90,8 @@ steps: PROXY_ENABLE_BASIC_AUTH: true WEB_UI_CONFIG_FILE: /woodpecker/desktop/test/gui/woodpecker/config-opencloud.json commands: - - mkdir -p /srv/app/tmp/opencloud/opencloud/data/ - - mkdir -p /srv/app/tmp/opencloud/storage/users/ - - ./opencloud init - - ./opencloud server + - opencloud init + - opencloud server - name: wait-for-opencloud image: owncloudci/alpine:latest diff --git a/test/gui/woodpecker/script.sh b/test/gui/woodpecker/script.sh index 1b7eaeb750..d983f6c539 100644 --- a/test/gui/woodpecker/script.sh +++ b/test/gui/woodpecker/script.sh @@ -1,35 +1,6 @@ #!/bin/bash -source .woodpecker.env - -# Function to get the latest OpenCloud commit ID -get_latest_opencloud_commit_id() { - echo "Getting latest commit ID for branch: $OPENCLOUD_BRANCH" - latest_commit_id=$(git ls-remote https://github.com/opencloud-eu/opencloud.git "refs/heads/$OPENCLOUD_BRANCH" | cut -f 1) - - # Update the OPENCLOUD in the .woodpecker.env file - env_file="./.woodpecker.env" - sed -i "s/^OPENCLOUD_COMMITID=.*/OPENCLOUD_COMMITID=$latest_commit_id/" "$env_file" - - echo "Updated .woodpecker.env with latest commit ID: $latest_commit_id" - cat $env_file - exit 0 -} - -# Function to check if the cache exists for the given commit ID -check_opencloud_cache() { - echo "Checking OpenCloud cache for commit ID: $OPENCLOUD_COMMITID" - opencloud_cache=$(mc find s3/$CACHE_BUCKET/opencloud-build/$OPENCLOUD_COMMITID/opencloud 2>&1 | grep 'Object does not exist') - - if [[ "$opencloud_cache" != "" ]] - then - echo "$OPENCLOUD_COMMITID doesn't exist in cache." - ENV="OPENCLOUD_CACHE_FOUND=false\n" - else - echo "$OPENCLOUD_COMMITID found in cache." - ENV="OPENCLOUD_CACHE_FOUND=true\n" - fi -} +touch .woodpecker.env # get playwright version from package.json get_playwright_version() { @@ -80,8 +51,6 @@ check_python_cache() { if [[ "$1" == "" ]]; then echo "Usage: $0 [COMMAND]" echo "Commands:" - echo -e " get_latest_opencloud_commit_id \t get the latest OpenCloud commit ID" - echo -e " check_opencloud_cache \t\t check if the cache exists for the given commit ID" echo -e " get_playwright_version \t get the playwright version from package.json" echo -e " check_browsers_cache \t check if the browsers cache exists for the given playwright version" echo -e " check_python_cache \t check if a cache for the current requirements.txt exists" From 5e0ad2d9edb3473cc492000dd2f8547a19a578a4 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Tue, 24 Feb 2026 11:31:26 +0545 Subject: [PATCH 20/75] test: fix VFS test scenarios (#813) Signed-off-by: Saw-jan --- test/gui/shared/steps/file_context.py | 53 ++++++++++++++++++++------- test/gui/shared/steps/vfs_context.py | 2 +- test/gui/tst_syncing/test.feature | 8 ++-- test/gui/tst_vfs/test.feature | 39 +++++++++++++------- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/test/gui/shared/steps/file_context.py b/test/gui/shared/steps/file_context.py index 3ca54a682a..20e94a6998 100644 --- a/test/gui/shared/steps/file_context.py +++ b/test/gui/shared/steps/file_context.py @@ -9,7 +9,7 @@ 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 -from helpers.ConfigHelper import get_config +from helpers.ConfigHelper import get_config, is_windows from helpers.FilesHelper import ( build_conflicted_regex, sanitize_path, @@ -97,26 +97,27 @@ def extract_zip(zip_file_path, destination_dir): def add_copy_suffix(resource_path, resource_type): + suffix = ' (Copy)' if resource_type == 'file': source_dir = resource_path.rsplit('.', 1) - return source_dir[0] + ' - Copy.' + source_dir[-1] - return resource_path + ' - Copy' + return source_dir[0] + suffix + '.' + source_dir[-1] + return resource_path + suffix def copy_resource(resource_type, source, destination, from_files_for_upload=False): if from_files_for_upload: - source_dir = get_file_for_upload(source) + source = get_file_for_upload(source) else: - source_dir = get_resource_path(source) - destination_dir = get_resource_path(destination) - if source_dir == destination_dir and destination_dir != '/': - destination_dir = add_copy_suffix(source, resource_type) + source = get_resource_path(source) + destination = get_resource_path(destination) + if source == destination and destination != '/': + destination = add_copy_suffix(source, resource_type) wait_for_client_to_be_ready() - listen_sync_status_for_item(destination_dir, resource_type) + listen_sync_status_for_item(destination, resource_type) if resource_type == 'folder': - return shutil.copytree(source_dir, destination_dir) - return shutil.copy2(source_dir, destination_dir) + return shutil.copytree(source, destination) + return shutil.copy2(source, destination) def move_resource(username, resource_type, source, destination, is_temp_folder=False): @@ -164,9 +165,14 @@ def step(context, _, filename, filesize): create_file_with_size(filename, filesize) -@When(r'the user copies the (file|folder) "([^"]*)" to "([^"]*)"', regexp=True) -def step(context, resource_type, file_name, destination): - copy_resource(resource_type, file_name, destination, False) +@When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) +def step(context, resource_type, resource_name, destination_dir): + copy_resource(resource_type, resource_name, destination_dir, False) + + +@When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) +def step(context, resource_type, resource_name): + copy_resource(resource_type, resource_name, resource_name, False) @When(r'the user renames a (?:file|folder) "([^"]*)" to "([^"]*)"', regexp=True) @@ -298,6 +304,16 @@ def step(context, file_number, file_size, folder_name): ) +@When( + r'user "([^"]*)" reads the content of file "([^"]*)"', + regexp=True, +) +def step(context, username, file): + file_path = get_resource_path(file, username) + with open(file_path, 'r') as f: + f.read() + + @When( r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', regexp=True, @@ -307,6 +323,15 @@ def step(context, username, resource_type, resource_name): move_resource(username, resource_type, source_dir, '/', True) +@When( + r'user "([^"]*)" moves (folder|file) "([^"]*)" to the temp folder', + regexp=True, +) +def step(context, username, resource_type, resource_name): + destination= join(get_config('tempFolderPath'), resource_name) + move_resource(username, resource_type, resource_name, destination) + + @When( r'user "([^"]*)" moves (file|folder) "([^"]*)" to "([^"]*)" in the sync folder', regexp=True, diff --git a/test/gui/shared/steps/vfs_context.py b/test/gui/shared/steps/vfs_context.py index febd6c5214..9655a7079b 100644 --- a/test/gui/shared/steps/vfs_context.py +++ b/test/gui/shared/steps/vfs_context.py @@ -2,7 +2,7 @@ from helpers.SetupClientHelper import get_resource_path -@Then('the placeholder of file "|any|" should exist on the file system') +@Then('the placeholder file "|any|" should exist on the file system') def step(context, file_name): resource_path = get_resource_path(file_name) size_on_disk = get_file_size_on_disk(resource_path) diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index aebe81a115..3521c1ca57 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -243,12 +243,12 @@ Feature: Syncing files """ test content """ - And the user copies the folder "original" to "copied" - And the user waits for folder "copied" to be synced + And the user copies folder "original" into the same directory + And the user waits for folder "original (Copy)" to be synced Then as "Alice" folder "original" should exist in the server And as "Alice" the file "original/localFile.txt" should have the content "test content" in the server - And as "Alice" folder "copied" should exist in the server - And as "Alice" the file "copied/localFile.txt" should have the content "test content" in the server + And as "Alice" folder "original (Copy)" should exist in the server + And as "Alice" the file "original (Copy)/localFile.txt" should have the content "test content" in the server @issue-9281 Scenario: Verify that you can create a subfolder with long name(~220 characters) diff --git a/test/gui/tst_vfs/test.feature b/test/gui/tst_vfs/test.feature index 6961f86634..c4615774a1 100644 --- a/test/gui/tst_vfs/test.feature +++ b/test/gui/tst_vfs/test.feature @@ -1,37 +1,50 @@ @skipOnLinux -Feature: Enable/disable virtual file support +Feature: VFS support As a user - I want to enable virtual file support - So that I can synchronize virtual files with local folder + I want to sync files with vfs + So that I can decide which files to download - Scenario: Copy and paste virtual file + Scenario: Default VFS sync + Given user "Alice" has been created in the server with default attributes + And user "Alice" has uploaded file with content "openCloud" to "testFile.txt" in the server + And user "Alice" has created folder "parent" in the server + And user "Alice" has uploaded file with content "some contents" to "parent/lorem.txt" in the server + And user "Alice" has set up a client with default settings + Then the placeholder file "testFile.txt" should exist on the file system + And the placeholder file "parent/lorem.txt" should exist on the file system + When user "Alice" reads the content of file "parent/lorem.txt" + Then the file "parent/lorem.txt" should be downloaded + And the placeholder file "testFile.txt" should exist on the file system + + + Scenario: Copy placeholder file Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "sample file" to "sampleFile.txt" in the server And user "Alice" has uploaded file with content "lorem file" to "lorem.txt" in the server And user "Alice" has uploaded file with content "test file" to "testFile.txt" in the server And user "Alice" has created folder "Folder" in the server And user "Alice" has set up a client with default settings - Then the placeholder of file "lorem.txt" should exist on the file system - And the placeholder of file "sampleFile.txt" should exist on the file system - And the placeholder of file "testFile.txt" should exist on the file system + Then the placeholder file "lorem.txt" should exist on the file system + And the placeholder file "sampleFile.txt" should exist on the file system + And the placeholder file "testFile.txt" should exist on the file system When user "Alice" copies file "sampleFile.txt" to temp folder - And the user copies the file "lorem.txt" to "Folder" - And the user copies the file "testFile.txt" to "testFile.txt" + And the user copies file "lorem.txt" into folder "Folder" + And the user copies file "testFile.txt" into the same directory And the user waits for file "Folder/lorem.txt" to be synced Then the file "sampleFile.txt" should be downloaded And the file "Folder/lorem.txt" should be downloaded And the file "lorem.txt" should be downloaded And the file "testFile.txt" should be downloaded - And the file "testFile - Copy.txt" should be downloaded + And the file "testFile (Copy).txt" should be downloaded And as "Alice" file "Folder/lorem.txt" should exist in the server And as "Alice" file "lorem.txt" should exist in the server And as "Alice" file "sampleFile.txt" should exist in the server And as "Alice" file "testFile.txt" should exist in the server - And as "Alice" file "testFile - Copy.txt" should exist in the server + And as "Alice" file "testFile (Copy).txt" should exist in the server - Scenario: Move virtual file + Scenario: Move placeholder file Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "lorem file" to "lorem.txt" in the server And user "Alice" has uploaded file with content "some contents" to "sampleFile.txt" in the server @@ -40,7 +53,7 @@ Feature: Enable/disable virtual file support When user "Alice" moves file "lorem.txt" to "Folder" in the sync folder And user "Alice" moves file "sampleFile.txt" to the temp folder And the user waits for file "Folder/lorem.txt" to be synced - Then the placeholder of file "Folder/lorem.txt" should exist on the file system + Then the placeholder file "Folder/lorem.txt" should exist on the file system And as "Alice" file "Folder/lorem.txt" should exist in the server And as "Alice" file "lorem.txt" should not exist in the server And as "Alice" file "sampleFile.txt" should not exist in the server From a3ffae804325b3e5d15d7613299f1a538a8e8d8f Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Tue, 3 Mar 2026 11:48:36 +0545 Subject: [PATCH 21/75] ci: run gui tests only when related files are changed (#826) Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 18 ++++++++++++++++++ .woodpecker/cache-pnpm.yaml | 18 ++++++++++++++++++ .woodpecker/cache-python.yaml | 18 ++++++++++++++++++ .woodpecker/ui-tests.yaml | 3 ++- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index eabfac6628..be0dc40d53 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -9,6 +9,20 @@ variables: CACHE_BUCKET: from_secret: cache_s3_bucket MC_HOST: 'https://s3.ci.opencloud.eu' + - &trigger_path + include: + - test/gui/** + - src/** + - cmake/** + - THEME.cmake + - VERSION.cmake + - CMakeLists.txt + - OPENCLOUD.cmake + - .woodpecker/** + exclude: + - .woodpecker/ready-release-go.yaml + - .woodpecker/translation.yaml + - test/gui/manual-test-plan when: - branch: @@ -17,7 +31,11 @@ when: event: - push - manual + path: + <<: *trigger_path - event: pull_request + path: + <<: *trigger_path evaluate: | !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - event: tag diff --git a/.woodpecker/cache-pnpm.yaml b/.woodpecker/cache-pnpm.yaml index b8747a455f..356ec577ac 100644 --- a/.woodpecker/cache-pnpm.yaml +++ b/.woodpecker/cache-pnpm.yaml @@ -8,6 +8,20 @@ variables: CACHE_BUCKET: from_secret: cache_s3_bucket MC_HOST: 'https://s3.ci.opencloud.eu' + - &trigger_path + include: + - test/gui/** + - src/** + - cmake/** + - THEME.cmake + - VERSION.cmake + - CMakeLists.txt + - OPENCLOUD.cmake + - .woodpecker/** + exclude: + - .woodpecker/ready-release-go.yaml + - .woodpecker/translation.yaml + - test/gui/manual-test-plan when: - branch: @@ -16,8 +30,12 @@ when: event: - push - manual + path: + <<: *trigger_path - event: tag - event: pull_request + path: + <<: *trigger_path evaluate: | !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - event: cron diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index 16e78e9ded..761767065e 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -9,6 +9,20 @@ variables: CACHE_BUCKET: from_secret: cache_s3_bucket MC_HOST: 'https://s3.ci.opencloud.eu' + - &trigger_path + include: + - test/gui/** + - src/** + - cmake/** + - THEME.cmake + - VERSION.cmake + - CMakeLists.txt + - OPENCLOUD.cmake + - .woodpecker/** + exclude: + - .woodpecker/ready-release-go.yaml + - .woodpecker/translation.yaml + - test/gui/manual-test-plan when: - branch: @@ -17,8 +31,12 @@ when: event: - push - manual + path: + <<: *trigger_path - event: tag - event: pull_request + path: + <<: *trigger_path evaluate: | !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - event: cron diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 50d866fd6d..0719eed106 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -25,8 +25,9 @@ when: cron: nightly* depends_on: - - cache-pnpm - build + - cache-pnpm + - cache-python workspace: base: /woodpecker From 1373ee064a742aa1317fd10633b8b9ae02cd7ecd Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 11 Mar 2026 17:15:06 +0545 Subject: [PATCH 22/75] test(gui): add vfs tests (#815) * test: add vfs test for file-explorer actions Signed-off-by: Saw-jan * test: add hydration test for folders Signed-off-by: Saw-jan * ci: remove opencloud cache purge from matrix Signed-off-by: Saw-jan * test: check sync status Signed-off-by: Saw-jan * test: use display name for file actions Signed-off-by: Saw-jan * test: use file attributes to determine file status Signed-off-by: Saw-jan * ci: windows only implementation Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/purge-cache.yaml | 3 - .../gui/shared/scripts/helpers/FilesHelper.py | 14 -- test/gui/shared/scripts/helpers/SyncHelper.py | 23 ++++ .../shared/scripts/helpers/VFSFileHelper.py | 125 ++++++++++++++++++ test/gui/shared/steps/vfs_context.py | 22 ++- test/gui/tst_syncing/test.feature | 1 + test/gui/tst_vfs/test.feature | 115 ++++++++++++++++ 7 files changed, 274 insertions(+), 29 deletions(-) create mode 100644 test/gui/shared/scripts/helpers/VFSFileHelper.py diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index f3eb3daee1..a7bac0bfc9 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -39,9 +39,6 @@ matrix: - JOB_NAME: purge-browsers-cache PURGE_PATH: web/browsers-cache/ TTL: 14d - - JOB_NAME: purge-opencloud - PURGE_PATH: opencloud-build/ - TTL: 1d - JOB_NAME: purge-logs PURGE_PATH: desktop/testlogs/ USE_PUBLIC_BUCKET: true diff --git a/test/gui/shared/scripts/helpers/FilesHelper.py b/test/gui/shared/scripts/helpers/FilesHelper.py index 7fac7485f7..a3ad0cdbf2 100644 --- a/test/gui/shared/scripts/helpers/FilesHelper.py +++ b/test/gui/shared/scripts/helpers/FilesHelper.py @@ -1,6 +1,5 @@ import os import re -import ctypes import shutil from pathlib import Path @@ -8,8 +7,6 @@ from docx import Document from pptx import Presentation from openpyxl import load_workbook - -import squish from helpers.ConfigHelper import is_windows, get_config @@ -99,17 +96,6 @@ def get_size_in_bytes(size): raise ValueError("Invalid size: " + size) -def get_file_size_on_disk(resource_path): - if is_windows(): - timeout = get_config('maxSyncTimeout') * 1000 - squish.waitFor(lambda: os.path.exists(resource_path), timeout) - file_size_high = ctypes.c_ulonglong(0) - return ctypes.windll.kernel32.GetCompressedFileSizeW( - ctypes.c_wchar_p(resource_path), ctypes.pointer(file_size_high) - ) - raise OSError("'get_file_size_on_disk' function is only supported for Windows OS.") - - def get_file_size(resource_path): return os.stat(resource_path).st_size diff --git a/test/gui/shared/scripts/helpers/SyncHelper.py b/test/gui/shared/scripts/helpers/SyncHelper.py index a9b3e92ddb..98c751a10f 100644 --- a/test/gui/shared/scripts/helpers/SyncHelper.py +++ b/test/gui/shared/scripts/helpers/SyncHelper.py @@ -333,3 +333,26 @@ def wait_for_client_to_be_ready(): def clear_waited_after_sync(): global WAITED_AFTER_SYNC WAITED_AFTER_SYNC = False + + +def perform_file_explorer_vfs_action(resource_path, action): + if action == 'Free up space': + make_online_only(resource_path) + elif action == 'Always keep on this device': + make_available_locally(resource_path) + else: + raise ValueError( + f'Invalid file explorer action: {action}' + ) + + +def make_online_only(resource_path): + socket_connect = get_socket_connection() + resource_path = resource_path.rstrip('\\').rstrip('/') + socket_connect.sendCommand(f'MAKE_ONLINE_ONLY:{resource_path}\n') + + +def make_available_locally(resource_path): + socket_connect = get_socket_connection() + resource_path = resource_path.rstrip('\\').rstrip('/') + socket_connect.sendCommand(f'MAKE_AVAILABLE_LOCALLY:{resource_path}\n') diff --git a/test/gui/shared/scripts/helpers/VFSFileHelper.py b/test/gui/shared/scripts/helpers/VFSFileHelper.py new file mode 100644 index 0000000000..8a106240ec --- /dev/null +++ b/test/gui/shared/scripts/helpers/VFSFileHelper.py @@ -0,0 +1,125 @@ +import inspect +import ctypes +from ctypes import wintypes +from enum import IntFlag, Enum, unique + +from helpers.ConfigHelper import is_windows + + +error_message = "'%s' function is only supported in Windows OS." + +# ========================== +# Structures +# ========================== +class FILETIME(ctypes.Structure): + _fields_ = [ + ("dwLowDateTime", wintypes.DWORD), + ("dwHighDateTime", wintypes.DWORD), + ] + +class WIN32_FILE_ATTRIBUTE_DATA(ctypes.Structure): + _fields_ = [ + ("dwFileAttributes", wintypes.DWORD), + ("ftCreationTime", FILETIME), + ("ftLastAccessTime", FILETIME), + ("ftLastWriteTime", FILETIME), + ("nFileSizeHigh", wintypes.DWORD), + ("nFileSizeLow", wintypes.DWORD), + ] + +# Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants +@unique +class FileAttributeConstants(IntFlag): + __str__ = Enum.__str__ + FILE_ATTRIBUTE_PINNED = 0x00080000 + FILE_ATTRIBUTE_UNPINNED = 0x00100000 + FILE_ATTRIBUTE_ARCHIVE = 0x00000020 + +GetFileAttributesExW = None +GetCompressedFileSizeW = None + +if is_windows(): + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + + GetFileAttributesExW = kernel32.GetFileAttributesExW + GetFileAttributesExW.argtypes = [ + wintypes.LPCWSTR, + ctypes.c_int, + ctypes.POINTER(WIN32_FILE_ATTRIBUTE_DATA), + ] + GetFileAttributesExW.restype = wintypes.BOOL + + GetCompressedFileSizeW = kernel32.GetCompressedFileSizeW + GetCompressedFileSizeW.argtypes = [ + wintypes.LPCWSTR, + ctypes.POINTER(wintypes.DWORD), + ] + GetCompressedFileSizeW.restype = wintypes.DWORD + + +def get_file_attributes(path): + if is_windows(): + data = WIN32_FILE_ATTRIBUTE_DATA() + success = GetFileAttributesExW(path, 0, ctypes.byref(data)) + if not success: + raise ctypes.WinError(ctypes.get_last_error()) + attributes = FileAttributeConstants(data.dwFileAttributes) + mask = ( + FileAttributeConstants.FILE_ATTRIBUTE_PINNED | + FileAttributeConstants.FILE_ATTRIBUTE_UNPINNED | + FileAttributeConstants.FILE_ATTRIBUTE_ARCHIVE + ) + return attributes & mask + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def get_compressed_file_size(path): + if is_windows(): + high = wintypes.DWORD(0) + low = GetCompressedFileSizeW(path, ctypes.byref(high)) + + if low == 0xFFFFFFFF: + err = ctypes.get_last_error() + if err != 0: + raise ctypes.WinError(err) + + return (high.value << 32) | low + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def resource_archived(resource_path): + if is_windows(): + return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_ARCHIVE) + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def resource_pinned(resource_path): + if is_windows(): + return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_PINNED) + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def resource_unpinned(resource_path): + if is_windows(): + return bool(get_file_attributes(resource_path) & FileAttributeConstants.FILE_ATTRIBUTE_UNPINNED) + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def is_placeholder_resource(resource_path): + if is_windows(): + size_on_disk = get_compressed_file_size(resource_path) + unpinned = resource_unpinned(resource_path) + pinned = resource_pinned(resource_path) + archived = resource_archived(resource_path) + return (not size_on_disk or unpinned) and not (pinned and archived) + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) + + +def is_file_downloaded(resource_path): + if is_windows(): + size_on_disk = get_compressed_file_size(resource_path) + pinned = resource_pinned(resource_path) + unpinned = resource_unpinned(resource_path) + archived = resource_archived(resource_path) + return size_on_disk and (pinned or archived) and not unpinned + raise OSError(error_message % inspect.currentframe().f_back.f_code.co_name) diff --git a/test/gui/shared/steps/vfs_context.py b/test/gui/shared/steps/vfs_context.py index 9655a7079b..5416d7c507 100644 --- a/test/gui/shared/steps/vfs_context.py +++ b/test/gui/shared/steps/vfs_context.py @@ -1,23 +1,21 @@ -from helpers.FilesHelper import get_file_size_on_disk, get_file_size from helpers.SetupClientHelper import get_resource_path +from helpers.SyncHelper import perform_file_explorer_vfs_action +from helpers.VFSFileHelper import is_placeholder_resource, is_file_downloaded @Then('the placeholder file "|any|" should exist on the file system') def step(context, file_name): resource_path = get_resource_path(file_name) - size_on_disk = get_file_size_on_disk(resource_path) - test.compare( - size_on_disk, 0, f"Size of the placeholder on the disk is: '{size_on_disk}'" - ) + test.compare(is_placeholder_resource(resource_path), True, f"File is a placeholder") @Then('the file "|any|" should be downloaded') def step(context, file_name): resource_path = get_resource_path(file_name) - size_on_disk = get_file_size_on_disk(resource_path) - file_size = get_file_size(resource_path) - test.compare( - size_on_disk, - file_size, - f"Original file size '{file_size}' is not equal to its size on disk '{size_on_disk}'", - ) + test.compare(is_file_downloaded(resource_path), True, f"File is downloaded") + + +@When(r'user "([^"]*)" marks (?:file|folder) "([^"]*)" as "(Free up space|Always keep on this device)" from the file explorer', regexp=True) +def step(context, user, resource, action): + resource_path = get_resource_path(resource, user) + perform_file_explorer_vfs_action(resource_path, action) diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index 3521c1ca57..372eb6522a 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -563,6 +563,7 @@ Feature: Syncing files And the user copies file "simple.pdf" from outside the sync folder to "simple-folder/simple.pdf" in the sync folder And the user overwrites the file "simple-folder/uploaded-lorem.txt" with content "overwrite openCloud test text file" And the user waits for the files to sync + And the user waits for folder "simple-folder/sub-folder" to be synced Then the folder "simple-folder/sub-folder" should exist on the file system And the file "simple-folder/simple.pdf" should exist on the file system And as "Brian" folder "Shares/simple-folder/sub-folder" should exist in the server diff --git a/test/gui/tst_vfs/test.feature b/test/gui/tst_vfs/test.feature index c4615774a1..b8cd3e173b 100644 --- a/test/gui/tst_vfs/test.feature +++ b/test/gui/tst_vfs/test.feature @@ -57,3 +57,118 @@ Feature: VFS support And as "Alice" file "Folder/lorem.txt" should exist in the server And as "Alice" file "lorem.txt" should not exist in the server And as "Alice" file "sampleFile.txt" should not exist in the server + + + Scenario: Hydration and dehydration of files via file explorer + Given user "Alice" has been created in the server with default attributes + And user "Alice" has uploaded file with content "test content" to "testFile.txt" in the server + And user "Alice" has uploaded file with content "test content" to "simple.txt" in the server + And user "Alice" has uploaded file with content "test content" to "large.txt" in the server + And user "Alice" has created folder "parent" in the server + And user "Alice" has uploaded file with content "test content" to "parent/lorem.txt" in the server + And user "Alice" has uploaded file with content "test content" to "parent/epsum.txt" in the server + And user "Alice" has set up a client with default settings + Then the placeholder file "testFile.txt" should exist on the file system + And the placeholder file "simple.txt" should exist on the file system + And the placeholder file "large.txt" should exist on the file system + And the placeholder file "parent/lorem.txt" should exist on the file system + And the placeholder file "parent/epsum.txt" should exist on the file system + + # Hydrate some files by reading the content + When user "Alice" reads the content of file "testFile.txt" + And user "Alice" reads the content of file "parent/lorem.txt" + Then the file "testFile.txt" should be downloaded + And the file "parent/lorem.txt" should be downloaded + And the placeholder file "parent/epsum.txt" should exist on the file system + + # mark files "Always keep on this device" + When user "Alice" marks file "testFile.txt" as "Always keep on this device" from the file explorer + And the user waits for file "testFile.txt" to be synced + Then the file "testFile.txt" should be downloaded + When user "Alice" marks file "simple.txt" as "Always keep on this device" from the file explorer + And the user waits for file "simple.txt" to be synced + Then the file "simple.txt" should be downloaded + And the placeholder file "large.txt" should exist on the file system + + # mark files "Free up space" + When user "Alice" marks file "testFile.txt" as "Free up space" from the file explorer + And the user waits for file "testFile.txt" to be synced + Then the placeholder file "testFile.txt" should exist on the file system + When user "Alice" marks file "parent/lorem.txt" as "Free up space" from the file explorer + And the user waits for file "parent/lorem.txt" to be synced + Then the placeholder file "parent/lorem.txt" should exist on the file system + When user "Alice" marks file "simple.txt" as "Free up space" from the file explorer + And the user waits for file "simple.txt" to be synced + Then the placeholder file "simple.txt" should exist on the file system + + + Scenario: Hydration and dehydration of folders via file explorer + Given user "Alice" has been created in the server with default attributes + And user "Alice" has created folder "testFol" in the server + And user "Alice" has created folder "nested" in the server + And user "Alice" has created folder "nested/subfol1" in the server + And user "Alice" has created folder "nested/subfol1/subfol2" in the server + And user "Alice" has created folder "nested/subfol1/subfol2/subfol3" in the server + And user "Alice" has created folder "nested/subfol1/subfol2/subfol3/subfol4" in the server + And user "Alice" has uploaded file with content "test content" to "simple.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "nested/lorem.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "nested/subfol1/subfile1.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "nested/subfol1/subfol2/subfile2.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "nested/subfol1/subfol2/subfol3/subfile3.txt" in the server + And user "Alice" has uploaded file with content "some contents" to "nested/subfol1/subfol2/subfol3/subfol4/subfile4.txt" in the server + And user "Alice" has set up a client with default settings + Then the placeholder file "simple.txt" should exist on the file system + And the placeholder file "nested/lorem.txt" should exist on the file system + And the placeholder file "nested/subfol1/subfol2/subfol3/subfol4/subfile4.txt" should exist on the file system + + # mark sub folder as "Always keep on this device" + When user "Alice" reads the content of file "nested/subfol1/subfol2/subfile2.txt" + And user "Alice" marks folder "nested/subfol1" as "Always keep on this device" from the file explorer + And the user waits for folder "nested/subfol1" to be synced + Then the file "nested/subfol1/subfile1.txt" should be downloaded + And the file "nested/subfol1/subfol2/subfile2.txt" should be downloaded + And the file "nested/subfol1/subfol2/subfol3/subfile3.txt" should be downloaded + And the file "nested/subfol1/subfol2/subfol3/subfol4/subfile4.txt" should be downloaded + And the placeholder file "nested/lorem.txt" should exist on the file system + + # create local files and folders in "Always keep on this device" folder + When user "Alice" creates a folder "nested/subfol1/subfol2/localFol" inside the sync folder + And user "Alice" creates a file "nested/subfol1/subfol2/local.txt" with the following content inside the sync folder + """ + local file + """ + And the user waits for folder "nested/subfol1/subfol2/localFol" to be synced + And the user waits for file "nested/subfol1/subfol2/local.txt" to be synced + Then the file "nested/subfol1/subfol2/local.txt" should be downloaded + + # create local files and folders in "Free up space" folder + When user "Alice" creates a folder "nested/localFol" inside the sync folder + And user "Alice" creates a file "nested/local.txt" with the following content inside the sync folder + """ + local file + """ + And the user waits for folder "nested/localFol" to be synced + And the user waits for file "nested/local.txt" to be synced + Then the file "nested/local.txt" should be downloaded + + # upload files to "Always keep on this device" folder in the server + When user "Alice" uploads file with content "server content" to "nested/subfol1/subfol2/localFol/fromServer.txt" in the server + And the user waits for file "nested/subfol1/subfol2/localFol/fromServer.txt" to be synced + Then the file "nested/subfol1/subfol2/localFol/fromServer.txt" should be downloaded + + # upload files to "Free up space" folder in the server + When user "Alice" uploads file with content "server content" to "nested/fromServer.txt" in the server + And user "Alice" uploads file with content "server content" to "nested/localFol/fromServer.txt" in the server + And the user waits for file "nested/localFol/fromServer.txt" to be synced + Then the placeholder file "nested/fromServer.txt" should exist on the file system + And the placeholder file "nested/localFol/fromServer.txt" should exist on the file system + + # mark sub folder as "Free up space" + When user "Alice" marks folder "nested/subfol1/subfol2" as "Free up space" from the file explorer + And the user waits for folder "nested/subfol1/subfol2" to be synced + Then the placeholder file "nested/subfol1/subfol2/subfile2.txt" should exist on the file system + And the placeholder file "nested/subfol1/subfol2/local.txt" should exist on the file system + And the placeholder file "nested/subfol1/subfol2/localFol/fromServer.txt" should exist on the file system + And the placeholder file "nested/subfol1/subfol2/subfol3/subfile3.txt" should exist on the file system + And the placeholder file "nested/subfol1/subfol2/subfol3/subfol4/subfile4.txt" should exist on the file system + And the file "nested/subfol1/subfile1.txt" should be downloaded From c90f1c0694f128179a036dbddb56cfa8101640da Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 13 Apr 2026 11:46:10 +0545 Subject: [PATCH 23/75] [POC] test(gui): gui test automation with appium (#857) * test(gui): gui test automation with appium Signed-off-by: Saw-jan * test: pin python modules Signed-off-by: Saw-jan * test: fix sync pattern check Signed-off-by: Saw-jan * fix: replace with print Signed-off-by: Saw-jan * ci: disable squish ci pipeline Signed-off-by: Saw-jan * test: use selector as object Signed-off-by: Saw-jan * test: use app Signed-off-by: Saw-jan * test: fix selector Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 3 + .woodpecker/cache-pnpm.yaml | 3 + .woodpecker/cache-python.yaml | 3 + .woodpecker/ui-tests.yaml | 3 + test/gui/.gitignore | 3 +- test/gui/behave.ini | 8 + test/gui/environment.py | 43 +++ .../add-account/account.feature} | 2 +- .../scripts => }/helpers/ConfigHelper.py | 21 +- .../scripts => }/helpers/FilesHelper.py | 9 +- .../scripts => }/helpers/ReportHelper.py | 2 - .../scripts => }/helpers/SetupClientHelper.py | 74 +++-- .../scripts => }/helpers/SpaceHelper.py | 0 .../scripts => }/helpers/StacktraceHelper.py | 0 .../scripts => }/helpers/SyncHelper.py | 36 ++- .../scripts => }/helpers/UserHelper.py | 0 .../scripts => }/helpers/VFSFileHelper.py | 0 test/gui/helpers/WebUIHelper.py | 20 ++ .../scripts => }/helpers/WinPipeHelper.py | 0 .../scripts => }/helpers/api/http_helper.py | 0 .../scripts => }/helpers/api/provisioning.py | 8 +- .../{shared/scripts => }/helpers/api/utils.py | 0 .../scripts => }/helpers/api/webdav_helper.py | 0 .../pageObjects/AccountConnectionWizard.py | 190 +++++------- .../pageObjects/AccountSetting.py | 0 .../scripts => }/pageObjects/Activity.py | 0 .../scripts => }/pageObjects/EnterPassword.py | 0 .../pageObjects/PublicLinkDialog.py | 0 .../scripts => }/pageObjects/Settings.py | 0 .../pageObjects/SyncConnection.py | 0 .../pageObjects/SyncConnectionWizard.py | 0 .../scripts => }/pageObjects/Toolbar.py | 67 +--- test/gui/requirements.txt | 12 +- .../gui/shared/scripts/helpers/WebUIHelper.py | 37 --- test/gui/steps/account_context.py | 286 ++++++++++++++++++ test/gui/steps/server_context.py | 135 +++++++++ test/gui/tst_addAccount/test.py | 8 - 37 files changed, 690 insertions(+), 283 deletions(-) create mode 100644 test/gui/behave.ini create mode 100644 test/gui/environment.py rename test/gui/{tst_addAccount/test.feature => features/add-account/account.feature} (99%) rename test/gui/{shared/scripts => }/helpers/ConfigHelper.py (90%) rename test/gui/{shared/scripts => }/helpers/FilesHelper.py (96%) rename test/gui/{shared/scripts => }/helpers/ReportHelper.py (98%) rename test/gui/{shared/scripts => }/helpers/SetupClientHelper.py (85%) rename test/gui/{shared/scripts => }/helpers/SpaceHelper.py (100%) rename test/gui/{shared/scripts => }/helpers/StacktraceHelper.py (100%) rename test/gui/{shared/scripts => }/helpers/SyncHelper.py (94%) rename test/gui/{shared/scripts => }/helpers/UserHelper.py (100%) rename test/gui/{shared/scripts => }/helpers/VFSFileHelper.py (100%) create mode 100644 test/gui/helpers/WebUIHelper.py rename test/gui/{shared/scripts => }/helpers/WinPipeHelper.py (100%) rename test/gui/{shared/scripts => }/helpers/api/http_helper.py (100%) rename test/gui/{shared/scripts => }/helpers/api/provisioning.py (99%) rename test/gui/{shared/scripts => }/helpers/api/utils.py (100%) rename test/gui/{shared/scripts => }/helpers/api/webdav_helper.py (100%) rename test/gui/{shared/scripts => }/pageObjects/AccountConnectionWizard.py (54%) rename test/gui/{shared/scripts => }/pageObjects/AccountSetting.py (100%) rename test/gui/{shared/scripts => }/pageObjects/Activity.py (100%) rename test/gui/{shared/scripts => }/pageObjects/EnterPassword.py (100%) rename test/gui/{shared/scripts => }/pageObjects/PublicLinkDialog.py (100%) rename test/gui/{shared/scripts => }/pageObjects/Settings.py (100%) rename test/gui/{shared/scripts => }/pageObjects/SyncConnection.py (100%) rename test/gui/{shared/scripts => }/pageObjects/SyncConnectionWizard.py (100%) rename test/gui/{shared/scripts => }/pageObjects/Toolbar.py (67%) delete mode 100644 test/gui/shared/scripts/helpers/WebUIHelper.py create mode 100644 test/gui/steps/account_context.py create mode 100644 test/gui/steps/server_context.py delete mode 100644 test/gui/tst_addAccount/test.py diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index be0dc40d53..0178becc94 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -34,6 +34,9 @@ when: path: <<: *trigger_path - event: pull_request + branch: + - main + - stable-* path: <<: *trigger_path evaluate: | diff --git a/.woodpecker/cache-pnpm.yaml b/.woodpecker/cache-pnpm.yaml index 356ec577ac..8f33e5f55b 100644 --- a/.woodpecker/cache-pnpm.yaml +++ b/.woodpecker/cache-pnpm.yaml @@ -34,6 +34,9 @@ when: <<: *trigger_path - event: tag - event: pull_request + branch: + - main + - stable-* path: <<: *trigger_path evaluate: | diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index 761767065e..da43eb3bee 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -35,6 +35,9 @@ when: <<: *trigger_path - event: tag - event: pull_request + branch: + - main + - stable-* path: <<: *trigger_path evaluate: | diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 0719eed106..6e81b95050 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -20,6 +20,9 @@ when: - push - manual - event: pull_request + branch: + - main + - stable-* - event: tag - event: cron cron: nightly* diff --git a/test/gui/.gitignore b/test/gui/.gitignore index 7648ceb913..e10e7e0638 100644 --- a/test/gui/.gitignore +++ b/test/gui/.gitignore @@ -1,2 +1,3 @@ -shared/scripts/custom_lib +custom_lib/ reports +venv/ \ No newline at end of file diff --git a/test/gui/behave.ini b/test/gui/behave.ini new file mode 100644 index 0000000000..867a69bf4d --- /dev/null +++ b/test/gui/behave.ini @@ -0,0 +1,8 @@ +[behave] +paths=features +default_format = plain +default_tags = not (@skip) +logging_level = WARNING +capture_stderr = false +capture_stdout = false +capture_log = false \ No newline at end of file diff --git a/test/gui/environment.py b/test/gui/environment.py new file mode 100644 index 0000000000..73f9268845 --- /dev/null +++ b/test/gui/environment.py @@ -0,0 +1,43 @@ +import psutil +import shutil +import os + +from helpers.ConfigHelper import init_config +from helpers.api.provisioning import delete_created_users +from helpers.ConfigHelper import set_config, get_config +from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths +from helpers.SetupClientHelper import app + + +def before_feature(context, feature): + init_config() + + +def before_scenario(context, feature): + set_config("currentUserSyncPath", "") + + +def after_scenario(context, scenario): + # clean up config dir + shutil.rmtree(get_config("clientConfigDir")) + # clean up sync dir + for entry in os.scandir(get_config("clientRootSyncPath")): + try: + if entry.is_file() or entry.is_symlink(): + print("Deleting file: " + entry.name) + os.unlink(prefix_path_namespace(entry.path)) + elif entry.is_dir(): + print("Deleting folder: " + entry.name) + shutil.rmtree(prefix_path_namespace(entry.path)) + except OSError as e: + print(f"Failed to delete '{entry.name}'.\nReason: {e}.") + # cleanup paths created outside of the temporary directory during the test + cleanup_created_paths() + delete_created_users() + # quit the application + app().quit() + for process in psutil.process_iter(['pid', 'exe']): + if process.info['exe'] == get_config("app_path"): + print("Closing desktop client...") + psutil.Process(process.info['pid']).kill() + break diff --git a/test/gui/tst_addAccount/test.feature b/test/gui/features/add-account/account.feature similarity index 99% rename from test/gui/tst_addAccount/test.feature rename to test/gui/features/add-account/account.feature index 6d05242a6d..9e70f4d2ef 100644 --- a/test/gui/tst_addAccount/test.feature +++ b/test/gui/features/add-account/account.feature @@ -17,7 +17,7 @@ Feature: adding accounts Then the download everything option should be selected by default for Linux And the user should be able to choose the local download directory - + @smoke Scenario: Adding normal Account Given the user has started the client When the user adds the following account: diff --git a/test/gui/shared/scripts/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py similarity index 90% rename from test/gui/shared/scripts/helpers/ConfigHelper.py rename to test/gui/helpers/ConfigHelper.py index af602387ce..908030ff84 100644 --- a/test/gui/shared/scripts/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -11,7 +11,7 @@ def read_env_file(): envs = {} script_path = os.path.dirname(os.path.realpath(__file__)) - env_path = os.path.abspath(os.path.join(script_path, '..', '..', '..', 'envs.txt')) + env_path = os.path.abspath(os.path.join(script_path, '..', 'envs.txt')) with open(env_path, 'rt', encoding='UTF-8') as f: for line in f: if not line.strip(): @@ -64,6 +64,7 @@ def get_default_home_dir(): # map environment variables to config keys CONFIG_ENV_MAP = { + 'app_path': 'APP_PATH', 'localBackendUrl': 'BACKEND_HOST', 'maxSyncTimeout': 'MAX_SYNC_TIMEOUT', 'minSyncTimeout': 'MIN_SYNC_TIMEOUT', @@ -73,14 +74,17 @@ def get_default_home_dir(): 'clientRootSyncPath': 'CLIENT_ROOT_SYNC_PATH', 'tempFolderPath': 'TEMP_FOLDER_PATH', 'guiTestReportDir': 'GUI_TEST_REPORT_DIR', - 'record_video_on_failure': 'RECORD_VIDEO_ON_FAILURE' + 'record_video_on_failure': 'RECORD_VIDEO_ON_FAILURE', } DEFAULT_PATH_CONFIG = { - 'custom_lib': os.path.abspath('../shared/scripts/custom_lib'), + 'custom_lib': os.path.abspath( + os.path.join(os.path.dirname(__file__), 'custom_lib') + ), 'home_dir': get_default_home_dir(), # allow to record first 5 videos 'video_record_limit': 5, + 'app_path': None, } # default config values @@ -98,13 +102,13 @@ def get_default_home_dir(): 'guiTestReportDir': os.path.abspath('../reports'), 'record_video_on_failure': False, 'files_for_upload': os.path.join(CURRENT_DIR.parent.parent, 'files-for-upload'), - 'syncConnectionName': 'Personal' + 'syncConnectionName': 'Personal', } # Permission roles mapping PERMISSION_ROLES = { 'Viewer': 'b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5', - 'Editor': 'fb6c3e19-e378-47e5-b277-9732f9de6e21' + 'Editor': 'fb6c3e19-e378-47e5-b277-9732f9de6e21', } CONFIG.update(DEFAULT_PATH_CONFIG) @@ -130,9 +134,7 @@ def init_config(): # try reading configs from config.ini try: script_path = os.path.dirname(os.path.realpath(__file__)) - cfg_path = os.path.abspath( - os.path.join(script_path, '..', '..', '..', 'config.ini') - ) + cfg_path = os.path.abspath(os.path.join(script_path, '..', 'config.ini')) read_cfg_file(cfg_path) except: pass @@ -165,6 +167,9 @@ def init_config(): else: CONFIG[key] = value.rstrip('/') + '/' + if 'app_path' not in CONFIG or not CONFIG['app_path']: + raise KeyError('APP_PATH must be set in config.ini or environment variables') + def get_config(key): return CONFIG[key] diff --git a/test/gui/shared/scripts/helpers/FilesHelper.py b/test/gui/helpers/FilesHelper.py similarity index 96% rename from test/gui/shared/scripts/helpers/FilesHelper.py rename to test/gui/helpers/FilesHelper.py index a3ad0cdbf2..1e246fda69 100644 --- a/test/gui/shared/scripts/helpers/FilesHelper.py +++ b/test/gui/helpers/FilesHelper.py @@ -2,11 +2,11 @@ import re import shutil from pathlib import Path - from pypdf import PdfReader from docx import Document from pptx import Presentation from openpyxl import load_workbook + from helpers.ConfigHelper import is_windows, get_config @@ -16,12 +16,9 @@ def build_conflicted_regex(filename): namepart = filename.split(".")[0] extpart = filename.split(".")[1] # pylint: disable=anomalous-backslash-in-string - return "%s \(conflicted copy \d{4}-\d{2}-\d{2} \d{6}\)\.%s" % ( - namepart, - extpart, - ) + return rf"{namepart} \(conflicted copy \d{{4}}-\d{{2}}-\d{{2}} \d{{6}}\)\.{extpart}" # pylint: disable=anomalous-backslash-in-string - return "%s \(conflicted copy \d{4}-\d{2}-\d{2} \d{6}\)" % filename + return rf"{filename} \(conflicted copy \d{{4}}-\d{{2}}-\d{{2}} \d{{6}}\)" def sanitize_path(path): diff --git a/test/gui/shared/scripts/helpers/ReportHelper.py b/test/gui/helpers/ReportHelper.py similarity index 98% rename from test/gui/shared/scripts/helpers/ReportHelper.py rename to test/gui/helpers/ReportHelper.py index c7eff43a0b..f29e15c6f6 100644 --- a/test/gui/shared/scripts/helpers/ReportHelper.py +++ b/test/gui/helpers/ReportHelper.py @@ -2,8 +2,6 @@ import glob import shutil import test -import squish -import squishinfo from helpers.ConfigHelper import get_config from helpers.FilesHelper import prefix_path_namespace diff --git a/test/gui/shared/scripts/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py similarity index 85% rename from test/gui/shared/scripts/helpers/SetupClientHelper.py rename to test/gui/helpers/SetupClientHelper.py index 4c66aec862..ca663d8be7 100644 --- a/test/gui/shared/scripts/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -1,24 +1,29 @@ import uuid import os import subprocess +import test +import psutil from urllib.parse import urlparse from os import makedirs from os.path import exists, join -import test -import psutil -import squish -import squishinfo from PySide6.QtCore import QSettings, QUuid, QUrl, QJsonValue +from appium import webdriver +from appium.options.common.base import AppiumOptions from helpers.SpaceHelper import get_space_id, get_personal_space_id from helpers.ConfigHelper import get_config, set_config, is_windows from helpers.SyncHelper import listen_sync_status_for_item from helpers.api.utils import url_join from helpers.UserHelper import get_displayname_for_user, get_password_for_user -from helpers.ReportHelper import is_video_enabled from helpers.api import provisioning +app_driver = None + + +def app(): + return app_driver + def substitute_inline_codes(value): value = value.replace('%local_server%', get_config('localBackendUrl')) @@ -32,7 +37,7 @@ def substitute_inline_codes(value): return value -def get_client_details(context): +def get_client_details(table): client_details = { 'server': '', 'user': '', @@ -40,16 +45,16 @@ def get_client_details(context): 'sync_folder': '', 'oauth': False, } - for row in context.table[0:]: - row[1] = substitute_inline_codes(row[1]) - if row[0] == 'server': - client_details.update({'server': row[1]}) - elif row[0] == 'user': - client_details.update({'user': row[1]}) - elif row[0] == 'password': - client_details.update({'password': row[1]}) - elif row[0] == 'sync_folder': - client_details.update({'sync_folder': row[1]}) + for key, value in table.items(): + value = substitute_inline_codes(value) + if key == 'server': + client_details.update({'server': value}) + elif key == 'user': + client_details.update({'user': value}) + elif key == 'password': + client_details.update({'password': value}) + elif key == 'sync_folder': + client_details.update({'sync_folder': value}) return client_details @@ -103,26 +108,30 @@ def get_current_user_sync_path(): def start_client(): + global app_driver log_command_suffix = "" logfile = get_config("clientLogFile") - logdir = get_config("clientLogDir") + "/" + squishinfo.testCaseName + logdir = get_config("clientLogDir") if logfile != "": log_command_suffix = f' --logfile {logfile}' elif logdir != "": log_command_suffix = f' --logdir {logdir}' - squish.startApplication( - 'opencloud -s' - + f' {log_command_suffix}' - + ' --logdebug' + options = AppiumOptions() + options.set_capability( + 'app', + f'{get_config("app_path")} -s {log_command_suffix} --logdebug', + ) + options.set_capability( + 'appium:environ', + { + 'XDG_CONFIG_HOME': '/tmp/opencloudtest/.config', + }, + ) + app_driver = webdriver.Remote( + command_executor='http://127.0.0.1:4723', options=options ) - if is_video_enabled(): - test.startVideoCapture() - else: - test.log( - f'Video recordings reached the maximum limit of {get_config("video_record_limit")}.' - + 'Skipping video recording...' - ) + app_driver.implicitly_wait = 10 def get_polling_interval(): @@ -134,6 +143,7 @@ def get_polling_interval(): polling_interval = polling_interval.format(**args) return polling_interval + def generate_account_config(users, space='Personal'): sync_paths = {} settings = QSettings(get_config('clientConfigFile'), QSettings.Format.IniFormat) @@ -145,7 +155,7 @@ def generate_account_config(users, space='Personal'): for idx, username in enumerate(users): users_uuids[username] = QUuid.createUuid() settings.beginGroup("Accounts") - settings.beginWriteArray(str(idx+1),len(users)) + settings.beginWriteArray(str(idx + 1), len(users)) settings.setValue("capabilities", capabilities_variant) settings.setValue("default_sync_root", create_user_sync_path(username)) @@ -161,7 +171,7 @@ def generate_account_config(users, space='Personal'): settings.beginGroup("Folders") for idx, username in enumerate(users): sync_path = create_space_path(username, space) - settings.beginWriteArray(str(idx+1),len(users)) + settings.beginWriteArray(str(idx + 1), len(users)) if space == 'Personal': space_id = get_personal_space_id(username) @@ -181,17 +191,17 @@ def generate_account_config(users, space='Personal'): settings.setValue("virtualFilesMode", 'cfapi') else: settings.setValue("virtualFilesMode", 'off') - settings.setValue("journalPath",".sync_journal.db") + settings.setValue("journalPath", ".sync_journal.db") settings.endArray() settings.setValue("size", len(users)) sync_paths.update({username: sync_path}) settings.endGroup() - settings.sync() return sync_paths + def setup_client(username, space='Personal'): set_config('syncConnectionName', space) sync_paths = generate_account_config([username], space) diff --git a/test/gui/shared/scripts/helpers/SpaceHelper.py b/test/gui/helpers/SpaceHelper.py similarity index 100% rename from test/gui/shared/scripts/helpers/SpaceHelper.py rename to test/gui/helpers/SpaceHelper.py diff --git a/test/gui/shared/scripts/helpers/StacktraceHelper.py b/test/gui/helpers/StacktraceHelper.py similarity index 100% rename from test/gui/shared/scripts/helpers/StacktraceHelper.py rename to test/gui/helpers/StacktraceHelper.py diff --git a/test/gui/shared/scripts/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py similarity index 94% rename from test/gui/shared/scripts/helpers/SyncHelper.py rename to test/gui/helpers/SyncHelper.py index 98c751a10f..34b4e7bb46 100644 --- a/test/gui/shared/scripts/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -1,13 +1,12 @@ import os import re -import sys -import test +import time import urllib.request -import squish from helpers.ConfigHelper import get_config, is_linux, is_windows from helpers.FilesHelper import sanitize_path + if is_windows(): from helpers.WinPipeHelper import WinPipeConnect as SocketConnect else: @@ -29,8 +28,7 @@ # see https://kb.froglogic.com/squish/howto/using-external-python-interpreter-squish-6-6/ # if the IDE fails to reference the script, # add the folder in Edit->Preferences->PyDev->Interpreters->Libraries - sys.path.append(custom_lib) - from custom_lib.syncstate import SocketConnect + from helpers.custom_lib.syncstate import SocketConnect # socket messages socket_messages = [] @@ -170,10 +168,12 @@ def generate_sync_pattern_from_messages(messages): sync_messages = filter_sync_messages(messages) for message in sync_messages: - # E.g; from "STATUS:OK:/tmp/client-bdd/Alice/" + # E.g; from; + # Linux: "STATUS:OK:/tmp/client-bdd/Alice/" + # Win: "STATUS:OK:C:\tmp\client-bdd\Alice\" # excludes ":/tmp/client-bdd/Alice/" # adds only "STATUS:OK" to the pattern list - if match := re.search(':(/|[A-Z]{1}:\\\\|[A-Z]{1}:\/).*', message): + if match := re.search(r':(/|[A-Za-z]:[\\/]).*', message): (end, _) = match.span() # shared resources will have status like "STATUS:OK+SWM" status = message[:end].replace('+SWM', '') @@ -222,7 +222,7 @@ def wait_for_resource_to_sync(resource, resource_type='FOLDER', patterns=None): if patterns is None: patterns = get_synced_pattern(resource) - synced = squish.waitFor( + synced = wait_for( lambda: has_sync_pattern(patterns, resource), timeout, ) @@ -232,7 +232,7 @@ def wait_for_resource_to_sync(resource, resource_type='FOLDER', patterns=None): # and pass the step if the last sync status is STATUS:OK status = get_current_sync_status(resource, resource_type) if status.startswith(SYNC_STATUS['OK']): - test.log( + print( '[WARN] Failed to match sync pattern for resource: ' + resource + f'\nBut its last status is "{SYNC_STATUS["OK"]}"' @@ -271,7 +271,7 @@ def has_sync_pattern(patterns, resource=None): if pattern_len == len(actual_pattern) and pattern == actual_pattern: return True # 100 milliseconds polling interval - squish.snooze(0.1) + time.sleep(0.1) return False @@ -299,7 +299,7 @@ def wait_for_resource_to_have_sync_status( if not timeout: timeout = get_config('maxSyncTimeout') * 1000 - result = squish.waitFor( + result = wait_for( lambda: has_sync_status(resource, status), timeout, ) @@ -341,9 +341,7 @@ def perform_file_explorer_vfs_action(resource_path, action): elif action == 'Always keep on this device': make_available_locally(resource_path) else: - raise ValueError( - f'Invalid file explorer action: {action}' - ) + raise ValueError(f'Invalid file explorer action: {action}') def make_online_only(resource_path): @@ -356,3 +354,13 @@ def make_available_locally(resource_path): socket_connect = get_socket_connection() resource_path = resource_path.rstrip('\\').rstrip('/') socket_connect.sendCommand(f'MAKE_AVAILABLE_LOCALLY:{resource_path}\n') + + +def wait_for(condition, timeout, interval=0.5): + start = time.time() * 1000 + while True: + if condition(): + return True + if time.time() * 1000 - start > timeout: + return False + time.sleep(interval) diff --git a/test/gui/shared/scripts/helpers/UserHelper.py b/test/gui/helpers/UserHelper.py similarity index 100% rename from test/gui/shared/scripts/helpers/UserHelper.py rename to test/gui/helpers/UserHelper.py diff --git a/test/gui/shared/scripts/helpers/VFSFileHelper.py b/test/gui/helpers/VFSFileHelper.py similarity index 100% rename from test/gui/shared/scripts/helpers/VFSFileHelper.py rename to test/gui/helpers/VFSFileHelper.py diff --git a/test/gui/helpers/WebUIHelper.py b/test/gui/helpers/WebUIHelper.py new file mode 100644 index 0000000000..eafbe25383 --- /dev/null +++ b/test/gui/helpers/WebUIHelper.py @@ -0,0 +1,20 @@ +import pyperclip +from playwright.sync_api import sync_playwright + + +def authorize_via_webui(username, password): + url = pyperclip.paste() + with sync_playwright() as pw: + browser = pw.chromium.launch(headless=True) + context = browser.new_context(ignore_https_errors=True) + page = context.new_page() + + page.goto(url) + page.fill('#oc-login-username', username) + page.fill('#oc-login-password', password) + page.click('button :text("Log in")') + page.click('button :text("Allow")') + page.wait_for_selector(':text("Login successful")') + + context.close() + browser.close() diff --git a/test/gui/shared/scripts/helpers/WinPipeHelper.py b/test/gui/helpers/WinPipeHelper.py similarity index 100% rename from test/gui/shared/scripts/helpers/WinPipeHelper.py rename to test/gui/helpers/WinPipeHelper.py diff --git a/test/gui/shared/scripts/helpers/api/http_helper.py b/test/gui/helpers/api/http_helper.py similarity index 100% rename from test/gui/shared/scripts/helpers/api/http_helper.py rename to test/gui/helpers/api/http_helper.py diff --git a/test/gui/shared/scripts/helpers/api/provisioning.py b/test/gui/helpers/api/provisioning.py similarity index 99% rename from test/gui/shared/scripts/helpers/api/provisioning.py rename to test/gui/helpers/api/provisioning.py index 2f78d2df23..133d9db39d 100644 --- a/test/gui/shared/scripts/helpers/api/provisioning.py +++ b/test/gui/helpers/api/provisioning.py @@ -1,9 +1,11 @@ +import json +from PySide6.QtCore import QJsonDocument + +import helpers.api.http_helper as request from helpers.ConfigHelper import get_config from helpers import UserHelper -import helpers.api.http_helper as request from helpers.api.utils import url_join -import json -from PySide6.QtCore import QJsonDocument + created_groups = {} created_users = {} diff --git a/test/gui/shared/scripts/helpers/api/utils.py b/test/gui/helpers/api/utils.py similarity index 100% rename from test/gui/shared/scripts/helpers/api/utils.py rename to test/gui/helpers/api/utils.py diff --git a/test/gui/shared/scripts/helpers/api/webdav_helper.py b/test/gui/helpers/api/webdav_helper.py similarity index 100% rename from test/gui/shared/scripts/helpers/api/webdav_helper.py rename to test/gui/helpers/api/webdav_helper.py diff --git a/test/gui/shared/scripts/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py similarity index 54% rename from test/gui/shared/scripts/pageObjects/AccountConnectionWizard.py rename to test/gui/pageObjects/AccountConnectionWizard.py index 3395969c9f..fefcc384f7 100644 --- a/test/gui/shared/scripts/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -1,9 +1,6 @@ -import test -import names -import squish import os - -from pageObjects.EnterPassword import EnterPassword +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By from helpers.WebUIHelper import authorize_via_webui from helpers.ConfigHelper import get_config @@ -13,103 +10,61 @@ set_current_user_sync_path, ) from helpers.SyncHelper import listen_sync_status_for_item +from helpers.SetupClientHelper import app class AccountConnectionWizard: - SERVER_ADDRESS_BOX = { - "container": names.setupWizardWindow_contentWidget_QStackedWidget, - "name": "urlLineEdit", - "type": "QLineEdit", - "visible": 1, - } - NEXT_BUTTON = { - "container": names.settings_dialogStack_QStackedWidget, - "name": "nextButton", - "type": "QPushButton", - "visible": 1, - } - CONFIRM_INSECURE_CONNECTION_BUTTON = { - "text": "Confirm", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.insecure_connection_QMessageBox, - } - USERNAME_BOX = { - "container": names.contentWidget_OCC_QmlUtils_OCQuickWidget, - "id": "userNameField", - "type": "TextField", - "visible": True, - } - SELECT_LOCAL_FOLDER = { - "container": names.advancedConfigGroupBox_localDirectoryGroupBox_QGroupBox, - "name": "localDirectoryLineEdit", - "type": "QLineEdit", - "visible": 1, - } - DIRECTORY_NAME_BOX = { - "container": names.advancedConfigGroupBox_localDirectoryGroupBox_QGroupBox, - "name": "chooseLocalDirectoryButton", - "type": "QToolButton", - "visible": 1, - } - CHOOSE_BUTTON = { - "text": "Choose", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.qFileDialog_QFileDialog, - } - OAUTH_CREDENTIAL_PAGE = { - "container": names.contentWidget_contentWidget_QStackedWidget, - "type": "OCC::Wizard::OAuthCredentialsSetupWizardPage", - "visible": 1, - } - COPY_URL_TO_CLIPBOARD_BUTTON = { - "container": names.contentWidget_OCC_QmlUtils_OCQuickWidget, - "id": "copyToClipboardButton", - "type": "Button", - "visible": True, - } - CONF_SYNC_MANUALLY_RADIO_BUTTON = { - "container": names.advancedConfigGroupBox_syncModeGroupBox_QGroupBox, - "name": "configureSyncManuallyRadioButton", - "type": "QRadioButton", - "visible": 1, - } - ADVANCED_CONFIGURATION_CHECKBOX = { - "container": names.setupWizardWindow_contentWidget_QStackedWidget, - "name": "advancedConfigGroupBox", - "type": "QGroupBox", - "visible": 1, - } - DIRECTORY_NAME_EDIT_BOX = { - "buddy": names.qFileDialog_fileNameLabel_QLabel, - "name": "fileNameEdit", - "type": "QLineEdit", - "visible": 1, - } - SYNC_EVERYTHING_RADIO_BUTTON = { - "container": names.advancedConfigGroupBox_syncModeGroupBox_QGroupBox, - "name": "syncEverythingRadioButton", - "type": "QRadioButton", - "visible": 1, - } + SERVER_ADDRESS_BOX = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.Settings.centralwidget.dialogStack.SetupWizardWidget.contentWidget.ServerUrlSetupWizardPage.urlLineEdit", + ) + NEXT_BUTTON = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.Settings.centralwidget.dialogStack.SetupWizardWidget.nextButton", + ) + ACCEPT_CERTIFICATE_YES = SimpleNamespace( + by=By.NAME, + selector="Yes", + ) + SELECT_LOCAL_FOLDER = SimpleNamespace(by=None, selector=None) + DIRECTORY_NAME_BOX = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.Settings.centralwidget.dialogStack.SetupWizardWidget.contentWidget.AccountConfiguredWizardPage.advancedConfigGroupBox.advancedConfigGroupBoxContentWidget.localDirectoryGroupBox.chooseLocalDirectoryButton", + ) + CHOOSE_FOLDER_BUTTON = SimpleNamespace(by=By.NAME, selector="Choose") + OAUTH_CREDENTIAL_PAGE = SimpleNamespace(by=None, selector=None) + COPY_URL_TO_CLIPBOARD_BUTTON = SimpleNamespace( + by=By.NAME, + selector="Copy URL", + ) + CONF_SYNC_MANUALLY_RADIO_BUTTON = SimpleNamespace(by=None, selector=None) + ADVANCED_CONFIGURATION_CHECKBOX = SimpleNamespace( + by=By.NAME, + selector="Advanced configuration", + ) + DIRECTORY_NAME_EDIT_BOX = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.QFileDialog.fileNameEdit", + ) + SYNC_EVERYTHING_RADIO_BUTTON = SimpleNamespace(by=None, selector=None) @staticmethod def add_server(server_url): - squish.mouseClick( - squish.waitForObject(AccountConnectionWizard.SERVER_ADDRESS_BOX) - ) - squish.type( - squish.waitForObject(AccountConnectionWizard.SERVER_ADDRESS_BOX), - server_url, + url_input = app().find_element( + AccountConnectionWizard.SERVER_ADDRESS_BOX.by, + AccountConnectionWizard.SERVER_ADDRESS_BOX.selector, ) + url_input.clear() + url_input.send_keys(get_config("localBackendUrl")) + AccountConnectionWizard.next_step() @staticmethod def accept_certificate(): - squish.clickButton(squish.waitForObject(EnterPassword.ACCEPT_CERTIFICATE_YES)) + app().find_element( + AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.by, + AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.selector, + ).click() @staticmethod def add_user_credentials(username, password): @@ -117,22 +72,22 @@ def add_user_credentials(username, password): @staticmethod def oidc_login(username, password): - AccountConnectionWizard.browser_login(username, password, "oidc") + AccountConnectionWizard.browser_login(username, password) @staticmethod - def browser_login(username, password, login_type=None): - # wait 500ms for copy button to fully load - squish.snooze(1 / 2) - squish.mouseClick( - squish.waitForObject(AccountConnectionWizard.COPY_URL_TO_CLIPBOARD_BUTTON) - ) - authorize_via_webui(username, password, login_type) + def browser_login(username, password): + app().find_element( + AccountConnectionWizard.COPY_URL_TO_CLIPBOARD_BUTTON.by, + AccountConnectionWizard.COPY_URL_TO_CLIPBOARD_BUTTON.selector, + ).click() + authorize_via_webui(username, password) @staticmethod def next_step(): - squish.clickButton( - squish.waitForObjectExists(AccountConnectionWizard.NEXT_BUTTON) - ) + app().find_element( + AccountConnectionWizard.NEXT_BUTTON.by, + AccountConnectionWizard.NEXT_BUTTON.selector, + ).click() @staticmethod def select_sync_folder(user): @@ -140,14 +95,20 @@ def select_sync_folder(user): sync_path = create_user_sync_path(user) AccountConnectionWizard.select_advanced_config() - squish.mouseClick( - squish.waitForObject(AccountConnectionWizard.DIRECTORY_NAME_BOX) - ) - squish.type( - squish.waitForObject(AccountConnectionWizard.DIRECTORY_NAME_EDIT_BOX), - sync_path, + app().find_element( + AccountConnectionWizard.DIRECTORY_NAME_BOX.by, + AccountConnectionWizard.DIRECTORY_NAME_BOX.selector, + ).click() + dir_location_input = app().find_element( + AccountConnectionWizard.DIRECTORY_NAME_EDIT_BOX.by, + AccountConnectionWizard.DIRECTORY_NAME_EDIT_BOX.selector, ) - squish.clickButton(squish.waitForObject(AccountConnectionWizard.CHOOSE_BUTTON)) + dir_location_input.clear() + dir_location_input.send_keys(sync_path) + app().find_element( + AccountConnectionWizard.CHOOSE_FOLDER_BUTTON.by, + AccountConnectionWizard.CHOOSE_FOLDER_BUTTON.selector, + ).click() return os.path.join(sync_path, get_config('syncConnectionName')) @staticmethod @@ -205,14 +166,12 @@ def select_manual_sync_folder_option(): ) ) - @staticmethod def select_download_everything_option(): squish.clickButton( squish.waitForObject(AccountConnectionWizard.SYNC_EVERYTHING_RADIO_BUTTON) ) - @staticmethod def is_new_connection_window_visible(): visible = False @@ -235,9 +194,10 @@ def is_credential_window_visible(): @staticmethod def select_advanced_config(): - squish.waitForObject( - AccountConnectionWizard.ADVANCED_CONFIGURATION_CHECKBOX - ).setChecked(True) + app().find_element( + AccountConnectionWizard.ADVANCED_CONFIGURATION_CHECKBOX.by, + AccountConnectionWizard.ADVANCED_CONFIGURATION_CHECKBOX.selector, + ).click() @staticmethod def can_change_local_sync_dir(): @@ -247,7 +207,7 @@ def can_change_local_sync_dir(): squish.clickButton( squish.waitForObject(AccountConnectionWizard.DIRECTORY_NAME_BOX) ) - squish.waitForObjectExists(AccountConnectionWizard.CHOOSE_BUTTON) + squish.waitForObjectExists(AccountConnectionWizard.CHOOSE_FOLDER_BUTTON) can_change = True except: pass diff --git a/test/gui/shared/scripts/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/AccountSetting.py rename to test/gui/pageObjects/AccountSetting.py diff --git a/test/gui/shared/scripts/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/Activity.py rename to test/gui/pageObjects/Activity.py diff --git a/test/gui/shared/scripts/pageObjects/EnterPassword.py b/test/gui/pageObjects/EnterPassword.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/EnterPassword.py rename to test/gui/pageObjects/EnterPassword.py diff --git a/test/gui/shared/scripts/pageObjects/PublicLinkDialog.py b/test/gui/pageObjects/PublicLinkDialog.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/PublicLinkDialog.py rename to test/gui/pageObjects/PublicLinkDialog.py diff --git a/test/gui/shared/scripts/pageObjects/Settings.py b/test/gui/pageObjects/Settings.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/Settings.py rename to test/gui/pageObjects/Settings.py diff --git a/test/gui/shared/scripts/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/SyncConnection.py rename to test/gui/pageObjects/SyncConnection.py diff --git a/test/gui/shared/scripts/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py similarity index 100% rename from test/gui/shared/scripts/pageObjects/SyncConnectionWizard.py rename to test/gui/pageObjects/SyncConnectionWizard.py diff --git a/test/gui/shared/scripts/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py similarity index 67% rename from test/gui/shared/scripts/pageObjects/Toolbar.py rename to test/gui/pageObjects/Toolbar.py index be66fa984e..2e495a3da9 100644 --- a/test/gui/shared/scripts/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -1,54 +1,20 @@ -import names -import squish -import object # pylint: disable=redefined-builtin +from types import SimpleNamespace +from urllib.parse import urlparse +from appium.webdriver.common.appiumby import AppiumBy as By from helpers.SetupClientHelper import wait_until_app_killed from helpers.ConfigHelper import get_config +from helpers.SetupClientHelper import app class Toolbar: - TOOLBAR_ROW = { - "container": names.dialogStack_quickWidget_OCC_QmlUtils_OCQuickWidget, - "type": "RowLayout", - "visible": True, - } - ACCOUNT_BUTTON = { - "checkable": False, - "container": names.dialogStack_quickWidget_OCC_QmlUtils_OCQuickWidget, - "type": "AccountButton", - "visible": True, - } - ADD_ACCOUNT_BUTTON = { - "container": names.dialogStack_quickWidget_QQuickWidget, - "id": "addAccountButton", - "type": "AccountButton", - "visible": True, - } - ACTIVITY_BUTTON = { - "container": names.dialogStack_quickWidget_QQuickWidget, - "id": "logButton", - "type": "AccountButton", - "visible": True, - } - SETTINGS_BUTTON = { - "container": names.dialogStack_quickWidget_QQuickWidget, - "id": "settingsButton", - "type": "AccountButton", - "visible": True, - } - QUIT_BUTTON = { - "container": names.dialogStack_quickWidget_QQuickWidget, - "id": "quitButton", - "type": "AccountButton", - "visible": True, - } - CONFIRM_QUIT_BUTTON = { - "text": "Yes", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.quit_OpenCloud_Desktop_QMessageBox, - } + TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) + ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) + ADD_ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) + ACTIVITY_BUTTON = SimpleNamespace(by=None, selector=None) + SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) + QUIT_BUTTON = SimpleNamespace(by=None, selector=None) + CONFIRM_QUIT_BUTTON = SimpleNamespace(by=None, selector=None) TOOLBAR_ITEMS = ["Add Account", "Activity", "Settings", "Quit"] @@ -147,11 +113,6 @@ def account_has_focus(display_name): @staticmethod def account_exists(display_name): - account, selector = Toolbar.get_account(display_name) - if ( - account is None - or selector is None - and account["displayname"] != display_name - ): - raise LookupError(f'Account "{display_name}" does not exist') - squish.waitForObject(selector) + server_host = urlparse(get_config('localBackendUrl')).netloc + account_label = f"{display_name}@{server_host}" + app().find_element(By.NAME, account_label) diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 7aca78c907..2a440f8e2e 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -1,11 +1,17 @@ requests==2.32.*; python_version >= "3.10" -PyGObject==3.42.*; sys_platform != 'win32' and python_version >= "3.10" +PyGObject==3.42.*; python_version >= "3.10" and sys_platform == 'linux' psutil==5.9.*; python_version >= "3.10" black==24.3.*; python_version >= "3.10" pylint==3.2.*; python_version >= "3.10" -pywin32==305; sys_platform == 'win32' and python_version >= "3.10" +pywin32==305; python_version >= "3.10" and sys_platform == 'win32' pyside6==6.9.*; python_version >= "3.10" pypdf==6.5.*; python_version >= "3.10" python-docx==1.2.*; python_version >= "3.10" python-pptx==1.0.*; python_version >= "3.10" -openpyxl==3.1.*; python_version >= "3.10" \ No newline at end of file +openpyxl==3.1.*; python_version >= "3.10" +pyperclip==1.11.*; python_version >= "3.10" +playwright==1.58.*; python_version >= "3.10" +behave==1.3.*; python_version >= "3.10" +Appium-Python-Client==5.3.*; python_version >= "3.10" +Flask==3.0.*; python_version >= "3.10" and sys_platform == 'linux' +numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' \ No newline at end of file diff --git a/test/gui/shared/scripts/helpers/WebUIHelper.py b/test/gui/shared/scripts/helpers/WebUIHelper.py deleted file mode 100644 index 52e2f30105..0000000000 --- a/test/gui/shared/scripts/helpers/WebUIHelper.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import subprocess -import squish - - -def get_clipboard_text(): - try: - return squish.getClipboardText() - except: - # Retry after 2 seconds - squish.snooze(2) - return squish.getClipboardText() - - -def authorize_via_webui(username, password, login_type='oidc'): - script_path = os.path.dirname(os.path.realpath(__file__)) - - webui_path = os.path.join(script_path, '..', '..', '..', 'webUI') - os.chdir(webui_path) - - envs = { - 'OC_USERNAME': username.strip('"'), - 'OC_PASSWORD': password.strip('"'), - 'OC_AUTH_URL': get_clipboard_text(), - } - proc = subprocess.run( - f"pnpm run {login_type}-login", - capture_output=True, - shell=True, - env={**os.environ, **envs}, - check=False, - ) - if proc.returncode: - if proc.stderr.decode('utf-8'): - raise OSError(proc.stderr.decode('utf-8')) - raise OSError(proc.stdout.decode('utf-8')) - os.chdir(script_path) diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py new file mode 100644 index 0000000000..d424a975ff --- /dev/null +++ b/test/gui/steps/account_context.py @@ -0,0 +1,286 @@ +from behave import given as Given, when as When, then as Then + +from pageObjects.AccountConnectionWizard import AccountConnectionWizard + +from pageObjects.Toolbar import Toolbar + +from helpers.SetupClientHelper import ( + start_client, + substitute_inline_codes, + get_client_details, + get_resource_path, +) + +from helpers.SyncHelper import wait_for_initial_sync_to_complete + + +@Given('the user has started the client') +def step(context): + start_client() + + +@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 "{displayname}" 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]) + + +@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): + 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) + 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/steps/server_context.py b/test/gui/steps/server_context.py new file mode 100644 index 0000000000..f7d85312ac --- /dev/null +++ b/test/gui/steps/server_context.py @@ -0,0 +1,135 @@ +from behave import given as Given + +from helpers.api import provisioning + + +@Given('user "{user}" has been created in the server with default attributes') +def step(context, user): + provisioning.create_user(user) + + +# @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 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'], +# ) diff --git a/test/gui/tst_addAccount/test.py b/test/gui/tst_addAccount/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_addAccount/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 8264baeceb84bc26fecd42351cc051b56510a0ed Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 15 Apr 2026 13:01:28 +0545 Subject: [PATCH 24/75] test: tag smoke test scenarios (#867) Signed-off-by: Sajan Gurung --- test/gui/features/add-account/account.feature | 6 ++-- test/gui/tst_activity/test.feature | 2 +- test/gui/tst_deleteFilesFolders/test.feature | 6 ++-- test/gui/tst_editFiles/test.feature | 6 ++-- test/gui/tst_loginLogout/test.feature | 4 +-- test/gui/tst_moveFilesFolders/test.feature | 4 +-- .../tst_removeAccountConnection/test.feature | 2 +- test/gui/tst_spaces/test.feature | 12 ++++---- test/gui/tst_syncing/test.feature | 28 +++++++++---------- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 9e70f4d2ef..a515bcbc07 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -26,7 +26,7 @@ Feature: adding accounts | password | 1234 | Then the account with displayname "Alice Hansen" should be displayed - + @smoke @skip Scenario: Adding multiple accounts Given user "Brian" has been created in the server with default attributes And user "Alice" has set up a client with default settings @@ -50,7 +50,7 @@ Feature: adding accounts | password | 1234 | Then "Alice Hansen" account should be opened - + @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 @@ -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 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/tst_activity/test.feature b/test/gui/tst_activity/test.feature index 97e8a0d07d..bd5f7f5efb 100644 --- a/test/gui/tst_activity/test.feature +++ b/test/gui/tst_activity/test.feature @@ -3,7 +3,7 @@ Feature: filter activity for user I want to filter activity So that I can view activity of specific user - + @smoke @skip Scenario: filter synced activities Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes diff --git a/test/gui/tst_deleteFilesFolders/test.feature b/test/gui/tst_deleteFilesFolders/test.feature index 24a6dd6fba..d2c217376a 100644 --- a/test/gui/tst_deleteFilesFolders/test.feature +++ b/test/gui/tst_deleteFilesFolders/test.feature @@ -6,7 +6,7 @@ Feature: deleting files and folders Background: Given user "Alice" has been created in the server with default attributes - @issue-9439 + @issue-9439 @smoke @skip Scenario Outline: Delete a file Given user "Alice" has uploaded file with content "openCloud test text file 0" to "" in the server And user "Alice" has set up a client with default settings @@ -19,7 +19,7 @@ Feature: deleting files and folders | textfile0-with-name-more-than-20-characters | | ~`!@#$^&()-_=+{[}];',textfile.txt | - @issue-9439 + @issue-9439 @smoke @skip Scenario Outline: Delete a folder Given user "Alice" has created folder "" in the server And user "Alice" has set up a client with default settings @@ -31,7 +31,7 @@ Feature: deleting files and folders | simple-empty-folder | | simple-folder-with-name-more-than-20-characters | - + @smoke @skip Scenario: Delete a file and a folder Given user "Alice" has uploaded file with content "test file 1" to "textfile1.txt" in the server And user "Alice" has uploaded file with content "test file 2" to "textfile2.txt" in the server diff --git a/test/gui/tst_editFiles/test.feature b/test/gui/tst_editFiles/test.feature index d76454cb4f..308446fdb4 100644 --- a/test/gui/tst_editFiles/test.feature +++ b/test/gui/tst_editFiles/test.feature @@ -6,7 +6,7 @@ Feature: edit files Background: Given user "Alice" has been created in the server with default attributes - + @smoke @skip Scenario: Modify original content of a file with special character Given user "Alice" has uploaded file with content "openCloud test text file 0" to "S@mpleFile!With,$pecial&Characters.txt" in the server And user "Alice" has set up a client with default settings @@ -14,8 +14,8 @@ Feature: edit files And the user waits for file "S@mpleFile!With,$pecial&Characters.txt" to be synced Then as "Alice" the file "S@mpleFile!With,$pecial&Characters.txt" should have the content "overwrite openCloud test text file" in the server - - Scenario: Modify original content of a file + @smoke @skip + Scenario: Modify original content of a file Given user "Alice" has set up a client with default settings When user "Alice" creates a file "testfile.txt" with the following content inside the sync folder """ diff --git a/test/gui/tst_loginLogout/test.feature b/test/gui/tst_loginLogout/test.feature index 2f694b32ad..aedd31706f 100644 --- a/test/gui/tst_loginLogout/test.feature +++ b/test/gui/tst_loginLogout/test.feature @@ -6,13 +6,13 @@ Feature: Logout users Background: Given user "Alice" has been created in the server with default attributes - + @smoke @skip Scenario: logging out Given user "Alice" has set up a client with default settings When the user "Alice" logs out using the client-UI Then user "Alice" should be signed out - + @smoke @skip Scenario: login after logging out Given user "Alice" has set up a client with default settings And user "Alice" has logged out from the client-UI diff --git a/test/gui/tst_moveFilesFolders/test.feature b/test/gui/tst_moveFilesFolders/test.feature index 8ff5e5bd8e..e33738a4b9 100644 --- a/test/gui/tst_moveFilesFolders/test.feature +++ b/test/gui/tst_moveFilesFolders/test.feature @@ -41,7 +41,7 @@ Feature: move file and folder And as "Alice" folder "test-folder1" should not exist in the server And as "Alice" folder "test-folder2" should not exist in the server - + @smoke @skip Scenario: Rename a file and a folder Given user "Alice" has uploaded file with content "test file 1" to "textfile.txt" in the server And user "Alice" has set up a client with default settings @@ -53,7 +53,7 @@ Feature: move file and folder But as "Alice" file "textfile.txt" should not exist in the server And as "Alice" folder "folder1" should not exist in the server - + @smoke @skip Scenario: Move files from one folder to another Given user "Alice" has uploaded file with content "test file 1" to "folder1/file1.txt" in the server And user "Alice" has uploaded file with content "test file 2" to "folder1/file2.txt" in the server diff --git a/test/gui/tst_removeAccountConnection/test.feature b/test/gui/tst_removeAccountConnection/test.feature index 45e0af9676..b11a7ed392 100644 --- a/test/gui/tst_removeAccountConnection/test.feature +++ b/test/gui/tst_removeAccountConnection/test.feature @@ -3,7 +3,7 @@ Feature: remove account connection I want to remove my account So that I won't be using any client-UI services - + @smoke @skip Scenario: remove an account connection Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes diff --git a/test/gui/tst_spaces/test.feature b/test/gui/tst_spaces/test.feature index c3f42fe380..2f731007b3 100644 --- a/test/gui/tst_spaces/test.feature +++ b/test/gui/tst_spaces/test.feature @@ -7,7 +7,7 @@ Feature: Project spaces Given user "Alice" has been created in the server with default attributes And the administrator has created a space "Project101" - + @smoke @skip Scenario: User with Viewer role can open the file Given the administrator has created a folder "planning" in space "Project101" And the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" @@ -25,7 +25,7 @@ Feature: Project spaces Then user "Alice" should not be able to edit the file "testfile.txt" on the file system And as "Alice" the file "testfile.txt" in the space "Project101" should have content "some content" in the server - + @smoke @skip Scenario: User with Editor role can edit the file Given the administrator has created a folder "planning" in space "Project101" And the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" @@ -35,7 +35,7 @@ Feature: Project spaces And the user waits for file "testfile.txt" to be synced Then as "Alice" the file "testfile.txt" in the space "Project101" should have content "some content edited" in the server - + @smoke @skip Scenario: User with Manager role can add files and folders Given the administrator has added user "Alice" to space "Project101" with role "manager" And user "Alice" has set up a client with space "Project101" @@ -48,7 +48,7 @@ Feature: Project spaces Then as "Alice" the file "localFile.txt" in the space "Project101" should have content "test content" in the server And as "Alice" the space "Project101" should have folder "localFolder" in the server - + @smoke @skip Scenario: User with Editor role can rename a file Given the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" And the administrator has added user "Alice" to space "Project101" with role "editor" @@ -58,7 +58,7 @@ Feature: Project spaces Then as "Alice" the space "Project101" should have file "renamedFile.txt" in the server And as "Alice" the file "renamedFile.txt" in the space "Project101" should have content "some content" in the server - + @smoke @skip Scenario: Remove folder sync connection (Project Space) Given the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" And the administrator has added user "Alice" to space "Project101" with role "manager" @@ -82,7 +82,7 @@ Feature: Project spaces | resource | status | account | | simple-folder | Blacklisted | Alice Hansen@%local_server_hostname% | - + @smoke @skip Scenario: Sharee with Editor role deletes the shared resource Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index 372eb6522a..f4e1e8501c 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -6,7 +6,7 @@ Feature: Syncing files Background: Given user "Alice" has been created in the server with default attributes - @smokeTest @issue-9281 + @issue-9281 @smoke @skip Scenario: Syncing a file to the server Given user "Alice" has set up a client with default settings When user "Alice" creates a file "lorem-for-upload.txt" with the following content inside the sync folder @@ -19,7 +19,7 @@ Feature: Syncing files Then the file "lorem-for-upload.txt" should have status "Uploaded" in the activity tab And as "Alice" the file "lorem-for-upload.txt" should have the content "test content" in the server - + @smoke @skip Scenario: Syncing all files and folders from the server Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -82,7 +82,7 @@ Feature: Syncing files But the folder "simple-folder" should not exist on the file system And the folder "large-folder" should not exist on the file system - @skipOnWindows + @skipOnWindows @smoke @skip Scenario: Sync only one folder from the server Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -157,7 +157,7 @@ Feature: Syncing files | bFolder | And the user cancels the sync connection wizard - + @smoke @skip Scenario Outline: Syncing a folder to the server Given user "Alice" has set up a client with default settings When user "Alice" creates a folder inside the sync folder @@ -189,7 +189,7 @@ Feature: Syncing files Then the file "trailing-space.txt " should be ignored And the file "folder with space at end " should be ignored - + @smoke @skip Scenario: Many subfolders can be synced Given user "Alice" has created folder "parent" in the server And user "Alice" has set up a client with default settings @@ -235,7 +235,7 @@ Feature: Syncing files And as "Alice" folder "parent/subfolder4" should exist in the server And as "Alice" folder "parent/subfolder5" should exist in the server - + @smoke @skip Scenario: Both original and copied folders can be synced Given user "Alice" has set up a client with default settings When user "Alice" creates a folder "original" inside the sync folder @@ -250,7 +250,7 @@ Feature: Syncing files And as "Alice" folder "original (Copy)" should exist in the server And as "Alice" the file "original (Copy)/localFile.txt" should have the content "test content" in the server - @issue-9281 + @issue-9281 @smoke @skip Scenario: Verify that you can create a subfolder with long name(~220 characters) Given user "Alice" has created a folder "Folder1" inside the sync folder And user "Alice" has set up a client with default settings @@ -259,7 +259,7 @@ Feature: Syncing files Then the folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" should exist on the file system And as "Alice" folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" should exist in the server - + @smoke @skip Scenario: Verify pre existing folders in local (Desktop client) are copied over to the server Given user "Alice" has created a folder "Folder1" inside the sync folder And user "Alice" has created a folder "Folder1/subFolder1" inside the sync folder @@ -299,7 +299,7 @@ Feature: Syncing files | foldername | | An empty folder which name is obviously more than 59 characters | - @skipOnWindows + @skipOnWindows @smoke @skip Scenario: Invalid system names are synced (Linux only) Given user "Alice" has created folder "CON" in the server And user "Alice" has created folder "test%" in the server @@ -327,7 +327,7 @@ Feature: Syncing files But the folder "CON" should not exist on the file system And the file "PRN" should not exist on the file system - + @smoke @skip Scenario: various types of files can be synced from server to client Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has uploaded file "testavatar.png" to "simple-folder/testavatar.png" in the server @@ -376,7 +376,7 @@ Feature: Syncing files And as "Alice" file "simple.pptx" should exist in the server And as "Alice" file "simple.xlsx" should exist in the server - + @smoke @skip Scenario Outline: File with long name can be synced Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder @@ -389,7 +389,7 @@ Feature: Syncing files | filename | | thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIs.txt | - + @smoke @skip Scenario: Syncing file of 1 GB size Given user "Alice" has set up a client with default settings When user "Alice" creates a file "newfile.txt" with size "1GB" inside the sync folder @@ -428,7 +428,7 @@ Feature: Syncing files And as "Alice" folder "folder3" should exist in the server And as user "Alice" folder "folder3" should contain "1000" items in the server - + @smoke @skip Scenario: Skip sync folder configuration Given the user has started the client And the user has entered the following account information: @@ -570,7 +570,7 @@ Feature: Syncing files And as "Brian" file "Shares/simple-folder/simple.pdf" should exist in the server And as "Brian" the file "Shares/simple-folder/uploaded-lorem.txt" should have the content "overwrite openCloud test text file" in the server - @skipOnWindows + @skipOnWindows @smoke @skip Scenario: Unselected subfolders are excluded from local sync Given user "Alice" has created folder "test-folder" in the server And user "Alice" has created folder "test-folder/sub-folder1" in the server From adf553da22edabbc4da1ee88b3640becb155d031 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 16 Apr 2026 12:14:43 +0545 Subject: [PATCH 25/75] test(gui): fix and enable multi-account test scenario (#869) * test: try to delete config dir only if exists Signed-off-by: Sajan Gurung * test: fix config path Signed-off-by: Sajan Gurung * test: check for nul values Signed-off-by: Sajan Gurung * test: enable multiple account test Signed-off-by: Sajan Gurung * test: fix multiple account test Signed-off-by: Sajan Gurung * test: add sure assertion module Signed-off-by: Sajan Gurung * test: use const for config filename Signed-off-by: Sajan Gurung * test: fix python requirement entry Signed-off-by: Sajan Gurung --------- Signed-off-by: Sajan Gurung --- test/gui/environment.py | 26 ++++---- test/gui/features/add-account/account.feature | 2 +- test/gui/helpers/ConfigHelper.py | 29 ++++++--- test/gui/helpers/SetupClientHelper.py | 11 +--- .../pageObjects/AccountConnectionWizard.py | 6 +- test/gui/pageObjects/EnterPassword.py | 61 +++---------------- test/gui/pageObjects/Toolbar.py | 26 +++++--- test/gui/requirements.txt | 3 +- test/gui/steps/account_context.py | 16 ++--- 9 files changed, 79 insertions(+), 101 deletions(-) diff --git a/test/gui/environment.py b/test/gui/environment.py index 73f9268845..dcf2d62c55 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -18,24 +18,24 @@ def before_scenario(context, feature): def after_scenario(context, scenario): - # clean up config dir - shutil.rmtree(get_config("clientConfigDir")) # clean up sync dir - for entry in os.scandir(get_config("clientRootSyncPath")): - try: - if entry.is_file() or entry.is_symlink(): - print("Deleting file: " + entry.name) - os.unlink(prefix_path_namespace(entry.path)) - elif entry.is_dir(): - print("Deleting folder: " + entry.name) - shutil.rmtree(prefix_path_namespace(entry.path)) - except OSError as e: - print(f"Failed to delete '{entry.name}'.\nReason: {e}.") + if os.path.exists(get_config("clientRootSyncPath")): + for entry in os.scandir(get_config("clientRootSyncPath")): + try: + if entry.is_file() or entry.is_symlink(): + print("Deleting file: " + entry.name) + os.unlink(prefix_path_namespace(entry.path)) + elif entry.is_dir(): + print("Deleting folder: " + entry.name) + shutil.rmtree(prefix_path_namespace(entry.path)) + except OSError as e: + print(f"Failed to delete '{entry.name}'.\nReason: {e}.") # cleanup paths created outside of the temporary directory during the test cleanup_created_paths() delete_created_users() # quit the application - app().quit() + if app() is not None: + app().quit() for process in psutil.process_iter(['pid', 'exe']): if process.info['exe'] == get_config("app_path"): print("Closing desktop client...") diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index a515bcbc07..b42556844b 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -26,7 +26,7 @@ Feature: adding accounts | password | 1234 | Then the account with displayname "Alice Hansen" should be displayed - @smoke @skip + @smoke Scenario: Adding multiple accounts Given user "Brian" has been created in the server with default attributes And user "Alice" has set up a client with default settings diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index 908030ff84..74809ceb45 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -1,12 +1,13 @@ import os import platform import builtins +import tempfile from tempfile import gettempdir from configparser import ConfigParser from pathlib import Path CURRENT_DIR = Path(__file__).resolve().parent - +APP_CONFIG_FILE = "opencloud.cfg" def read_env_file(): envs = {} @@ -39,7 +40,7 @@ def is_linux(): def get_win_user_home(): - return os.environ.get('UserProfile') + return os.environ.get('USERPROFILE', '') def get_client_root_path(): @@ -48,12 +49,18 @@ def get_client_root_path(): return os.path.join(gettempdir(), 'opencloudtest') +def get_config_home_linux(): + return os.path.join(tempfile.gettempdir(), 'opencloudtest', '.config') + + +def get_config_home_win(): + return os.path.join(get_win_user_home(), 'AppData', 'Local', 'Temp', 'opencloudtest', '.config') + + def get_config_home(): if is_windows(): - # There is no way to set custom config path in windows - # TODO: set to different path if option is available - return os.path.join(get_win_user_home(), 'AppData', 'Roaming', 'OpenCloud') - return os.path.join(get_config_from_env_file('XDG_CONFIG_HOME'), 'OpenCloud') + return get_config_home_win() + return get_config_home_linux() def get_default_home_dir(): @@ -62,6 +69,12 @@ def get_default_home_dir(): return os.environ.get('HOME') +def get_app_env(): + return { + 'XDG_CONFIG_HOME': get_config_home(), + 'APPDATA': get_config_home(), + } + # map environment variables to config keys CONFIG_ENV_MAP = { 'app_path': 'APP_PATH', @@ -97,8 +110,7 @@ def get_default_home_dir(): 'clientLogDir': '', 'clientRootSyncPath': get_client_root_path(), 'tempFolderPath': os.path.join(get_client_root_path(), 'temp'), - 'clientConfigDir': get_config_home(), - 'clientConfigFile': os.path.join(get_config_home(), "opencloud.cfg"), + 'clientConfigFile': os.path.join(get_config_home(), "OpenCloud", APP_CONFIG_FILE), 'guiTestReportDir': os.path.abspath('../reports'), 'record_video_on_failure': False, 'files_for_upload': os.path.join(CURRENT_DIR.parent.parent, 'files-for-upload'), @@ -157,7 +169,6 @@ def init_config(): elif key in ( 'clientRootSyncPath', 'tempFolderPath', - 'clientConfigDir', 'guiTestReportDir', ): # make sure there is always one trailing slash diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index ca663d8be7..ec8863708b 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -11,10 +11,10 @@ from appium.options.common.base import AppiumOptions from helpers.SpaceHelper import get_space_id, get_personal_space_id -from helpers.ConfigHelper import get_config, set_config, is_windows +from helpers.ConfigHelper import get_config, set_config, is_windows, get_app_env from helpers.SyncHelper import listen_sync_status_for_item from helpers.api.utils import url_join -from helpers.UserHelper import get_displayname_for_user, get_password_for_user +from helpers.UserHelper import get_displayname_for_user from helpers.api import provisioning @@ -122,12 +122,7 @@ def start_client(): 'app', f'{get_config("app_path")} -s {log_command_suffix} --logdebug', ) - options.set_capability( - 'appium:environ', - { - 'XDG_CONFIG_HOME': '/tmp/opencloudtest/.config', - }, - ) + options.set_capability('appium:environ', get_app_env()) app_driver = webdriver.Remote( command_executor='http://127.0.0.1:4723', options=options ) diff --git a/test/gui/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py index fefcc384f7..e52d169639 100644 --- a/test/gui/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -75,11 +75,15 @@ def oidc_login(username, password): AccountConnectionWizard.browser_login(username, password) @staticmethod - def browser_login(username, password): + def copy_login_url(): app().find_element( AccountConnectionWizard.COPY_URL_TO_CLIPBOARD_BUTTON.by, AccountConnectionWizard.COPY_URL_TO_CLIPBOARD_BUTTON.selector, ).click() + + @staticmethod + def browser_login(username, password): + AccountConnectionWizard.copy_login_url() authorize_via_webui(username, password) @staticmethod diff --git a/test/gui/pageObjects/EnterPassword.py b/test/gui/pageObjects/EnterPassword.py index 769b712300..fcbe21cb90 100644 --- a/test/gui/pageObjects/EnterPassword.py +++ b/test/gui/pageObjects/EnterPassword.py @@ -1,55 +1,16 @@ -import names -import squish - +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By +from pageObjects.AccountConnectionWizard import AccountConnectionWizard from helpers.WebUIHelper import authorize_via_webui -from helpers.ConfigHelper import get_config +from helpers.SetupClientHelper import app class EnterPassword: - LOGIN_CONTAINER = { - "name": "LoginRequiredDialog", - "type": "OCC::LoginRequiredDialog", - "visible": 1, - } - LOGIN_USER_LABEL = { - "container": names.groupBox_OCC_QmlUtils_OCQuickWidget, - "type": "Label", - "visible": True, - } - USERNAME_BOX = { - "name": "usernameLineEdit", - "type": "QLineEdit", - "visible": 1, - "window": LOGIN_CONTAINER, - } - LOGOUT_BUTTON = { - "container": names.groupBox_OCC_QmlUtils_OCQuickWidget, - "id": "logOutButton", - "type": "Button", - "visible": True, - } - COPY_URL_TO_CLIPBOARD_BUTTON = { - "container": names.groupBox_OCC_QmlUtils_OCQuickWidget, - "id": "copyToClipboardButton", - "type": "Button", - "visible": True, - } - TLS_CERT_WINDOW = { - "name": "OCC__TlsErrorDialog", - "type": "OCC::TlsErrorDialog", - "visible": 1, - } - ACCEPT_CERTIFICATE_YES = { - "text": "Yes", - "type": "QPushButton", - "visible": 1, - "window": TLS_CERT_WINDOW, - } - - def __init__(self, occurrence=1): - if occurrence > 1: - self.TLS_CERT_WINDOW.update({"occurrence": occurrence}) + LOGIN_CONTAINER = SimpleNamespace(by=None, selector=None) + LOGIN_USER_LABEL = SimpleNamespace(by=None, selector=None) + USERNAME_BOX = SimpleNamespace(by=None, selector=None) + LOGOUT_BUTTON = SimpleNamespace(by=None, selector=None) def get_username(self): # Parse username from the login label: @@ -58,9 +19,7 @@ def get_username(self): return username.capitalize() def oidc_relogin(self, username, password): - # wait 500ms for copy button to fully load - squish.snooze(1 / 2) - squish.mouseClick(squish.waitForObject(self.COPY_URL_TO_CLIPBOARD_BUTTON)) + AccountConnectionWizard.copy_login_url() authorize_via_webui(username, password) def relogin(self, username, password, oauth=False): @@ -70,4 +29,4 @@ def login_after_setup(self, username, password): self.oidc_relogin(username, password) def accept_certificate(self): - squish.clickButton(squish.waitForObject(self.ACCEPT_CERTIFICATE_YES)) + AccountConnectionWizard.accept_certificate() diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 2e495a3da9..3cce34751d 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -10,7 +10,7 @@ class Toolbar: TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) - ADD_ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) + ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") ACTIVITY_BUTTON = SimpleNamespace(by=None, selector=None) SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) QUIT_BUTTON = SimpleNamespace(by=None, selector=None) @@ -41,7 +41,10 @@ def open_activity(): @staticmethod def open_new_account_setup(): - squish.mouseClick(squish.waitForObject(Toolbar.ADD_ACCOUNT_BUTTON)) + app().find_element( + Toolbar.ADD_ACCOUNT_BUTTON.by, + Toolbar.ADD_ACCOUNT_BUTTON.selector, + ).click() @staticmethod def open_account(displayname): @@ -95,8 +98,14 @@ def get_accounts(): @staticmethod def get_account(display_name): - accounts, selectors = Toolbar.get_accounts() - return accounts.get(display_name), selectors.get(display_name) + server_host = urlparse(get_config('localBackendUrl')).netloc + account_label = f"{display_name}@{server_host}" + account = None + try: + account = app().find_element(By.NAME, account_label) + except: + pass + return account @staticmethod def get_active_account(): @@ -108,11 +117,10 @@ def get_active_account(): @staticmethod def account_has_focus(display_name): - account, selector = Toolbar.get_account(display_name) - return account["current"] and squish.waitForObject(selector).checked + account = Toolbar.get_account(display_name) + return account.get_attribute("checked") == "true" @staticmethod def account_exists(display_name): - server_host = urlparse(get_config('localBackendUrl')).netloc - account_label = f"{display_name}@{server_host}" - app().find_element(By.NAME, account_label) + account = Toolbar.get_account(display_name) + return account is not None diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 2a440f8e2e..958646f7db 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -14,4 +14,5 @@ playwright==1.58.*; python_version >= "3.10" behave==1.3.*; python_version >= "3.10" Appium-Python-Client==5.3.*; python_version >= "3.10" Flask==3.0.*; python_version >= "3.10" and sys_platform == 'linux' -numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' \ No newline at end of file +numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' +sure==2.0.*; python_version >= "3.10" \ No newline at end of file diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index d424a975ff..9e4b0a7168 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -1,17 +1,18 @@ from behave import given as Given, when as When, then as Then +from sure import expect from pageObjects.AccountConnectionWizard import AccountConnectionWizard - from pageObjects.Toolbar import Toolbar - +from pageObjects.EnterPassword import EnterPassword from helpers.SetupClientHelper import ( start_client, + setup_client, substitute_inline_codes, get_client_details, get_resource_path, ) - from helpers.SyncHelper import wait_for_initial_sync_to_complete +from helpers.UserHelper import get_displayname_for_user, get_password_for_user @Given('the user has started the client') @@ -31,7 +32,7 @@ def step(context): @Then('the account with displayname "{displayname}" should be displayed') def step(context, displayname): displayname = substitute_inline_codes(displayname) - Toolbar.account_exists(displayname) + expect(Toolbar.account_exists(displayname)).to.be.true @Then('the account with displayname "|any|" should not be displayed') @@ -46,7 +47,7 @@ def step(context, displayname): ) -@Given('user "|any|" has set up a client with default settings') +@Given('user "{username}" has set up a client with default settings') def step(context, username): password = get_password_for_user(username) setup_client(username) @@ -241,11 +242,10 @@ def step(context): Toolbar.quit_opencloud() -@Then('"|any|" account should be opened') +@Then('"{displayname}" 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") + expect(Toolbar.account_has_focus(displayname)).to.be.true @Then( From 2beb4b80b2b51a8fed10594f3ae478871bf267a6 Mon Sep 17 00:00:00 2001 From: Pradip Subedi Date: Tue, 21 Apr 2026 14:36:42 +0545 Subject: [PATCH 26/75] test(gui): port tst_deleteFilesFolders suite (#872) * test: port tst_deleteFilesFolders suite * test: fix placement of files * test: use ensure instead of assert --- .../delete-files-folders/delete.feature} | 6 +- test/gui/helpers/SyncHelper.py | 2 +- test/gui/shared/steps/file_context.py | 432 ----------------- test/gui/shared/steps/sync_context.py | 321 ------------- test/gui/steps/file_context.py | 446 ++++++++++++++++++ test/gui/steps/server_context.py | 58 +-- test/gui/steps/sync_context.py | 320 +++++++++++++ test/gui/tst_deleteFilesFolders/test.py | 8 - 8 files changed, 800 insertions(+), 793 deletions(-) rename test/gui/{tst_deleteFilesFolders/test.feature => features/delete-files-folders/delete.feature} (98%) delete mode 100644 test/gui/shared/steps/file_context.py delete mode 100644 test/gui/shared/steps/sync_context.py create mode 100644 test/gui/steps/file_context.py create mode 100644 test/gui/steps/sync_context.py delete mode 100644 test/gui/tst_deleteFilesFolders/test.py diff --git a/test/gui/tst_deleteFilesFolders/test.feature b/test/gui/features/delete-files-folders/delete.feature similarity index 98% rename from test/gui/tst_deleteFilesFolders/test.feature rename to test/gui/features/delete-files-folders/delete.feature index d2c217376a..5e70c2629b 100644 --- a/test/gui/tst_deleteFilesFolders/test.feature +++ b/test/gui/features/delete-files-folders/delete.feature @@ -6,7 +6,7 @@ Feature: deleting files and folders Background: Given user "Alice" has been created in the server with default attributes - @issue-9439 @smoke @skip + @issue-9439 @smoke Scenario Outline: Delete a file Given user "Alice" has uploaded file with content "openCloud test text file 0" to "" in the server And user "Alice" has set up a client with default settings @@ -19,7 +19,7 @@ Feature: deleting files and folders | textfile0-with-name-more-than-20-characters | | ~`!@#$^&()-_=+{[}];',textfile.txt | - @issue-9439 @smoke @skip + @issue-9439 @smoke Scenario Outline: Delete a folder Given user "Alice" has created folder "" in the server And user "Alice" has set up a client with default settings @@ -31,7 +31,7 @@ Feature: deleting files and folders | simple-empty-folder | | simple-folder-with-name-more-than-20-characters | - @smoke @skip + @smoke Scenario: Delete a file and a folder Given user "Alice" has uploaded file with content "test file 1" to "textfile1.txt" in the server And user "Alice" has uploaded file with content "test file 2" to "textfile2.txt" in the server diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 34b4e7bb46..3504bdee46 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -326,7 +326,7 @@ def wait_for_resource_to_have_sync_error(resource, resource_type): def wait_for_client_to_be_ready(): global WAITED_AFTER_SYNC if not WAITED_AFTER_SYNC: - squish.snooze(get_config('minSyncTimeout')) + time.sleep(get_config('minSyncTimeout')) WAITED_AFTER_SYNC = True diff --git a/test/gui/shared/steps/file_context.py b/test/gui/shared/steps/file_context.py deleted file mode 100644 index 20e94a6998..0000000000 --- a/test/gui/shared/steps/file_context.py +++ /dev/null @@ -1,432 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import re -import builtins -import shutil -import zipfile -from os.path import isfile, join, isdir -import squish - -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 -from helpers.ConfigHelper import get_config, is_windows -from helpers.FilesHelper import ( - build_conflicted_regex, - sanitize_path, - can_read, - can_write, - read_file_content, - get_size_in_bytes, - prefix_path_namespace, - remember_path, - convert_path_separators_for_os, - get_file_for_upload, -) - - -def folder_exists(folder_path, timeout=1000): - return squish.waitFor( - lambda: isdir(sanitize_path(folder_path)), - timeout, - ) - - -def file_exists(file_path, timeout=1000): - return squish.waitFor( - lambda: isfile(sanitize_path(file_path)), - timeout, - ) - - -# To create folders in a temporary directory, we set is_temp_folder True -# And if is_temp_folder is True, the create_folder function create folders in tempFolderPath -def create_folder(foldername, username=None, is_temp_folder=False): - if is_temp_folder: - folder_path = join(get_config('tempFolderPath'), foldername) - else: - folder_path = get_resource_path(foldername, username) - os.makedirs(prefix_path_namespace(convert_path_separators_for_os(folder_path))) - - -def rename_file_folder(source, destination): - source = get_resource_path(source) - destination = get_resource_path(destination) - os.rename(source, destination) - - -def create_file_with_size(filename, filesize, is_temp_folder=False): - if is_temp_folder: - file = join(get_config('tempFolderPath'), filename) - else: - file = get_resource_path(filename) - with open(prefix_path_namespace(file), 'wb') as f: - f.seek(get_size_in_bytes(filesize) - 1) - f.write(b'\0') - - -def write_file(resource, content): - with open(prefix_path_namespace(resource), 'w', encoding='utf-8') as f: - f.write(content) - - -def wait_and_write_file(path, content): - wait_for_client_to_be_ready() - listen_sync_status_for_item(get_resource_path(path), 'FILE') - write_file(path, content) - - -def wait_and_try_to_write_file(resource, content): - wait_for_client_to_be_ready() - listen_sync_status_for_item(get_resource_path(resource), 'FILE') - try: - write_file(resource, content) - except: - pass - - -def create_zip(resources, zip_file_name, cwd=''): - os.chdir(cwd) - with zipfile.ZipFile(zip_file_name, 'w') as zipped_file: - for resource in resources: - zipped_file.write(resource) - - -def extract_zip(zip_file_path, destination_dir): - with zipfile.ZipFile(zip_file_path, 'r') as zip_file: - zip_file.extractall(destination_dir) - - -def add_copy_suffix(resource_path, resource_type): - suffix = ' (Copy)' - if resource_type == 'file': - source_dir = resource_path.rsplit('.', 1) - return source_dir[0] + suffix + '.' + source_dir[-1] - return resource_path + suffix - - -def copy_resource(resource_type, source, destination, from_files_for_upload=False): - if from_files_for_upload: - source = get_file_for_upload(source) - else: - source = get_resource_path(source) - destination = get_resource_path(destination) - if source == destination and destination != '/': - destination = add_copy_suffix(source, resource_type) - - wait_for_client_to_be_ready() - listen_sync_status_for_item(destination, resource_type) - if resource_type == 'folder': - return shutil.copytree(source, destination) - return shutil.copy2(source, destination) - - -def move_resource(username, resource_type, source, destination, is_temp_folder=False): - if not is_temp_folder: - source = get_resource_path(source, username) - if destination == '/': - destination = '' - destination = get_resource_path(destination, username) - - wait_for_client_to_be_ready() - listen_sync_status_for_item(destination, resource_type) - shutil.move(source, destination) - - -def deleteResource(resource, resource_type): - resource_path = sanitize_path(get_resource_path(resource)) - if resource_type == 'file': - os.remove(resource_path) - else: - shutil.rmtree(resource_path) - - -@When( - 'user "|any|" creates a file "|any|" with the following content inside the sync folder' -) -def step(context, username, filename): - file_content = '\n'.join(context.multiLineText) - file = get_resource_path(filename, username) - wait_and_write_file(convert_path_separators_for_os(file), file_content) - - -@When('user "|any|" creates a folder "|any|" inside the sync folder') -def step(context, username, foldername): - wait_for_client_to_be_ready() - create_folder(foldername, username) - - -@Given('user "|any|" has created a folder "|any|" inside the sync folder') -def step(context, username, foldername): - create_folder(foldername, username) - - -@When('user "|any|" creates a file "|any|" with size "|any|" inside the sync folder') -def step(context, _, filename, filesize): - create_file_with_size(filename, filesize) - - -@When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) -def step(context, resource_type, resource_name, destination_dir): - copy_resource(resource_type, resource_name, destination_dir, False) - - -@When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) -def step(context, resource_type, resource_name): - copy_resource(resource_type, resource_name, resource_name, False) - - -@When(r'the user renames a (?:file|folder) "([^"]*)" to "([^"]*)"', regexp=True) -def step(context, source, destination): - wait_for_client_to_be_ready() - rename_file_folder(source, destination) - - -@Then('the file "|any|" should exist on the file system with the following content') -def step(context, file_path): - expected = '\n'.join(context.multiLineText) - file_path = get_resource_path(file_path) - with open(file_path, 'r', encoding='utf-8') as f: - contents = f.read() - test.compare( - expected, - contents, - 'file expected to exist with content ' - + expected - + ' but does not have the expected content', - ) - - -@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 - ) - - 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', - ) - - -@Given('the user has changed the content of local file "|any|" to:') -def step(context, filename): - file_content = '\n'.join(context.multiLineText) - wait_and_write_file(get_resource_path(filename), file_content) - - -@Then( - 'a conflict file for "|any|" should exist on the file system with the following content' -) -def step(context, filename): - expected = '\n'.join(context.multiLineText) - - onlyfiles = [ - f for f in os.listdir(get_resource_path()) if isfile(get_resource_path(f)) - ] - found = False - pattern = re.compile(build_conflicted_regex(filename)) - for file in onlyfiles: - if pattern.match(file): - with open(get_resource_path(file), 'r', encoding='utf-8') as f: - if f.read() == expected: - found = True - break - - if not found: - raise AssertionError('Conflict file not found with given name') - - -@When('the user overwrites the file "|any|" with content "|any|"') -def step(context, resource, content): - resource = get_resource_path(resource) - wait_and_write_file(resource, content) - - -@When('the user tries to overwrite the file "|any|" with content "|any|"') -def step(context, resource, content): - resource = get_resource_path(resource) - wait_and_try_to_write_file(resource, content) - - -@When('user "|any|" tries to overwrite the file "|any|" with content "|any|"') -def step(context, user, resource, content): - resource = get_resource_path(resource, user) - wait_and_try_to_write_file(resource, content) - - -@When(r'the user deletes the (file|folder) "([^"]*)"', regexp=True) -def step(context, item_type, resource): - wait_for_client_to_be_ready() - deleteResource(resource, item_type) - - -@When('user "|any|" creates the following files inside the sync folder:') -def step(context, username): - wait_for_client_to_be_ready() - - for row in context.table[1:]: - file = get_resource_path(row[0], username) - write_file(file, '') - - -@Given('the user has created a folder "|any|" in temp folder') -def step(context, folder_name): - create_folder(folder_name, is_temp_folder=True) - - -@Given( - 'the user has created "|any|" files each of size "|any|" bytes inside folder "|any|" in temp folder' -) -def step(context, file_number, file_size, folder_name): - current_sync_path = get_temp_resource_path(folder_name) - if folder_exists(current_sync_path): - file_size = builtins.int(file_size) - for i in range(0, builtins.int(file_number)): - file_name = f'file{i}.txt' - create_file_with_size(join(current_sync_path, file_name), file_size, True) - else: - raise FileNotFoundError( - f"Folder '{folder_name}' does not exist in the temp folder" - ) - - -@When( - r'user "([^"]*)" reads the content of file "([^"]*)"', - regexp=True, -) -def step(context, username, file): - file_path = get_resource_path(file, username) - with open(file_path, 'r') as f: - f.read() - - -@When( - r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', - regexp=True, -) -def step(context, username, resource_type, resource_name): - source_dir = join(get_config('tempFolderPath'), resource_name) - move_resource(username, resource_type, source_dir, '/', True) - - -@When( - r'user "([^"]*)" moves (folder|file) "([^"]*)" to the temp folder', - regexp=True, -) -def step(context, username, resource_type, resource_name): - destination= join(get_config('tempFolderPath'), resource_name) - move_resource(username, resource_type, resource_name, destination) - - -@When( - r'user "([^"]*)" moves (file|folder) "([^"]*)" to "([^"]*)" in the sync folder', - regexp=True, -) -def step(context, username, resource_type, source, destination): - move_resource(username, resource_type, source, destination) - - -@Then('user "|any|" should be able to open the file "|any|" on the file system') -def step(context, user, file_name): - file_path = get_resource_path(file_name, user) - test.compare(can_read(file_path), True, 'File should be readable') - - -@Then('as "|any|" the file "|any|" should have content "|any|" on the file system') -def step(context, user, file_name, content): - file_path = get_resource_path(file_name, user) - file_content = read_file_content(file_path) - test.compare(file_content, content, 'Comparing file content') - - -@Then('user "|any|" should not be able to edit the file "|any|" on the file system') -def step(context, user, file_name): - file_path = get_resource_path(file_name, user) - test.compare(not can_write(file_path), True, 'File should not be writable') - - -@Given( - 'the user has created a zip file "|any|" with the following resources in the temp folder' -) -def step(context, zip_file_name): - resource_list = [] - - for row in context.table[1:]: - resource_list.append(row[0]) - resource = join(get_config('tempFolderPath'), row[0]) - if row[1] == 'folder': - os.makedirs(resource) - elif row[1] == 'file': - content = '' - if len(row) > 2 and row[2]: - content = row[2] - write_file(resource, content) - create_zip(resource_list, zip_file_name, get_config('tempFolderPath')) - - -@When('user "|any|" unzips the zip file "|any|" inside the sync root') -def step(context, username, zip_file_name): - destination_dir = get_resource_path('/', username) - zip_file_path = join(destination_dir, zip_file_name) - extract_zip(zip_file_path, destination_dir) - - -@When('user "|any|" copies file "|any|" to temp folder') -def step(context, username, source): - wait_for_client_to_be_ready() - source_dir = get_resource_path(source, username) - destination_dir = get_temp_resource_path(source) - shutil.copy2(source_dir, destination_dir) - - -@Given('the user has created folder "|any|" in the default home path') -def step(context, folder_name): - folder_path = join(get_config('home_dir'), folder_name) - os.makedirs(prefix_path_namespace(folder_path)) - remember_path(folder_path) - # when account is added, folder with suffix will be created - remember_path(f'{folder_path} (2)') - - -@Given( - r'the user has copied file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', - regexp=True, -) -def step(context, resource_name, destination): - copy_resource('file', resource_name, destination, True) - - -@When( - r'the user copies file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', - regexp=True, -) -def step(context, resource_name, destination): - copy_resource('file', resource_name, destination, True) - - -@When('the user deletes the following files') -def step(context): - wait_for_client_to_be_ready() - - for row in context.table[1:]: - filename = row[0] - deleteResource(filename, 'file') - - -@Given('user "|any|" has created a file "|any|" with size "|any|" in the sync folder') -def step(context, _, filename, filesize): - create_file_with_size(filename, filesize) diff --git a/test/gui/shared/steps/sync_context.py b/test/gui/shared/steps/sync_context.py deleted file mode 100644 index 7cafaaa9a3..0000000000 --- a/test/gui/shared/steps/sync_context.py +++ /dev/null @@ -1,321 +0,0 @@ -import squish - -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 -from helpers.SyncHelper import ( - wait_for_resource_to_sync, - wait_for_resource_to_have_sync_error, -) -from helpers.SetupClientHelper import ( - get_temp_resource_path, - set_current_user_sync_path, - substitute_inline_codes, - get_resource_path, -) -from helpers.FilesHelper import convert_path_separators_for_os - - -@Given('the user has paused the file sync') -def step(context): - SyncConnection.pause_sync() - - -@When('the user resumes the file sync on the client') -def step(context): - SyncConnection.resume_sync() - - -@When('the user force syncs the files') -def step(context): - SyncConnection.force_sync() - - -@When('the user waits for the files to sync') -def step(context): - wait_for_resource_to_sync(get_resource_path('/')) - - -@When(r'the user waits for (file|folder) "([^"]*)" to be synced', regexp=True) -def step(context, resource_type, resource): - resource = get_resource_path(resource) - wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) - - -@When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) -def step(context, resource_type, resource): - resource = get_resource_path(resource) - wait_for_resource_to_have_sync_error(resource, resource_type) - - -@When( - r'user "([^"]*)" waits for (file|folder) "([^"]*)" to have sync error', regexp=True -) -def step(context, username, resource_type, resource): - resource = get_resource_path(resource, username) - wait_for_resource_to_have_sync_error(resource, resource_type) - - -@Then('the "|any|" button should be available') -def step(context, item): - SyncConnection.open_menu() - SyncConnection.has_menu_item(item) - - -@Then('the "|any|" button should not be available') -def step(context, item): - SyncConnection.open_menu() - test.compare( - SyncConnection.menu_item_exists(item), - False, - f'Menu item "{item}" does not exist.', - ) - - -@When('the user clicks on the activity tab') -def step(context): - Toolbar.open_activity() - - -@Then('the table of conflict warnings should include file "|any|"') -def step(context, filename): - Activity.check_file_exist(filename) - - -@Then('the file "|any|" should be blacklisted') -def step(context, filename): - test.compare( - True, Activity.is_resource_blacklisted(filename), 'File is Blacklisted' - ) - - -@Then('the file "|any|" should be ignored') -def step(context, filename): - test.compare(True, Activity.is_resource_ignored(filename), 'File is Ignored') - - -@Then('the file "|any|" should be excluded') -def step(context, filename): - test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') - - -@When('the user selects "|any|" tab in the activity') -def step(context, tab_name): - Activity.click_tab(tab_name) - - -@Then('the toolbar should have the following tabs:') -def step(context): - for tab_name in context.table: - Toolbar.has_item(tab_name[0]) - - -@When('the user selects the following folders to sync:') -def step(context): - folders = [] - for row in context.table[1:]: - folders.append(row[0]) - SyncConnectionWizard.select_folders_to_sync(folders, new_sync_connection_wizard=True) - - -@When('the user sorts the folder list by "|any|"') -def step(context, header_text): - if (header_text := header_text.capitalize()) in ['Size', 'Name']: - SyncConnectionWizard.sort_by(header_text) - else: - raise ValueError("Sorting by '" + header_text + "' is not supported.") - - -@Then('the sync all checkbox should be checked') -def step(context): - test.compare( - SyncConnectionWizard.is_root_folder_checked(), - True, - 'Sync all checkbox is checked', - ) - - -@Then('the folders should be in the following order:') -def step(context): - row_index = 0 - for row in context.table[1:]: - expected_folder = row[0] - actual_folder = SyncConnectionWizard.get_item_name_from_row(row_index) - test.compare(actual_folder, expected_folder) - - row_index += 1 - - -@When('the user selects "|any|" space in sync connection wizard') -def step(context, space_name): - SyncConnectionWizard.select_space(space_name) - SyncConnectionWizard.next_step() - set_config('syncConnectionName', space_name) - - -@When('the user sets the sync path in sync connection wizard') -def step(context): - SyncConnectionWizard.set_sync_path() - - -@When( - 'the user sets the temp folder "|any|" as local sync path in sync connection wizard' -) -def step(context, folder_name): - sync_path = get_temp_resource_path(folder_name) - SyncConnectionWizard.set_sync_path(sync_path) - set_current_user_sync_path(sync_path) - - -@When('the user syncs the "|any|" space') -def step(context, space_name): - SyncConnectionWizard.sync_space(space_name) - - -@Then('the settings tab should have the following options in the general section:') -def step(context): - for item in context.table: - Settings.check_general_option(item[0]) - - -@Then('the settings tab should have the following options in the advanced section:') -def step(context): - for item in context.table: - Settings.check_advanced_option(item[0]) - - -@Then('the settings tab should have the following options in the network section:') -def step(context): - for item in context.table: - Settings.check_network_option(item[0]) - - -@When('the user opens the about dialog') -def step(context): - Settings.open_about_button() - - -@Then('the about dialog should be opened') -def step(context): - Settings.wait_for_about_dialog_to_be_visible() - - -@When('the user adds the folder sync connection') -def step(context): - SyncConnectionWizard.add_sync_connection() - - -@When('user unselects all the remote folders') -def step(context): - SyncConnectionWizard.deselect_all_remote_folders() - - -@Then('the sync folder list should be empty') -def step(context): - test.compare( - 0, - SyncConnection.get_folder_connection_count(), - 'Sync connections should be empty', - ) - - -@When('the user navigates back in the sync connection wizard') -def step(context): - SyncConnectionWizard.back() - - -@When('the user removes the folder sync connection') -def step(context): - SyncConnection.remove_folder_sync_connection() - SyncConnection.confirm_folder_sync_connection_removal() - - -@Then('the file "|any|" should have status "|any|" in the activity tab') -def step(context, file_name, status): - Activity.has_sync_status(file_name, status) - - -@When('the user opens the sync connection wizard') -def step(context): - SyncConnectionWizard.open_sync_connection_wizard() - - -@Then('the button to open sync connection wizard should be disabled') -def step(context): - test.compare( - False, - SyncConnectionWizard.is_add_sync_folder_button_enabled(), - 'Button to open sync connection wizard should be disabled', - ) - - -@When('the user checks the activities of account "|any|"') -def step(context, account): - account = substitute_inline_codes(account) - Activity.select_synced_filter(account) - - -@Then('the following activities should be displayed in synced table') -def step(context): - for row in context.table[1:]: - resource = row[0] - action = row[1] - account = substitute_inline_codes(row[2]) - test.compare( - Activity.check_synced_table(resource, action, account), - True, - 'Resource should be displayed in the synced table', - ) - - -@Then(r'the following activities (should|should not) be displayed in not synced table', regexp=True) -def step(context, should_or_should_not): - expected = should_or_should_not == "should" - for row in context.table[1:]: - resource = row[0] - status = row[1] - account = substitute_inline_codes(row[2]) - test.compare( - Activity.check_not_synced_table(resource, status, account), - expected, - 'Resource should be displayed in the not synced table', - ) - - -@When('the user unchecks the "|any|" filter') -def step(context, filter_option): - Activity.select_not_synced_filter(filter_option) - - -@Then('the following error message should appear in the client') -def step(context): - expected_error_message = '\n'.join(context.multiLineText) - - actual_error_message = SyncConnection.get_permission_error_message() - - # wait for error message to disappear - SyncConnection.wait_for_error_label(False) - - test.compare( - actual_error_message, - expected_error_message, - f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"' - ) - - -@Given('the user has waited for "|any|" seconds') -def step(context, wait_for): - squish.snooze(float(wait_for)) - - -@When('the user unselects the following folders to sync in "Choose what to sync" window:') -def step(context): - SyncConnection.choose_what_to_sync() - folders = [] - for row in context.table[1:]: - folders.append(row[0]) - SyncConnectionWizard.unselect_folders_to_sync(folders, new_sync_connection_wizard=False) diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py new file mode 100644 index 0000000000..80c5fdb6f6 --- /dev/null +++ b/test/gui/steps/file_context.py @@ -0,0 +1,446 @@ +# # -*- coding: utf-8 -*- +import os +# 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 +) +from helpers.SyncHelper import ( + wait_for_client_to_be_ready, + # listen_sync_status_for_item +) +# from helpers.ConfigHelper import get_config, is_windows +from helpers.FilesHelper import ( + # build_conflicted_regex, + sanitize_path, + # can_read, + # can_write, + # read_file_content, + # get_size_in_bytes, + # prefix_path_namespace, + # remember_path, + # convert_path_separators_for_os, + # 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 file_exists(file_path, timeout=1000): +# return squish.waitFor( +# lambda: isfile(sanitize_path(file_path)), +# timeout, +# ) + + +# To create folders in a temporary directory, we set is_temp_folder True +# And if is_temp_folder is True, the create_folder function create folders in tempFolderPath +# def create_folder(foldername, username=None, is_temp_folder=False): +# if is_temp_folder: +# folder_path = join(get_config('tempFolderPath'), foldername) +# else: +# folder_path = get_resource_path(foldername, username) +# os.makedirs(prefix_path_namespace(convert_path_separators_for_os(folder_path))) + + +# def rename_file_folder(source, destination): +# source = get_resource_path(source) +# destination = get_resource_path(destination) +# os.rename(source, destination) + + +# def create_file_with_size(filename, filesize, is_temp_folder=False): +# if is_temp_folder: +# file = join(get_config('tempFolderPath'), filename) +# else: +# file = get_resource_path(filename) +# with open(prefix_path_namespace(file), 'wb') as f: +# f.seek(get_size_in_bytes(filesize) - 1) +# f.write(b'\0') + + +# def write_file(resource, content): +# with open(prefix_path_namespace(resource), 'w', encoding='utf-8') as f: +# f.write(content) + + +# def wait_and_write_file(path, content): +# wait_for_client_to_be_ready() +# listen_sync_status_for_item(get_resource_path(path), 'FILE') +# write_file(path, content) + + +# def wait_and_try_to_write_file(resource, content): +# wait_for_client_to_be_ready() +# listen_sync_status_for_item(get_resource_path(resource), 'FILE') +# try: +# write_file(resource, content) +# except: +# pass + + +# def create_zip(resources, zip_file_name, cwd=''): +# os.chdir(cwd) +# with zipfile.ZipFile(zip_file_name, 'w') as zipped_file: +# for resource in resources: +# zipped_file.write(resource) + + +# def extract_zip(zip_file_path, destination_dir): +# with zipfile.ZipFile(zip_file_path, 'r') as zip_file: +# zip_file.extractall(destination_dir) + + +# def add_copy_suffix(resource_path, resource_type): +# suffix = ' (Copy)' +# if resource_type == 'file': +# source_dir = resource_path.rsplit('.', 1) +# return source_dir[0] + suffix + '.' + source_dir[-1] +# return resource_path + suffix + + +# def copy_resource(resource_type, source, destination, from_files_for_upload=False): +# if from_files_for_upload: +# source = get_file_for_upload(source) +# else: +# source = get_resource_path(source) +# destination = get_resource_path(destination) +# if source == destination and destination != '/': +# destination = add_copy_suffix(source, resource_type) + +# wait_for_client_to_be_ready() +# listen_sync_status_for_item(destination, resource_type) +# if resource_type == 'folder': +# return shutil.copytree(source, destination) +# return shutil.copy2(source, destination) + + +# def move_resource(username, resource_type, source, destination, is_temp_folder=False): +# if not is_temp_folder: +# source = get_resource_path(source, username) +# if destination == '/': +# destination = '' +# destination = get_resource_path(destination, username) + +# wait_for_client_to_be_ready() +# listen_sync_status_for_item(destination, resource_type) +# shutil.move(source, destination) + + +def deleteResource(resource, resource_type): + resource_path = sanitize_path(get_resource_path(resource)) + if resource_type == 'file': + os.remove(resource_path) + else: + shutil.rmtree(resource_path) + + +# @When( +# 'user "|any|" creates a file "|any|" with the following content inside the sync folder' +# ) +# def step(context, username, filename): +# file_content = '\n'.join(context.multiLineText) +# file = get_resource_path(filename, username) +# wait_and_write_file(convert_path_separators_for_os(file), file_content) + + +# @When('user "|any|" creates a folder "|any|" inside the sync folder') +# def step(context, username, foldername): +# wait_for_client_to_be_ready() +# create_folder(foldername, username) + + +# @Given('user "|any|" has created a folder "|any|" inside the sync folder') +# def step(context, username, foldername): +# create_folder(foldername, username) + + +# @When('user "|any|" creates a file "|any|" with size "|any|" inside the sync folder') +# def step(context, _, filename, filesize): +# create_file_with_size(filename, filesize) + + +# @When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) +# def step(context, resource_type, resource_name, destination_dir): +# copy_resource(resource_type, resource_name, destination_dir, False) + + +# @When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) +# def step(context, resource_type, resource_name): +# copy_resource(resource_type, resource_name, resource_name, False) + + +# @When(r'the user renames a (?:file|folder) "([^"]*)" to "([^"]*)"', regexp=True) +# def step(context, source, destination): +# wait_for_client_to_be_ready() +# rename_file_folder(source, destination) + + +# @Then('the file "|any|" should exist on the file system with the following content') +# def step(context, file_path): +# expected = '\n'.join(context.multiLineText) +# file_path = get_resource_path(file_path) +# with open(file_path, 'r', encoding='utf-8') as f: +# contents = f.read() +# test.compare( +# expected, +# contents, +# 'file expected to exist with content ' +# + expected +# + ' but does not have the expected content', +# ) + + +# @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 +# ) + +# 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', +# ) + + +# @Given('the user has changed the content of local file "|any|" to:') +# def step(context, filename): +# file_content = '\n'.join(context.multiLineText) +# wait_and_write_file(get_resource_path(filename), file_content) + + +# @Then( +# 'a conflict file for "|any|" should exist on the file system with the following content' +# ) +# def step(context, filename): +# expected = '\n'.join(context.multiLineText) + +# onlyfiles = [ +# f for f in os.listdir(get_resource_path()) if isfile(get_resource_path(f)) +# ] +# found = False +# pattern = re.compile(build_conflicted_regex(filename)) +# for file in onlyfiles: +# if pattern.match(file): +# with open(get_resource_path(file), 'r', encoding='utf-8') as f: +# if f.read() == expected: +# found = True +# break + +# if not found: +# raise AssertionError('Conflict file not found with given name') + + +# @When('the user overwrites the file "|any|" with content "|any|"') +# def step(context, resource, content): +# resource = get_resource_path(resource) +# wait_and_write_file(resource, content) + + +# @When('the user tries to overwrite the file "|any|" with content "|any|"') +# def step(context, resource, content): +# resource = get_resource_path(resource) +# wait_and_try_to_write_file(resource, content) + + +# @When('user "|any|" tries to overwrite the file "|any|" with content "|any|"') +# def step(context, user, resource, content): +# resource = get_resource_path(resource, user) +# wait_and_try_to_write_file(resource, content) + + +@When('the user deletes the {resource_type:ResourceType} "{resource_name}"') +def step(context, resource_type, resource_name): + wait_for_client_to_be_ready() + print(f"Deleting {resource_type} '{resource_name}'") + deleteResource(resource_name, resource_type) + + +# @When('user "|any|" creates the following files inside the sync folder:') +# def step(context, username): +# wait_for_client_to_be_ready() + +# for row in context.table[1:]: +# file = get_resource_path(row[0], username) +# write_file(file, '') + + +# @Given('the user has created a folder "|any|" in temp folder') +# def step(context, folder_name): +# create_folder(folder_name, is_temp_folder=True) + + +# @Given( +# 'the user has created "|any|" files each of size "|any|" bytes inside folder "|any|" in temp folder' +# ) +# def step(context, file_number, file_size, folder_name): +# current_sync_path = get_temp_resource_path(folder_name) +# if folder_exists(current_sync_path): +# file_size = builtins.int(file_size) +# for i in range(0, builtins.int(file_number)): +# file_name = f'file{i}.txt' +# create_file_with_size(join(current_sync_path, file_name), file_size, True) +# else: +# raise FileNotFoundError( +# f"Folder '{folder_name}' does not exist in the temp folder" +# ) + + +# @When( +# r'user "([^"]*)" reads the content of file "([^"]*)"', +# regexp=True, +# ) +# def step(context, username, file): +# file_path = get_resource_path(file, username) +# with open(file_path, 'r') as f: +# f.read() + + +# @When( +# r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', +# regexp=True, +# ) +# def step(context, username, resource_type, resource_name): +# source_dir = join(get_config('tempFolderPath'), resource_name) +# move_resource(username, resource_type, source_dir, '/', True) + + +# @When( +# r'user "([^"]*)" moves (folder|file) "([^"]*)" to the temp folder', +# regexp=True, +# ) +# def step(context, username, resource_type, resource_name): +# destination= join(get_config('tempFolderPath'), resource_name) +# move_resource(username, resource_type, resource_name, destination) + + +# @When( +# r'user "([^"]*)" moves (file|folder) "([^"]*)" to "([^"]*)" in the sync folder', +# regexp=True, +# ) +# def step(context, username, resource_type, source, destination): +# move_resource(username, resource_type, source, destination) + + +# @Then('user "|any|" should be able to open the file "|any|" on the file system') +# def step(context, user, file_name): +# file_path = get_resource_path(file_name, user) +# test.compare(can_read(file_path), True, 'File should be readable') + + +# @Then('as "|any|" the file "|any|" should have content "|any|" on the file system') +# def step(context, user, file_name, content): +# file_path = get_resource_path(file_name, user) +# file_content = read_file_content(file_path) +# test.compare(file_content, content, 'Comparing file content') + + +# @Then('user "|any|" should not be able to edit the file "|any|" on the file system') +# def step(context, user, file_name): +# file_path = get_resource_path(file_name, user) +# test.compare(not can_write(file_path), True, 'File should not be writable') + + +# @Given( +# 'the user has created a zip file "|any|" with the following resources in the temp folder' +# ) +# def step(context, zip_file_name): +# resource_list = [] + +# for row in context.table[1:]: +# resource_list.append(row[0]) +# resource = join(get_config('tempFolderPath'), row[0]) +# if row[1] == 'folder': +# os.makedirs(resource) +# elif row[1] == 'file': +# content = '' +# if len(row) > 2 and row[2]: +# content = row[2] +# write_file(resource, content) +# create_zip(resource_list, zip_file_name, get_config('tempFolderPath')) + + +# @When('user "|any|" unzips the zip file "|any|" inside the sync root') +# def step(context, username, zip_file_name): +# destination_dir = get_resource_path('/', username) +# zip_file_path = join(destination_dir, zip_file_name) +# extract_zip(zip_file_path, destination_dir) + + +# @When('user "|any|" copies file "|any|" to temp folder') +# def step(context, username, source): +# wait_for_client_to_be_ready() +# source_dir = get_resource_path(source, username) +# destination_dir = get_temp_resource_path(source) +# shutil.copy2(source_dir, destination_dir) + + +# @Given('the user has created folder "|any|" in the default home path') +# def step(context, folder_name): +# folder_path = join(get_config('home_dir'), folder_name) +# os.makedirs(prefix_path_namespace(folder_path)) +# remember_path(folder_path) +# # when account is added, folder with suffix will be created +# remember_path(f'{folder_path} (2)') + + +# @Given( +# r'the user has copied file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', +# regexp=True, +# ) +# def step(context, resource_name, destination): +# copy_resource('file', resource_name, destination, True) + + +# @When( +# r'the user copies file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', +# regexp=True, +# ) +# def step(context, resource_name, destination): +# copy_resource('file', resource_name, destination, True) + + +# @When('the user deletes the following files') +# def step(context): +# wait_for_client_to_be_ready() + +# for row in context.table[1:]: +# filename = row[0] +# deleteResource(filename, 'file') + + +# @Given('user "|any|" has created a file "|any|" with size "|any|" in the sync folder') +# def step(context, _, filename, filesize): +# create_file_with_size(filename, filesize) diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index f7d85312ac..ca45d8e7d7 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -1,34 +1,32 @@ -from behave import given as Given +from behave import given as Given, then as Then, register_type +from sure import ensure +import parse +from helpers.api import provisioning, webdav_helper as webdav -from helpers.api import provisioning +@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): provisioning.create_user(user) +@Then('as "{user_name}" {resource_type:ResourceType} "{resource_name}" should not exist in the server') +def step(context, user_name, resource_type, resource_name): + resource_exists = webdav.resource_exists(user_name, resource_name) + + with ensure('{0} "{1}" should not exist, but it does', resource_type.capitalize(), resource_name): + resource_exists.should.be.false -# @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 "{user_name}" {resource_type:ResourceType} "{resource_name}" should exist in the server') +def step(context, user_name, resource_type, resource_name): + resource_exists = webdav.resource_exists(user_name, resource_name) + + with ensure('{0} "{1}" should exist, but it does not', resource_type.capitalize(), resource_name): + resource_exists.should.be.true # @Then('as "|any|" the file "|any|" should have the content "|any|" in the server') @@ -52,14 +50,18 @@ def step(context, user): # ) -# @Given('user "|any|" has created folder "|any|" in the server') -# def step(context, user, folder_name): -# webdav.create_folder(user, folder_name) +@Given('user "{user}" has created folder "{folder_name}" 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) +# @Given('user "{user}" has been created in the server with default attributes') +# def step(context, user): +# provisioning.create_user(user) + +@Given('user "{user}" has uploaded file with content "{file_content}" to "{file_name}" 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') diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py new file mode 100644 index 0000000000..7918f57bc1 --- /dev/null +++ b/test/gui/steps/sync_context.py @@ -0,0 +1,320 @@ +from behave import when as When +# 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 +from helpers.SyncHelper import ( + wait_for_resource_to_sync, + # wait_for_resource_to_have_sync_error, +) +from helpers.SetupClientHelper import ( + # get_temp_resource_path, + # set_current_user_sync_path, + # substitute_inline_codes, + get_resource_path, +) +# from helpers.FilesHelper import convert_path_separators_for_os + + +# @Given('the user has paused the file sync') +# def step(context): +# SyncConnection.pause_sync() + + +# @When('the user resumes the file sync on the client') +# def step(context): +# SyncConnection.resume_sync() + + +# @When('the user force syncs the files') +# def step(context): +# SyncConnection.force_sync() + + +@When('the user waits for the files to sync') +def step(context): + wait_for_resource_to_sync(get_resource_path('/')) + + +# @When(r'the user waits for (file|folder) "([^"]*)" to be synced', regexp=True) +# def step(context, resource_type, resource): +# resource = get_resource_path(resource) +# wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) + + +# @When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) +# def step(context, resource_type, resource): +# resource = get_resource_path(resource) +# wait_for_resource_to_have_sync_error(resource, resource_type) + + +# @When( +# r'user "([^"]*)" waits for (file|folder) "([^"]*)" to have sync error', regexp=True +# ) +# def step(context, username, resource_type, resource): +# resource = get_resource_path(resource, username) +# wait_for_resource_to_have_sync_error(resource, resource_type) + + +# @Then('the "|any|" button should be available') +# def step(context, item): +# SyncConnection.open_menu() +# SyncConnection.has_menu_item(item) + + +# @Then('the "|any|" button should not be available') +# def step(context, item): +# SyncConnection.open_menu() +# test.compare( +# SyncConnection.menu_item_exists(item), +# False, +# f'Menu item "{item}" does not exist.', +# ) + + +# @When('the user clicks on the activity tab') +# def step(context): +# Toolbar.open_activity() + + +# @Then('the table of conflict warnings should include file "|any|"') +# def step(context, filename): +# Activity.check_file_exist(filename) + + +# @Then('the file "|any|" should be blacklisted') +# def step(context, filename): +# test.compare( +# True, Activity.is_resource_blacklisted(filename), 'File is Blacklisted' +# ) + + +# @Then('the file "|any|" should be ignored') +# def step(context, filename): +# test.compare(True, Activity.is_resource_ignored(filename), 'File is Ignored') + + +# @Then('the file "|any|" should be excluded') +# def step(context, filename): +# test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') + + +# @When('the user selects "|any|" tab in the activity') +# def step(context, tab_name): +# Activity.click_tab(tab_name) + + +# @Then('the toolbar should have the following tabs:') +# def step(context): +# for tab_name in context.table: +# Toolbar.has_item(tab_name[0]) + + +# @When('the user selects the following folders to sync:') +# def step(context): +# folders = [] +# for row in context.table[1:]: +# folders.append(row[0]) +# SyncConnectionWizard.select_folders_to_sync(folders, new_sync_connection_wizard=True) + + +# @When('the user sorts the folder list by "|any|"') +# def step(context, header_text): +# if (header_text := header_text.capitalize()) in ['Size', 'Name']: +# SyncConnectionWizard.sort_by(header_text) +# else: +# raise ValueError("Sorting by '" + header_text + "' is not supported.") + + +# @Then('the sync all checkbox should be checked') +# def step(context): +# test.compare( +# SyncConnectionWizard.is_root_folder_checked(), +# True, +# 'Sync all checkbox is checked', +# ) + + +# @Then('the folders should be in the following order:') +# def step(context): +# row_index = 0 +# for row in context.table[1:]: +# expected_folder = row[0] +# actual_folder = SyncConnectionWizard.get_item_name_from_row(row_index) +# test.compare(actual_folder, expected_folder) + +# row_index += 1 + + +# @When('the user selects "|any|" space in sync connection wizard') +# def step(context, space_name): +# SyncConnectionWizard.select_space(space_name) +# SyncConnectionWizard.next_step() +# set_config('syncConnectionName', space_name) + + +# @When('the user sets the sync path in sync connection wizard') +# def step(context): +# SyncConnectionWizard.set_sync_path() + + +# @When( +# 'the user sets the temp folder "|any|" as local sync path in sync connection wizard' +# ) +# def step(context, folder_name): +# sync_path = get_temp_resource_path(folder_name) +# SyncConnectionWizard.set_sync_path(sync_path) +# set_current_user_sync_path(sync_path) + + +# @When('the user syncs the "|any|" space') +# def step(context, space_name): +# SyncConnectionWizard.sync_space(space_name) + + +# @Then('the settings tab should have the following options in the general section:') +# def step(context): +# for item in context.table: +# Settings.check_general_option(item[0]) + + +# @Then('the settings tab should have the following options in the advanced section:') +# def step(context): +# for item in context.table: +# Settings.check_advanced_option(item[0]) + + +# @Then('the settings tab should have the following options in the network section:') +# def step(context): +# for item in context.table: +# Settings.check_network_option(item[0]) + + +# @When('the user opens the about dialog') +# def step(context): +# Settings.open_about_button() + + +# @Then('the about dialog should be opened') +# def step(context): +# Settings.wait_for_about_dialog_to_be_visible() + + +# @When('the user adds the folder sync connection') +# def step(context): +# SyncConnectionWizard.add_sync_connection() + + +# @When('user unselects all the remote folders') +# def step(context): +# SyncConnectionWizard.deselect_all_remote_folders() + + +# @Then('the sync folder list should be empty') +# def step(context): +# test.compare( +# 0, +# SyncConnection.get_folder_connection_count(), +# 'Sync connections should be empty', +# ) + + +# @When('the user navigates back in the sync connection wizard') +# def step(context): +# SyncConnectionWizard.back() + + +# @When('the user removes the folder sync connection') +# def step(context): +# SyncConnection.remove_folder_sync_connection() +# SyncConnection.confirm_folder_sync_connection_removal() + + +# @Then('the file "|any|" should have status "|any|" in the activity tab') +# def step(context, file_name, status): +# Activity.has_sync_status(file_name, status) + + +# @When('the user opens the sync connection wizard') +# def step(context): +# SyncConnectionWizard.open_sync_connection_wizard() + + +# @Then('the button to open sync connection wizard should be disabled') +# def step(context): +# test.compare( +# False, +# SyncConnectionWizard.is_add_sync_folder_button_enabled(), +# 'Button to open sync connection wizard should be disabled', +# ) + + +# @When('the user checks the activities of account "|any|"') +# def step(context, account): +# account = substitute_inline_codes(account) +# Activity.select_synced_filter(account) + + +# @Then('the following activities should be displayed in synced table') +# def step(context): +# for row in context.table[1:]: +# resource = row[0] +# action = row[1] +# account = substitute_inline_codes(row[2]) +# test.compare( +# Activity.check_synced_table(resource, action, account), +# True, +# 'Resource should be displayed in the synced table', +# ) + + +# @Then(r'the following activities (should|should not) be displayed in not synced table', regexp=True) +# def step(context, should_or_should_not): +# expected = should_or_should_not == "should" +# for row in context.table[1:]: +# resource = row[0] +# status = row[1] +# account = substitute_inline_codes(row[2]) +# test.compare( +# Activity.check_not_synced_table(resource, status, account), +# expected, +# 'Resource should be displayed in the not synced table', +# ) + + +# @When('the user unchecks the "|any|" filter') +# def step(context, filter_option): +# Activity.select_not_synced_filter(filter_option) + + +# @Then('the following error message should appear in the client') +# def step(context): +# expected_error_message = '\n'.join(context.multiLineText) + +# actual_error_message = SyncConnection.get_permission_error_message() + +# # wait for error message to disappear +# SyncConnection.wait_for_error_label(False) + +# test.compare( +# actual_error_message, +# expected_error_message, +# f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"' +# ) + + +# @Given('the user has waited for "|any|" seconds') +# def step(context, wait_for): +# squish.snooze(float(wait_for)) + + +# @When('the user unselects the following folders to sync in "Choose what to sync" window:') +# def step(context): +# SyncConnection.choose_what_to_sync() +# folders = [] +# for row in context.table[1:]: +# folders.append(row[0]) +# SyncConnectionWizard.unselect_folders_to_sync(folders, new_sync_connection_wizard=False) diff --git a/test/gui/tst_deleteFilesFolders/test.py b/test/gui/tst_deleteFilesFolders/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_deleteFilesFolders/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 3fe0b2fca28a70de5971357eb74a09fa56d8786f Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:54:44 +0545 Subject: [PATCH 27/75] port tst_activity to appium (#868) Signed-off-by: prashant-gurung899 --- .../activity/activity.feature} | 3 +- test/gui/pageObjects/Activity.py | 187 ++++-------------- test/gui/pageObjects/EnterPassword.py | 7 +- test/gui/pageObjects/Toolbar.py | 7 +- test/gui/steps/account_context.py | 8 +- test/gui/steps/file_context.py | 4 +- test/gui/steps/server_context.py | 4 +- test/gui/steps/sync_context.py | 46 ++--- test/gui/tst_activity/test.py | 8 - 9 files changed, 75 insertions(+), 199 deletions(-) rename test/gui/{tst_activity/test.feature => features/activity/activity.feature} (98%) delete mode 100644 test/gui/tst_activity/test.py diff --git a/test/gui/tst_activity/test.feature b/test/gui/features/activity/activity.feature similarity index 98% rename from test/gui/tst_activity/test.feature rename to test/gui/features/activity/activity.feature index bd5f7f5efb..8b32544265 100644 --- a/test/gui/tst_activity/test.feature +++ b/test/gui/features/activity/activity.feature @@ -3,12 +3,13 @@ Feature: filter activity for user I want to filter activity So that I can view activity of specific user - @smoke @skip + @smoke Scenario: filter synced activities Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server And the user has set up the following accounts with default settings: + | users | | Alice | | Brian | When the user clicks on the activity tab diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 01be75e6ad..96cb57d066 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -1,85 +1,24 @@ -import names -import squish -from objectmaphelper import RegularExpression +# from objectmaphelper import RegularExpression +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config +from helpers.SetupClientHelper import app class Activity: - TAB_CONTAINER = { - "container": names.settings_dialogStack_QStackedWidget, - "type": "QTabWidget", - "visible": 1, - } - SUBTAB_CONTAINER = { - "container": names.settings_dialogStack_QStackedWidget, - "name": "qt_tabwidget_tabbar", - "type": "QTabBar", - "visible": 1, - } - NOT_SYNCED_TABLE = { - "container": names.qt_tabwidget_stackedwidget_OCC_IssuesWidget_OCC_IssuesWidget, - "name": "_tableView", - "type": "QTableView", - "visible": 1, - } - LOCAL_ACTIVITY_FILTER_BUTTON = { - "container": names.qt_tabwidget_stackedwidget_OCC_ProtocolWidget_OCC_ProtocolWidget, - "name": "_filterButton", - "type": "QPushButton", - "visible": 1, - } - SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR = { - "type": "QMenu", - "unnamed": 1, - "visible": 1, - "window": names.settings_OCC_SettingsDialog, - } - SYNCED_ACTIVITY_TABLE = { - "container": names.qt_tabwidget_stackedwidget_OCC_ProtocolWidget_OCC_ProtocolWidget, - "name": "_tableView", - "type": "QTableView", - "visible": 1, - } - NOT_SYNCED_FILTER_BUTTON = { - "container": names.qt_tabwidget_stackedwidget_OCC_IssuesWidget_OCC_IssuesWidget, - "name": "_filterButton", - "type": "QPushButton", - "visible": 1, - } - NOT_SYNCED_FILTER_OPTION_SELECTOR = { - "type": "QMenu", - "unnamed": 1, - "visible": 1, - "window": names.settings_OCC_SettingsDialog, - } - SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = { - "container": names.oCC_ProtocolWidget_tableView_QTableView, - "name": "ActivityListHeaderV2", - "orientation": 1, - "type": "OCC::ExpandingHeaderView", - "visible": 1, - } - NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = { - "container": names.oCC_IssuesWidget_tableView_QTableView, - "name": "ActivityErrorListHeaderV2", - "orientation": 1, - "type": "OCC::ExpandingHeaderView", - "visible": 1, - } + TAB_CONTAINER = SimpleNamespace(by=None, selector=None) + SUBTAB_CONTAINER = SimpleNamespace(by=By.XPATH, selector="//*[@name='{tab_name}']") + NOT_SYNCED_TABLE = SimpleNamespace(by=None, selector=None) + LOCAL_ACTIVITY_FILTER_BUTTON = SimpleNamespace(by=By.NAME, selector="Filter") + SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR = SimpleNamespace(by=By.NAME, selector=None) + SYNCED_ACTIVITY_TABLE = SimpleNamespace(by=None, selector=None) + NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=None, selector=None) + NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) + SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) + NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) - @staticmethod - def get_tab_object(tab_index): - return { - "container": Activity.SUBTAB_CONTAINER, - "index": tab_index, - "type": "TabItem", - } - - @staticmethod - def get_tab_text(tab_index): - return squish.waitForObjectExists(Activity.get_tab_object(tab_index)).text @staticmethod def get_not_synced_file_selector(resource): @@ -103,46 +42,11 @@ def get_not_synced_status(row): @staticmethod def click_tab(tab_name): - tab_found = False - - # NOTE: Some activity tabs are loaded dynamically - # and the tab index changes after all the tabs are loaded properly - # So wait for a second to let the UI render the tabs properly - # before trying to click the tab - squish.snooze(get_config("lowestSyncTimeout")) - - # Selecting tab by name fails for "Not Synced" when there are no unsynced files - # Because files count will be appended like "Not Synced (2)" - # So to overcome this the following approach has been implemented - tab_count = squish.waitForObjectExists(Activity.SUBTAB_CONTAINER).count - tabs = [] - for index in range(tab_count): - tab_text = Activity.get_tab_text(index) - tabs.append(tab_text) - - if tab_name in tab_text: - tab_found = True - # click_tab becomes flaky with "Not Synced" tab - # because the tab text changes. e.g. "Not Synced (2)" - # squish.click_tab(Activity.TAB_CONTAINER, tab_text) - - # NOTE: If only the objectOrName is specified, - # the object is clicked in the middle by the Qt::LeftButton button - # and with no keyboard modifiers pressed. - squish.mouseClick( - squish.waitForObjectExists(Activity.get_tab_object(index)) - ) - break - - if not tab_found: - raise LookupError( - "Tab not found: " - + tab_name - + " in " - + str(tabs) - + ". Tabs count: " - + str(tab_count) - ) + selector = Activity.SUBTAB_CONTAINER.selector.format(tab_name=tab_name) + app().find_element( + Activity.SUBTAB_CONTAINER.by, + selector + ).click() @staticmethod def check_file_exist(filename): @@ -157,7 +61,7 @@ def is_resource_blacklisted(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "Blacklisted"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod @@ -165,7 +69,7 @@ def is_resource_ignored(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "File Ignored"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod @@ -173,7 +77,7 @@ def is_resource_excluded(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "Excluded"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod @@ -182,7 +86,7 @@ def has_sync_status(filename, status): file_row = squish.waitForObject( Activity.get_not_synced_file_selector(filename), get_config("lowestSyncTimeout") * 1000, - )["row"] + )["row"] if Activity.get_not_synced_status(file_row) == status: return True return False @@ -191,11 +95,13 @@ def has_sync_status(filename, status): @staticmethod def select_synced_filter(sync_filter): - squish.clickButton(squish.waitForObject(Activity.LOCAL_ACTIVITY_FILTER_BUTTON)) - squish.activateItem( - squish.waitForObjectItem( - Activity.SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR, sync_filter - ) + app().find_element( + Activity.LOCAL_ACTIVITY_FILTER_BUTTON.by, + Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector + ).click() + app().find_element( + Activity.SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR.by, + sync_filter ) @staticmethod @@ -220,34 +126,9 @@ def get_synced_table_column_number_by_name(column_name): @staticmethod def check_synced_table(resource, action, account): - try: - file_row = squish.waitForObject( - Activity.get_synced_file_selector(resource), - get_config("lowestSyncTimeout") * 1000, - )["row"] - squish.waitForObjectExists( - { - "column": Activity.get_synced_table_column_number_by_name("Action"), - "row": file_row, - "container": Activity.SYNCED_ACTIVITY_TABLE, - "text": action, - "type": "QModelIndex", - } - ) - squish.waitForObjectExists( - { - "column": Activity.get_synced_table_column_number_by_name( - "Account" - ), - "row": file_row, - "container": Activity.SYNCED_ACTIVITY_TABLE, - "text": account, - "type": "QModelIndex", - } - ) - return True - except: - return False + app().find_element(By.NAME, resource) + app().find_element(By.NAME, action) + app().find_element(By.NAME, account) @staticmethod def select_not_synced_filter(filter_option): @@ -275,7 +156,7 @@ def check_not_synced_table(resource, status, account): file_row = squish.waitForObject( Activity.get_not_synced_file_selector(resource), get_config("lowestSyncTimeout") * 1000, - )["row"] + )["row"] squish.waitForObjectExists( { "column": Activity.get_not_synced_table_column_number_by_name( diff --git a/test/gui/pageObjects/EnterPassword.py b/test/gui/pageObjects/EnterPassword.py index fcbe21cb90..4d211d6e49 100644 --- a/test/gui/pageObjects/EnterPassword.py +++ b/test/gui/pageObjects/EnterPassword.py @@ -8,13 +8,16 @@ class EnterPassword: LOGIN_CONTAINER = SimpleNamespace(by=None, selector=None) - LOGIN_USER_LABEL = SimpleNamespace(by=None, selector=None) + LOGIN_USER_LABEL = SimpleNamespace(by=By.XPATH, selector="//filler[@name='Login required']//label[contains(@name, 'Connecting')]") USERNAME_BOX = SimpleNamespace(by=None, selector=None) LOGOUT_BUTTON = SimpleNamespace(by=None, selector=None) def get_username(self): # Parse username from the login label: - label = str(squish.waitForObjectExists(self.LOGIN_USER_LABEL).text) + label = app().find_element( + EnterPassword.LOGIN_USER_LABEL.by, + EnterPassword.LOGIN_USER_LABEL.selector + ).text username = label.split(" ", maxsplit=2)[1] return username.capitalize() diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 3cce34751d..0142bec5ef 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -11,7 +11,7 @@ class Toolbar: TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") - ACTIVITY_BUTTON = SimpleNamespace(by=None, selector=None) + ACTIVITY_BUTTON = SimpleNamespace(by=By.NAME, selector="Activity") SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) QUIT_BUTTON = SimpleNamespace(by=None, selector=None) CONFIRM_QUIT_BUTTON = SimpleNamespace(by=None, selector=None) @@ -37,7 +37,10 @@ def has_item(item_name, timeout=get_config("minSyncTimeout") * 1000): @staticmethod def open_activity(): - squish.mouseClick(squish.waitForObject(Toolbar.ACTIVITY_BUTTON)) + app().find_element( + Toolbar.ACTIVITY_BUTTON.by, + Toolbar.ACTIVITY_BUTTON.selector + ).click() @staticmethod def open_new_account_setup(): diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 9e4b0a7168..ae127d7e7a 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -9,9 +9,10 @@ setup_client, substitute_inline_codes, get_client_details, + generate_account_config, get_resource_path, ) -from helpers.SyncHelper import wait_for_initial_sync_to_complete +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 @@ -69,13 +70,12 @@ def step(context): start_client() # accept certificate for each user for idx, _ in enumerate(users): - enter_password = EnterPassword(len(users) - idx) + enter_password = EnterPassword() 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) + enter_password = EnterPassword() username = enter_password.get_username() password = get_password_for_user(username) listen_sync_status_for_item(sync_paths[username]) diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 80c5fdb6f6..a0718837df 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -9,11 +9,11 @@ from behave import when as When, register_type from helpers.SetupClientHelper import ( - get_resource_path, + get_resource_path, # get_temp_resource_path ) from helpers.SyncHelper import ( - wait_for_client_to_be_ready, + wait_for_client_to_be_ready, # listen_sync_status_for_item ) # from helpers.ConfigHelper import get_config, is_windows diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index ca45d8e7d7..94b511b471 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -16,7 +16,7 @@ def step(context, user): @Then('as "{user_name}" {resource_type:ResourceType} "{resource_name}" should not exist in the server') def step(context, user_name, resource_type, resource_name): resource_exists = webdav.resource_exists(user_name, resource_name) - + with ensure('{0} "{1}" should not exist, but it does', resource_type.capitalize(), resource_name): resource_exists.should.be.false @@ -24,7 +24,7 @@ def step(context, user_name, resource_type, resource_name): @Then('as "{user_name}" {resource_type:ResourceType} "{resource_name}" should exist in the server') def step(context, user_name, resource_type, resource_name): resource_exists = webdav.resource_exists(user_name, resource_name) - + with ensure('{0} "{1}" should exist, but it does not', resource_type.capitalize(), resource_name): resource_exists.should.be.true diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 7918f57bc1..5873a1d275 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -1,8 +1,8 @@ -from behave import when as When +from behave import when as When, then as Then # from pageObjects.SyncConnectionWizard import SyncConnectionWizard # from pageObjects.SyncConnection import SyncConnection -# from pageObjects.Toolbar import Toolbar -# from pageObjects.Activity import Activity +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 @@ -13,7 +13,7 @@ from helpers.SetupClientHelper import ( # get_temp_resource_path, # set_current_user_sync_path, - # substitute_inline_codes, + substitute_inline_codes, get_resource_path, ) # from helpers.FilesHelper import convert_path_separators_for_os @@ -75,9 +75,9 @@ def step(context): # ) -# @When('the user clicks on the activity tab') -# def step(context): -# Toolbar.open_activity() +@When('the user clicks on the activity tab') +def step(context): + Toolbar.open_activity() # @Then('the table of conflict warnings should include file "|any|"') @@ -102,9 +102,9 @@ def step(context): # test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') -# @When('the user selects "|any|" tab in the activity') -# def step(context, tab_name): -# Activity.click_tab(tab_name) +@When('the user selects "{tab_name}" tab in the activity') +def step(context, tab_name): + Activity.click_tab(tab_name) # @Then('the toolbar should have the following tabs:') @@ -252,23 +252,19 @@ def step(context): # ) -# @When('the user checks the activities of account "|any|"') -# def step(context, account): -# account = substitute_inline_codes(account) -# Activity.select_synced_filter(account) +@When('the user checks the activities of account "{account}"') +def step(context, account): + account = substitute_inline_codes(account) + Activity.select_synced_filter(account) -# @Then('the following activities should be displayed in synced table') -# def step(context): -# for row in context.table[1:]: -# resource = row[0] -# action = row[1] -# account = substitute_inline_codes(row[2]) -# test.compare( -# Activity.check_synced_table(resource, action, account), -# True, -# 'Resource should be displayed in the synced table', -# ) +@Then('the following activities should be displayed in synced table') +def step(context): + for row in context.table: + resource = row[0] + action = row[1] + account = substitute_inline_codes(row[2]) + Activity.check_synced_table(resource, action, account) # @Then(r'the following activities (should|should not) be displayed in not synced table', regexp=True) diff --git a/test/gui/tst_activity/test.py b/test/gui/tst_activity/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_activity/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 2f5a8ba5d94885ce58361eaa199f1daed534f7dc Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 23 Apr 2026 09:59:08 +0545 Subject: [PATCH 28/75] test(gui): fix and enable add-account scenarios (#876) * test(gui): enable manual folder sync scenario Signed-off-by: Saw-jan * test(gui): enable account re-add scenario Signed-off-by: Saw-jan * test(gui): skip manual sync scenario Signed-off-by: Saw-jan * test(gui): check space selection Signed-off-by: Saw-jan * test(gui): remove obsolete step files Signed-off-by: Saw-jan --------- 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/AccountSetting.py | 93 ++---- test/gui/pageObjects/SyncConnectionWizard.py | 208 +++++-------- test/gui/pageObjects/Toolbar.py | 7 +- test/gui/shared/steps/account_context.py | 290 ------------------ test/gui/shared/steps/server_context.py | 139 --------- test/gui/step_types/types.py | 10 + test/gui/steps/account_context.py | 23 +- test/gui/steps/file_context.py | 111 +++---- test/gui/steps/server_context.py | 7 +- test/gui/steps/sync_context.py | 12 +- 15 files changed, 304 insertions(+), 715 deletions(-) create mode 100644 test/gui/helpers/TableParser.py delete mode 100644 test/gui/shared/steps/account_context.py delete mode 100644 test/gui/shared/steps/server_context.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..a941f821b6 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/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/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/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index f876342d36..8bab42a240 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,19 @@ 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, + ) + space_item = spaces_list.find_element( + SyncConnectionWizard.SPACE_NAME_SELECTOR.by, + SyncConnectionWizard.SPACE_NAME_SELECTOR.selector.format( + space_name=space_name + ), + ) + 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): @@ -271,11 +195,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 +244,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 +255,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/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/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'], - ) 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..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,13 @@ 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 @Given('the user has started the client') @@ -96,12 +104,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 +113,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) @@ -279,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) 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 14bf6b7528c49dbef156f8cd4aa699fabb826773 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 23 Apr 2026 10:08:18 +0545 Subject: [PATCH 29/75] test(gui): move non-smoke test suites Signed-off-by: Saw-jan --- .../tabs-settings}/test.feature | 0 test/gui/{tst_vfs => features/vfs}/test.feature | 0 test/gui/tst_checkAlltabs/test.py | 8 -------- test/gui/tst_vfs/test.py | 8 -------- 4 files changed, 16 deletions(-) rename test/gui/{tst_checkAlltabs => features/tabs-settings}/test.feature (100%) rename test/gui/{tst_vfs => features/vfs}/test.feature (100%) delete mode 100644 test/gui/tst_checkAlltabs/test.py delete mode 100644 test/gui/tst_vfs/test.py diff --git a/test/gui/tst_checkAlltabs/test.feature b/test/gui/features/tabs-settings/test.feature similarity index 100% rename from test/gui/tst_checkAlltabs/test.feature rename to test/gui/features/tabs-settings/test.feature diff --git a/test/gui/tst_vfs/test.feature b/test/gui/features/vfs/test.feature similarity index 100% rename from test/gui/tst_vfs/test.feature rename to test/gui/features/vfs/test.feature diff --git a/test/gui/tst_checkAlltabs/test.py b/test/gui/tst_checkAlltabs/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_checkAlltabs/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') diff --git a/test/gui/tst_vfs/test.py b/test/gui/tst_vfs/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_vfs/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From df42cf39030af48127e241f20c65c90482970d40 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 23 Apr 2026 10:12:15 +0545 Subject: [PATCH 30/75] test(gui): move remaining step files Signed-off-by: Saw-jan --- test/gui/{shared => }/steps/spaces_context.py | 0 test/gui/{shared => }/steps/vfs_context.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/gui/{shared => }/steps/spaces_context.py (100%) rename test/gui/{shared => }/steps/vfs_context.py (100%) diff --git a/test/gui/shared/steps/spaces_context.py b/test/gui/steps/spaces_context.py similarity index 100% rename from test/gui/shared/steps/spaces_context.py rename to test/gui/steps/spaces_context.py diff --git a/test/gui/shared/steps/vfs_context.py b/test/gui/steps/vfs_context.py similarity index 100% rename from test/gui/shared/steps/vfs_context.py rename to test/gui/steps/vfs_context.py From 87a3e31b906e0874960b7f85ade5766c6efcb41e Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 23 Apr 2026 10:12:34 +0545 Subject: [PATCH 31/75] test(gui): move files-for-upload Signed-off-by: Saw-jan --- test/gui/{shared => }/files-for-upload/simple.docx | Bin test/gui/{shared => }/files-for-upload/simple.pdf | Bin test/gui/{shared => }/files-for-upload/simple.pptx | Bin test/gui/{shared => }/files-for-upload/simple.xlsx | Bin test/gui/{shared => }/files-for-upload/simple1.docx | Bin test/gui/{shared => }/files-for-upload/simple1.pdf | Bin test/gui/{shared => }/files-for-upload/simple1.xlsx | Bin test/gui/{shared => }/files-for-upload/simple2.docx | Bin test/gui/{shared => }/files-for-upload/simple2.pdf | Bin test/gui/{shared => }/files-for-upload/simple2.xlsx | Bin test/gui/{shared => }/files-for-upload/simple3.docx | Bin test/gui/{shared => }/files-for-upload/simple3.pdf | Bin test/gui/{shared => }/files-for-upload/simple3.xlsx | Bin .../{shared => }/files-for-upload/test_video.mp4 | Bin .../{shared => }/files-for-upload/testavatar.jpeg | Bin .../{shared => }/files-for-upload/testavatar.jpg | Bin .../{shared => }/files-for-upload/testavatar.png | Bin .../gui/{shared => }/files-for-upload/testimage.mp3 | Bin 18 files changed, 0 insertions(+), 0 deletions(-) rename test/gui/{shared => }/files-for-upload/simple.docx (100%) rename test/gui/{shared => }/files-for-upload/simple.pdf (100%) rename test/gui/{shared => }/files-for-upload/simple.pptx (100%) rename test/gui/{shared => }/files-for-upload/simple.xlsx (100%) rename test/gui/{shared => }/files-for-upload/simple1.docx (100%) rename test/gui/{shared => }/files-for-upload/simple1.pdf (100%) rename test/gui/{shared => }/files-for-upload/simple1.xlsx (100%) rename test/gui/{shared => }/files-for-upload/simple2.docx (100%) rename test/gui/{shared => }/files-for-upload/simple2.pdf (100%) rename test/gui/{shared => }/files-for-upload/simple2.xlsx (100%) rename test/gui/{shared => }/files-for-upload/simple3.docx (100%) rename test/gui/{shared => }/files-for-upload/simple3.pdf (100%) rename test/gui/{shared => }/files-for-upload/simple3.xlsx (100%) rename test/gui/{shared => }/files-for-upload/test_video.mp4 (100%) rename test/gui/{shared => }/files-for-upload/testavatar.jpeg (100%) rename test/gui/{shared => }/files-for-upload/testavatar.jpg (100%) rename test/gui/{shared => }/files-for-upload/testavatar.png (100%) rename test/gui/{shared => }/files-for-upload/testimage.mp3 (100%) diff --git a/test/gui/shared/files-for-upload/simple.docx b/test/gui/files-for-upload/simple.docx similarity index 100% rename from test/gui/shared/files-for-upload/simple.docx rename to test/gui/files-for-upload/simple.docx diff --git a/test/gui/shared/files-for-upload/simple.pdf b/test/gui/files-for-upload/simple.pdf similarity index 100% rename from test/gui/shared/files-for-upload/simple.pdf rename to test/gui/files-for-upload/simple.pdf diff --git a/test/gui/shared/files-for-upload/simple.pptx b/test/gui/files-for-upload/simple.pptx similarity index 100% rename from test/gui/shared/files-for-upload/simple.pptx rename to test/gui/files-for-upload/simple.pptx diff --git a/test/gui/shared/files-for-upload/simple.xlsx b/test/gui/files-for-upload/simple.xlsx similarity index 100% rename from test/gui/shared/files-for-upload/simple.xlsx rename to test/gui/files-for-upload/simple.xlsx diff --git a/test/gui/shared/files-for-upload/simple1.docx b/test/gui/files-for-upload/simple1.docx similarity index 100% rename from test/gui/shared/files-for-upload/simple1.docx rename to test/gui/files-for-upload/simple1.docx diff --git a/test/gui/shared/files-for-upload/simple1.pdf b/test/gui/files-for-upload/simple1.pdf similarity index 100% rename from test/gui/shared/files-for-upload/simple1.pdf rename to test/gui/files-for-upload/simple1.pdf diff --git a/test/gui/shared/files-for-upload/simple1.xlsx b/test/gui/files-for-upload/simple1.xlsx similarity index 100% rename from test/gui/shared/files-for-upload/simple1.xlsx rename to test/gui/files-for-upload/simple1.xlsx diff --git a/test/gui/shared/files-for-upload/simple2.docx b/test/gui/files-for-upload/simple2.docx similarity index 100% rename from test/gui/shared/files-for-upload/simple2.docx rename to test/gui/files-for-upload/simple2.docx diff --git a/test/gui/shared/files-for-upload/simple2.pdf b/test/gui/files-for-upload/simple2.pdf similarity index 100% rename from test/gui/shared/files-for-upload/simple2.pdf rename to test/gui/files-for-upload/simple2.pdf diff --git a/test/gui/shared/files-for-upload/simple2.xlsx b/test/gui/files-for-upload/simple2.xlsx similarity index 100% rename from test/gui/shared/files-for-upload/simple2.xlsx rename to test/gui/files-for-upload/simple2.xlsx diff --git a/test/gui/shared/files-for-upload/simple3.docx b/test/gui/files-for-upload/simple3.docx similarity index 100% rename from test/gui/shared/files-for-upload/simple3.docx rename to test/gui/files-for-upload/simple3.docx diff --git a/test/gui/shared/files-for-upload/simple3.pdf b/test/gui/files-for-upload/simple3.pdf similarity index 100% rename from test/gui/shared/files-for-upload/simple3.pdf rename to test/gui/files-for-upload/simple3.pdf diff --git a/test/gui/shared/files-for-upload/simple3.xlsx b/test/gui/files-for-upload/simple3.xlsx similarity index 100% rename from test/gui/shared/files-for-upload/simple3.xlsx rename to test/gui/files-for-upload/simple3.xlsx diff --git a/test/gui/shared/files-for-upload/test_video.mp4 b/test/gui/files-for-upload/test_video.mp4 similarity index 100% rename from test/gui/shared/files-for-upload/test_video.mp4 rename to test/gui/files-for-upload/test_video.mp4 diff --git a/test/gui/shared/files-for-upload/testavatar.jpeg b/test/gui/files-for-upload/testavatar.jpeg similarity index 100% rename from test/gui/shared/files-for-upload/testavatar.jpeg rename to test/gui/files-for-upload/testavatar.jpeg diff --git a/test/gui/shared/files-for-upload/testavatar.jpg b/test/gui/files-for-upload/testavatar.jpg similarity index 100% rename from test/gui/shared/files-for-upload/testavatar.jpg rename to test/gui/files-for-upload/testavatar.jpg diff --git a/test/gui/shared/files-for-upload/testavatar.png b/test/gui/files-for-upload/testavatar.png similarity index 100% rename from test/gui/shared/files-for-upload/testavatar.png rename to test/gui/files-for-upload/testavatar.png diff --git a/test/gui/shared/files-for-upload/testimage.mp3 b/test/gui/files-for-upload/testimage.mp3 similarity index 100% rename from test/gui/shared/files-for-upload/testimage.mp3 rename to test/gui/files-for-upload/testimage.mp3 From 5223832fbc81b00e0d5f460cafb5bc701b168f1f Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 23 Apr 2026 17:42:26 +0545 Subject: [PATCH 32/75] test(gui): fix and enable manual sync scenario (#881) Signed-off-by: Saw-jan --- test/gui/features/add-account/account.feature | 2 +- test/gui/pageObjects/SyncConnectionWizard.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index a941f821b6..93ae5b5365 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/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 8bab42a240..7268aa8739 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -1,5 +1,6 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By +from selenium.webdriver.common.keys import Keys import time from helpers.SetupClientHelper import get_current_user_sync_path @@ -136,7 +137,11 @@ def select_space(space_name): space_name=space_name ), ) - space_item.click() + # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 + # Workaround until above fix is merged + # TODO: Remove 'send_keys' and uncomment 'click' action + space_item.send_keys(Keys.ARROW_DOWN) + # space_item.click() if space_item.get_attribute("selected") != "true": raise AssertionError("Failed to select the space: " + space_name) From 63a93858be89dee839409fe6024e6c8659ffa03a Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:39:11 +0545 Subject: [PATCH 33/75] test(gui): port remove account connection scenario to appium (#878) * port removeAccountConnection test suite to appium Signed-off-by: prashant-gurung899 * test: fix account selection Signed-off-by: Saw-jan --------- Signed-off-by: prashant-gurung899 Signed-off-by: Saw-jan Co-authored-by: Saw-jan --- test/gui/features/add-account/account.feature | 11 +++--- .../removeAccountConnection.feature} | 7 ++-- test/gui/pageObjects/AccountSetting.py | 27 ++++++++++----- test/gui/pageObjects/SyncConnectionWizard.py | 3 +- test/gui/pageObjects/Toolbar.py | 25 +++++++++----- test/gui/steps/account_context.py | 34 ++++++++----------- test/gui/tst_removeAccountConnection/test.py | 8 ----- test/gui/tst_syncing/test.feature | 2 +- 8 files changed, 63 insertions(+), 54 deletions(-) rename test/gui/{tst_removeAccountConnection/test.feature => features/remove-account-connection/removeAccountConnection.feature} (87%) delete mode 100644 test/gui/tst_removeAccountConnection/test.py diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 93ae5b5365..202ed6eeef 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -24,7 +24,7 @@ Feature: adding accounts | server | %local_server% | | user | Alice | | password | 1234 | - Then the account with displayname "Alice Hansen" should be displayed + Then "Alice" account should be added @smoke Scenario: Adding multiple accounts @@ -35,9 +35,8 @@ Feature: adding accounts | server | %local_server% | | user | Brian | | password | AaBb2Cc3Dd4 | - Then "Brian Murphy" account should be opened - And the account with displayname "Alice Hansen" should be displayed - And the account with displayname "Brian Murphy" should be displayed + Then "Brian" account should be opened + And "Alice" account should be added Scenario: Adding account with self signed certificate for the first time @@ -48,7 +47,7 @@ Feature: adding accounts When the user adds the following account: | user | Alice | | password | 1234 | - Then "Alice Hansen" account should be opened + Then "Alice" account should be opened @smoke Scenario: Add space manually from sync connection window @@ -87,6 +86,6 @@ Feature: adding accounts | server | %local_server% | | user | Alice | | password | 1234 | - Then the account with displayname "Alice Hansen" should be displayed + Then "Alice" account should be added And the folder "large-folder" should exist on the file system And the file "testFile.txt" should exist on the file system diff --git a/test/gui/tst_removeAccountConnection/test.feature b/test/gui/features/remove-account-connection/removeAccountConnection.feature similarity index 87% rename from test/gui/tst_removeAccountConnection/test.feature rename to test/gui/features/remove-account-connection/removeAccountConnection.feature index b11a7ed392..3003bf68f3 100644 --- a/test/gui/tst_removeAccountConnection/test.feature +++ b/test/gui/features/remove-account-connection/removeAccountConnection.feature @@ -3,16 +3,17 @@ Feature: remove account connection I want to remove my account So that I won't be using any client-UI services - @smoke @skip + @smoke Scenario: remove an account connection Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes And the user has set up the following accounts with default settings: + | users | | Alice | | Brian | When the user removes the connection for user "Brian" - Then the account with displayname "Brian Murphy" should not be displayed - But the account with displayname "Alice Hansen" should be displayed + Then "Brian" account should not be displayed + But "Alice" account should be added Scenario: remove the only account connection diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 3c91e699d6..2418eb406a 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -8,6 +8,9 @@ class AccountSetting: + ACCOUNT_CONNECTION_CONTAINER = SimpleNamespace( + by=By.NAME, selector="Sync connections" + ) MANAGE_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Manage Account") ACCOUNT_MENU = SimpleNamespace(by=By.NAME, selector="{menu_item}") CONFIRM_REMOVE_CONNECTION_BUTTON = SimpleNamespace( @@ -21,10 +24,20 @@ class AccountSetting: @staticmethod def account_action(action): - app().find_element( - AccountSetting.MANAGE_ACCOUNT_BUTTON.by, - AccountSetting.MANAGE_ACCOUNT_BUTTON.selector, - ).click() + connections = app().find_elements( + AccountSetting.ACCOUNT_CONNECTION_CONTAINER.by, + AccountSetting.ACCOUNT_CONNECTION_CONTAINER.selector, + ) + manage_button = None + for connection in connections: + # use the active connection + if connection.get_attribute("showing") == "true": + manage_button = connection.find_element( + AccountSetting.MANAGE_ACCOUNT_BUTTON.by, + AccountSetting.MANAGE_ACCOUNT_BUTTON.selector, + ) + break + manage_button.click() app().find_element( AccountSetting.ACCOUNT_MENU.by, AccountSetting.ACCOUNT_MENU.selector.format(menu_item=action), @@ -129,9 +142,7 @@ def is_log_dialog_visible(): @staticmethod def remove_connection_for_user(username): - displayname = get_displayname_for_user(username) - displayname = substitute_inline_codes(displayname) - Toolbar.open_account(displayname) + Toolbar.open_account(username) AccountSetting.remove_account_connection() @staticmethod @@ -140,7 +151,7 @@ def wait_until_account_is_removed(username, timeout=10000): displayname = substitute_inline_codes(displayname) def account_removed(): - account = Toolbar.get_account(displayname) + account = Toolbar.get_account(username) return account is None result = wait_for(account_removed, timeout) diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 7268aa8739..3ed99b67c6 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -138,7 +138,8 @@ def select_space(space_name): ), ) # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 - # Workaround until above fix is merged + # Cannot select space by click event + # Select space using keyboard events as a workaround # TODO: Remove 'send_keys' and uncomment 'click' action space_item.send_keys(Keys.ARROW_DOWN) # space_item.click() diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index be6521e435..d36410d8bd 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -1,10 +1,12 @@ from types import SimpleNamespace from urllib.parse import urlparse from appium.webdriver.common.appiumby import AppiumBy as By +from selenium.webdriver.common.keys import Keys from helpers.SetupClientHelper import wait_until_app_killed from helpers.ConfigHelper import get_config from helpers.SetupClientHelper import app +from helpers.UserHelper import get_displayname_for_user class Toolbar: @@ -49,9 +51,15 @@ def open_new_account_setup(): ).click() @staticmethod - def open_account(displayname): - account_tab = Toolbar.get_account(displayname) - account_tab.click() + def open_account(username): + account_tab = Toolbar.get_account(username) + # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 + # Cannot activate account tab by click event + # Select the account tab using keyboard events as a workaround + # TODO: Remove the workaround and uncomment 'click' action + # account_tab.click() + account_tab.send_keys(Keys.TAB) + account_tab.send_keys(Keys.ENTER) @staticmethod def get_displayed_account_text(displayname, host): @@ -99,7 +107,8 @@ def get_accounts(): return accounts, selectors @staticmethod - def get_account(display_name): + def get_account(username): + display_name = get_displayname_for_user(username) server_host = urlparse(get_config('localBackendUrl')).netloc account_label = f"{display_name}@{server_host}" account = None @@ -118,11 +127,11 @@ def get_active_account(): return None, None @staticmethod - def account_has_focus(display_name): - account = Toolbar.get_account(display_name) + def account_has_focus(username): + account = Toolbar.get_account(username) return account.get_attribute("checked") == "true" @staticmethod - def account_exists(display_name): - account = Toolbar.get_account(display_name) + def account_exists(username): + account = Toolbar.get_account(username) return account is not None diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 48d4349f53..c10294245d 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -38,22 +38,16 @@ def step(context): ) -@Then('the account with displayname "{displayname}" should be displayed') -def step(context, displayname): - displayname = substitute_inline_codes(displayname) - expect(Toolbar.account_exists(displayname)).to.be.true - +@Then('"{username}" account should be added') +def step(context, username): + username = substitute_inline_codes(username) + expect(Toolbar.account_exists(username)).to.be.true -@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", - ) +@Then('"{username}" account should not be displayed') +def step(context, username): + username = substitute_inline_codes(username) + expect(Toolbar.account_exists(username)).to.be.false @Given('user "{username}" has set up a client with default settings') @@ -161,8 +155,9 @@ def step(context, _): AccountSetting.wait_until_sync_folder_is_configured() -@When('the user removes the connection for user "|any|"') +@When('the user removes the connection for user "{username}"') def step(context, username): + username = substitute_inline_codes(username) AccountSetting.remove_connection_for_user(username) @@ -247,10 +242,10 @@ def step(context): Toolbar.quit_opencloud() -@Then('"{displayname}" account should be opened') -def step(context, displayname): - displayname = substitute_inline_codes(displayname) - expect(Toolbar.account_has_focus(displayname)).to.be.true +@Then('"{username}" account should be opened') +def step(context, username): + username = substitute_inline_codes(username) + expect(Toolbar.account_has_focus(username)).to.be.true @Then( @@ -286,6 +281,7 @@ def step(context, warn_message): @Given('the user has removed the connection for user "{username}"') def step(context, username): + username = substitute_inline_codes(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/tst_removeAccountConnection/test.py b/test/gui/tst_removeAccountConnection/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_removeAccountConnection/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') diff --git a/test/gui/tst_syncing/test.feature b/test/gui/tst_syncing/test.feature index f4e1e8501c..ea1d21a211 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/tst_syncing/test.feature @@ -437,7 +437,7 @@ Feature: Syncing files | password | 1234 | When the user selects manual sync folder option in advanced section And the user cancels the sync connection wizard - Then the account with displayname "Alice Hansen" should be displayed + Then "Alice" account should be added And the sync folder list should be empty From ab95b08e53f6c0ffece6f2e67ccec9ed41fc4db7 Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:08:06 +0545 Subject: [PATCH 34/75] port moveFilesFolders test suite to appium (#884) Signed-off-by: prashant-gurung899 --- .../moveFilesFolders.feature} | 4 +- test/gui/steps/file_context.py | 46 +++++++++---------- test/gui/steps/server_context.py | 14 ++---- test/gui/tst_moveFilesFolders/test.py | 8 ---- 4 files changed, 30 insertions(+), 42 deletions(-) rename test/gui/{tst_moveFilesFolders/test.feature => features/move-files-folders/moveFilesFolders.feature} (99%) delete mode 100644 test/gui/tst_moveFilesFolders/test.py diff --git a/test/gui/tst_moveFilesFolders/test.feature b/test/gui/features/move-files-folders/moveFilesFolders.feature similarity index 99% rename from test/gui/tst_moveFilesFolders/test.feature rename to test/gui/features/move-files-folders/moveFilesFolders.feature index e33738a4b9..e1f92b2136 100644 --- a/test/gui/tst_moveFilesFolders/test.feature +++ b/test/gui/features/move-files-folders/moveFilesFolders.feature @@ -41,7 +41,7 @@ Feature: move file and folder And as "Alice" folder "test-folder1" should not exist in the server And as "Alice" folder "test-folder2" should not exist in the server - @smoke @skip + @smoke Scenario: Rename a file and a folder Given user "Alice" has uploaded file with content "test file 1" to "textfile.txt" in the server And user "Alice" has set up a client with default settings @@ -53,7 +53,7 @@ Feature: move file and folder But as "Alice" file "textfile.txt" should not exist in the server And as "Alice" folder "folder1" should not exist in the server - @smoke @skip + @smoke Scenario: Move files from one folder to another Given user "Alice" has uploaded file with content "test file 1" to "folder1/file1.txt" in the server And user "Alice" has uploaded file with content "test file 2" to "folder1/file2.txt" in the server diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 5b23c952dd..ca06deb329 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -52,10 +52,10 @@ def file_exists(file_path, timeout=1000): # os.makedirs(prefix_path_namespace(convert_path_separators_for_os(folder_path))) -# def rename_file_folder(source, destination): -# source = get_resource_path(source) -# destination = get_resource_path(destination) -# os.rename(source, destination) +def rename_file_folder(source, destination): + source = get_resource_path(source) + destination = get_resource_path(destination) + os.rename(source, destination) # def create_file_with_size(filename, filesize, is_temp_folder=False): @@ -124,16 +124,16 @@ def file_exists(file_path, timeout=1000): # return shutil.copy2(source, destination) -# def move_resource(username, resource_type, source, destination, is_temp_folder=False): -# if not is_temp_folder: -# source = get_resource_path(source, username) -# if destination == '/': -# destination = '' -# destination = get_resource_path(destination, username) +def move_resource(username, resource_type, source, destination, is_temp_folder=False): + if not is_temp_folder: + source = get_resource_path(source, username) + if destination == '/': + destination = '' + destination = get_resource_path(destination, username) -# wait_for_client_to_be_ready() -# listen_sync_status_for_item(destination, resource_type) -# shutil.move(source, destination) + wait_for_client_to_be_ready() + listen_sync_status_for_item(destination, resource_type) + shutil.move(source, destination) def deleteResource(resource, resource_type): @@ -179,10 +179,11 @@ def deleteResource(resource, resource_type): # copy_resource(resource_type, resource_name, resource_name, False) -# @When(r'the user renames a (?:file|folder) "([^"]*)" to "([^"]*)"', regexp=True) -# def step(context, source, destination): -# wait_for_client_to_be_ready() -# rename_file_folder(source, destination) +@When('the user renames a file "{source}" to "{destination}"') +@When('the user renames a folder "{source}" to "{destination}"') +def step(context, source, destination): + wait_for_client_to_be_ready() + rename_file_folder(source, destination) # @Then('the file "|any|" should exist on the file system with the following content') @@ -349,12 +350,11 @@ def step(context, resource_type, resource_name): # move_resource(username, resource_type, resource_name, destination) -# @When( -# r'user "([^"]*)" moves (file|folder) "([^"]*)" to "([^"]*)" in the sync folder', -# regexp=True, -# ) -# def step(context, username, resource_type, source, destination): -# move_resource(username, resource_type, source, destination) +@When( + 'user "{username}" moves {resource_type:ResourceType} "{source}" to "{destination}" in the sync folder' +) +def step(context, username, resource_type, source, destination): + move_resource(username, resource_type, source, destination) # @Then('user "|any|" should be able to open the file "|any|" on the file system') diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index b31232253e..1143374dfe 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -1,6 +1,5 @@ from behave import given as Given, then as Then from sure import ensure -import parse from helpers.api import provisioning, webdav_helper as webdav @@ -24,14 +23,11 @@ def step(context, user_name, resource_type, resource_name): resource_exists.should.be.true -# @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('as "{user_name}" the file "{file_name}" should have the content "{content}" in the server') +def step(context, user_name, file_name, content): + text_content = webdav.get_file_content(user_name, file_name) + with ensure('{0} should have content "{1}" but found "{2}"', file_name, content, text_content): + text_content.should.equal(content) # @Then( diff --git a/test/gui/tst_moveFilesFolders/test.py b/test/gui/tst_moveFilesFolders/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_moveFilesFolders/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 4a5c1a2ef066c7ad6ab03e2bd6d0da323ddebfeb Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:31:25 +0545 Subject: [PATCH 35/75] port syncing test suite to appium (#885) Signed-off-by: prashant-gurung899 --- .../sync-resources/syncResources.feature} | 12 +-- test/gui/steps/file_context.py | 86 +++++++++---------- test/gui/steps/sync_context.py | 10 +-- test/gui/tst_syncing/test.py | 8 -- 4 files changed, 54 insertions(+), 62 deletions(-) rename test/gui/{tst_syncing/test.feature => features/sync-resources/syncResources.feature} (99%) delete mode 100644 test/gui/tst_syncing/test.py diff --git a/test/gui/tst_syncing/test.feature b/test/gui/features/sync-resources/syncResources.feature similarity index 99% rename from test/gui/tst_syncing/test.feature rename to test/gui/features/sync-resources/syncResources.feature index ea1d21a211..b099972383 100644 --- a/test/gui/tst_syncing/test.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -157,7 +157,7 @@ Feature: Syncing files | bFolder | And the user cancels the sync connection wizard - @smoke @skip + @smoke Scenario Outline: Syncing a folder to the server Given user "Alice" has set up a client with default settings When user "Alice" creates a folder inside the sync folder @@ -189,7 +189,7 @@ Feature: Syncing files Then the file "trailing-space.txt " should be ignored And the file "folder with space at end " should be ignored - @smoke @skip + @smoke Scenario: Many subfolders can be synced Given user "Alice" has created folder "parent" in the server And user "Alice" has set up a client with default settings @@ -250,7 +250,7 @@ Feature: Syncing files And as "Alice" folder "original (Copy)" should exist in the server And as "Alice" the file "original (Copy)/localFile.txt" should have the content "test content" in the server - @issue-9281 @smoke @skip + @issue-9281 @smoke Scenario: Verify that you can create a subfolder with long name(~220 characters) Given user "Alice" has created a folder "Folder1" inside the sync folder And user "Alice" has set up a client with default settings @@ -259,7 +259,7 @@ Feature: Syncing files Then the folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" should exist on the file system And as "Alice" folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" should exist in the server - @smoke @skip + @smoke Scenario: Verify pre existing folders in local (Desktop client) are copied over to the server Given user "Alice" has created a folder "Folder1" inside the sync folder And user "Alice" has created a folder "Folder1/subFolder1" inside the sync folder @@ -376,7 +376,7 @@ Feature: Syncing files And as "Alice" file "simple.pptx" should exist in the server And as "Alice" file "simple.xlsx" should exist in the server - @smoke @skip + @smoke Scenario Outline: File with long name can be synced Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder @@ -389,7 +389,7 @@ Feature: Syncing files | filename | | thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIs.txt | - @smoke @skip + @smoke Scenario: Syncing file of 1 GB size Given user "Alice" has set up a client with default settings When user "Alice" creates a file "newfile.txt" with size "1GB" inside the sync folder diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index ca06deb329..57115c062c 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -20,11 +20,11 @@ # can_read, # can_write, # read_file_content, - # get_size_in_bytes, - # prefix_path_namespace, + get_size_in_bytes, + prefix_path_namespace, # remember_path, - # convert_path_separators_for_os, - # get_file_for_upload, + convert_path_separators_for_os, + get_file_for_upload, ) @@ -44,12 +44,12 @@ def file_exists(file_path, timeout=1000): # To create folders in a temporary directory, we set is_temp_folder True # And if is_temp_folder is True, the create_folder function create folders in tempFolderPath -# def create_folder(foldername, username=None, is_temp_folder=False): -# if is_temp_folder: -# folder_path = join(get_config('tempFolderPath'), foldername) -# else: -# folder_path = get_resource_path(foldername, username) -# os.makedirs(prefix_path_namespace(convert_path_separators_for_os(folder_path))) +def create_folder(foldername, username=None, is_temp_folder=False): + if is_temp_folder: + folder_path = join(get_config('tempFolderPath'), foldername) + else: + folder_path = get_resource_path(foldername, username) + os.makedirs(prefix_path_namespace(convert_path_separators_for_os(folder_path))) def rename_file_folder(source, destination): @@ -58,25 +58,25 @@ def rename_file_folder(source, destination): os.rename(source, destination) -# def create_file_with_size(filename, filesize, is_temp_folder=False): -# if is_temp_folder: -# file = join(get_config('tempFolderPath'), filename) -# else: -# file = get_resource_path(filename) -# with open(prefix_path_namespace(file), 'wb') as f: -# f.seek(get_size_in_bytes(filesize) - 1) -# f.write(b'\0') +def create_file_with_size(filename, filesize, is_temp_folder=False): + if is_temp_folder: + file = join(get_config('tempFolderPath'), filename) + else: + file = get_resource_path(filename) + with open(prefix_path_namespace(file), 'wb') as f: + f.seek(get_size_in_bytes(filesize) - 1) + f.write(b'\0') -# def write_file(resource, content): -# with open(prefix_path_namespace(resource), 'w', encoding='utf-8') as f: -# f.write(content) +def write_file(resource, content): + with open(prefix_path_namespace(resource), 'w', encoding='utf-8') as f: + f.write(content) -# def wait_and_write_file(path, content): -# wait_for_client_to_be_ready() -# listen_sync_status_for_item(get_resource_path(path), 'FILE') -# write_file(path, content) +def wait_and_write_file(path, content): + wait_for_client_to_be_ready() + listen_sync_status_for_item(get_resource_path(path), 'FILE') + write_file(path, content) # def wait_and_try_to_write_file(resource, content): @@ -116,7 +116,7 @@ def rename_file_folder(source, destination): # destination = get_resource_path(destination) # if source == destination and destination != '/': # destination = add_copy_suffix(source, resource_type) - +# # wait_for_client_to_be_ready() # listen_sync_status_for_item(destination, resource_type) # if resource_type == 'folder': @@ -144,29 +144,29 @@ def deleteResource(resource, resource_type): shutil.rmtree(resource_path) -# @When( -# 'user "|any|" creates a file "|any|" with the following content inside the sync folder' -# ) -# def step(context, username, filename): -# file_content = '\n'.join(context.multiLineText) -# file = get_resource_path(filename, username) -# wait_and_write_file(convert_path_separators_for_os(file), file_content) +@When( + 'user "{username}" creates a file "{filename}" with the following content inside the sync folder' +) +def step(context, username, filename): + file_content = context.text + file = get_resource_path(filename, username) + wait_and_write_file(convert_path_separators_for_os(file), file_content) -# @When('user "|any|" creates a folder "|any|" inside the sync folder') -# def step(context, username, foldername): -# wait_for_client_to_be_ready() -# create_folder(foldername, username) +@When('user "{username}" creates a folder "{foldername}" inside the sync folder') +def step(context, username, foldername): + wait_for_client_to_be_ready() + create_folder(foldername, username) -# @Given('user "|any|" has created a folder "|any|" inside the sync folder') -# def step(context, username, foldername): -# create_folder(foldername, username) +@Given('user "{username}" has created a folder "{foldername}" inside the sync folder') +def step(context, username, foldername): + create_folder(foldername, username) -# @When('user "|any|" creates a file "|any|" with size "|any|" inside the sync folder') -# def step(context, _, filename, filesize): -# create_file_with_size(filename, filesize) +@When('user "{user}" creates a file "{filename}" with size "{filesize}" inside the sync folder') +def step(context, user, filename, filesize): + create_file_with_size(filename, filesize) # @When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index f49c8dad6b..971505bb77 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -20,7 +20,7 @@ get_resource_path, ) -# from helpers.FilesHelper import convert_path_separators_for_os +from helpers.FilesHelper import convert_path_separators_for_os # @Given('the user has paused the file sync') @@ -43,10 +43,10 @@ def step(context): wait_for_resource_to_sync(get_resource_path('/')) -# @When(r'the user waits for (file|folder) "([^"]*)" to be synced', regexp=True) -# def step(context, resource_type, resource): -# resource = get_resource_path(resource) -# wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) +@When('the user waits for {resource_type:ResourceType} "{resource}" to be synced') +def step(context, resource_type, resource): + resource = get_resource_path(resource) + wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) # @When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) diff --git a/test/gui/tst_syncing/test.py b/test/gui/tst_syncing/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_syncing/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 6c04daaadd3b06d57716bd6e437bea4a039c9e0f Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 27 Apr 2026 16:46:39 +0545 Subject: [PATCH 36/75] test(gui): fix and enable spaces scenarios (#883) * test(gui): fix and enable spaces scenarios Signed-off-by: Saw-jan * test(gui): fix and enable spaces scenarios Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- test/gui/environment.py | 2 + .../spaces/spaces.feature} | 14 +- .../sync-resources/syncResources.feature | 5 +- test/gui/pageObjects/Settings.py | 60 +-- test/gui/pageObjects/SyncConnection.py | 142 +++--- test/gui/steps/file_context.py | 453 +++++++++--------- test/gui/steps/server_context.py | 200 ++++---- test/gui/steps/spaces_context.py | 35 +- test/gui/steps/sync_context.py | 383 +++++++-------- test/gui/tst_spaces/test.py | 8 - 10 files changed, 655 insertions(+), 647 deletions(-) rename test/gui/{tst_spaces/test.feature => features/spaces/spaces.feature} (97%) delete mode 100644 test/gui/tst_spaces/test.py diff --git a/test/gui/environment.py b/test/gui/environment.py index 078bc37e6c..26764d37ac 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -4,6 +4,7 @@ from helpers.ConfigHelper import init_config from helpers.api.provisioning import delete_created_users +from helpers.SpaceHelper import delete_project_spaces from helpers.ConfigHelper import set_config, get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths from helpers.SetupClientHelper import app @@ -33,6 +34,7 @@ def after_scenario(context, scenario): print(f"Failed to delete '{entry.name}'.\nReason: {e}.") # cleanup paths created outside of the temporary directory during the test cleanup_created_paths() + delete_project_spaces() delete_created_users() # quit the application if app() is not None: diff --git a/test/gui/tst_spaces/test.feature b/test/gui/features/spaces/spaces.feature similarity index 97% rename from test/gui/tst_spaces/test.feature rename to test/gui/features/spaces/spaces.feature index 2f731007b3..c8cfa8d975 100644 --- a/test/gui/tst_spaces/test.feature +++ b/test/gui/features/spaces/spaces.feature @@ -7,7 +7,7 @@ Feature: Project spaces Given user "Alice" has been created in the server with default attributes And the administrator has created a space "Project101" - @smoke @skip + @smoke Scenario: User with Viewer role can open the file Given the administrator has created a folder "planning" in space "Project101" And the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" @@ -25,7 +25,7 @@ Feature: Project spaces Then user "Alice" should not be able to edit the file "testfile.txt" on the file system And as "Alice" the file "testfile.txt" in the space "Project101" should have content "some content" in the server - @smoke @skip + @smoke Scenario: User with Editor role can edit the file Given the administrator has created a folder "planning" in space "Project101" And the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" @@ -35,7 +35,7 @@ Feature: Project spaces And the user waits for file "testfile.txt" to be synced Then as "Alice" the file "testfile.txt" in the space "Project101" should have content "some content edited" in the server - @smoke @skip + @smoke Scenario: User with Manager role can add files and folders Given the administrator has added user "Alice" to space "Project101" with role "manager" And user "Alice" has set up a client with space "Project101" @@ -48,7 +48,7 @@ Feature: Project spaces Then as "Alice" the file "localFile.txt" in the space "Project101" should have content "test content" in the server And as "Alice" the space "Project101" should have folder "localFolder" in the server - @smoke @skip + @smoke Scenario: User with Editor role can rename a file Given the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" And the administrator has added user "Alice" to space "Project101" with role "editor" @@ -58,13 +58,13 @@ Feature: Project spaces Then as "Alice" the space "Project101" should have file "renamedFile.txt" in the server And as "Alice" the file "renamedFile.txt" in the space "Project101" should have content "some content" in the server - @smoke @skip + @smoke Scenario: Remove folder sync connection (Project Space) Given the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" And the administrator has added user "Alice" to space "Project101" with role "manager" And user "Alice" has set up a client with space "Project101" When the user removes the folder sync connection - Then the sync folder list should be empty + Then for user "Alice" sync folder "Project101" should not be displayed But the file "testfile.txt" should exist on the file system @@ -82,7 +82,7 @@ Feature: Project spaces | resource | status | account | | simple-folder | Blacklisted | Alice Hansen@%local_server_hostname% | - @smoke @skip + @smoke Scenario: Sharee with Editor role deletes the shared resource Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index b099972383..77c3f1eb69 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -438,7 +438,8 @@ Feature: Syncing files When the user selects manual sync folder option in advanced section And the user cancels the sync connection wizard Then "Alice" account should be added - And the sync folder list should be empty + And for user "Alice" sync folder "Personal" should not be displayed + And for user "Alice" sync folder "Shares" should not be displayed Scenario: extract a zip file in the sync folder @@ -501,7 +502,7 @@ Feature: Syncing files Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has set up a client with default settings When the user removes the folder sync connection - Then the sync folder list should be empty + Then for user "Alice" sync folder "Personal" should not be displayed And the folder "simple-folder" should exist on the file system And as "Alice" folder "simple-folder" should exist in the server diff --git a/test/gui/pageObjects/Settings.py b/test/gui/pageObjects/Settings.py index cb134237fa..5a1fb79c3f 100644 --- a/test/gui/pageObjects/Settings.py +++ b/test/gui/pageObjects/Settings.py @@ -1,56 +1,16 @@ -import names -import squish +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By class Settings: - CHECKBOX_OPTION_ITEM = { - "container": names.stack_scrollArea_QScrollArea, - "type": "QCheckBox", - "visible": 1, - } - NETWORK_OPTION_ITEM = { - "container": names.stack_scrollArea_QScrollArea, - "type": "QGroupBox", - "visible": 1, - } - ABOUT_BUTTON = { - "container": names.settings_stack_QStackedWidget, - "name": "about_pushButton", - "type": "QPushButton", - "visible": 1, - } - ABOUT_DIALOG = { - "name": "OCC__AboutDialog", - "type": "OCC::AboutDialog", - "visible": 1, - } - ABOUT_DIALOG_OK_BUTTON = { - "text": "OK", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": ABOUT_DIALOG, - } - - GENERAL_OPTIONS_MAP = { - "Start on Login": "autostartCheckBox", - "Use Monochrome Icons in the system tray": "monoIconsCheckBox", - "Language": "languageDropdown", - "Show desktop Notifications": "desktopNotificationsCheckBox", - } - ADVANCED_OPTION_MAP = { - "Sync hidden files": "syncHiddenFilesCheckBox", - "Show crash reporter": "", - "Edit ignored files": "ignoredFilesButton", - "Log settings": "logSettingsButton", - "Ask for confirmation before synchronizing folders larger than 500 MB": "newFolderLimitCheckBox", - "Ask for confirmation before synchronizing external storages": "newExternalStorage", - } - NETWORK_OPTION_MAP = { - "Proxy Settings": "proxyGroupBox", - "Download Bandwidth": "downloadBox", - "Upload Bandwidth": "uploadBox", - } + CHECKBOX_OPTION_ITEM = SimpleNamespace(by=None, selector=None) + NETWORK_OPTION_ITEM = SimpleNamespace(by=None, selector=None) + ABOUT_BUTTON = SimpleNamespace(by=None, selector=None) + ABOUT_DIALOG = SimpleNamespace(by=None, selector=None) + ABOUT_DIALOG_OK_BUTTON = SimpleNamespace(by=None, selector=None) + GENERAL_OPTIONS_MAP = SimpleNamespace(by=None, selector=None) + ADVANCED_OPTION_MAP = SimpleNamespace(by=None, selector=None) + NETWORK_OPTION_MAP = SimpleNamespace(by=None, selector=None) @staticmethod def get_checkbox_option_selector(name): diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index 9d0ef78314..745ef17070 100644 --- a/test/gui/pageObjects/SyncConnection.py +++ b/test/gui/pageObjects/SyncConnection.py @@ -1,76 +1,70 @@ -import names -import squish -import object # pylint: disable=redefined-builtin +from types import SimpleNamespace +from appium.webdriver.common.appiumby import AppiumBy as By +from selenium.webdriver.common.keys import Keys from helpers.ConfigHelper import get_config +from helpers.SetupClientHelper import app class SyncConnection: - FOLDER_SYNC_CONNECTION_LIST = { - "container": names.quickWidget_scrollView_ScrollView, - "type": "ListView", - "visible": True, - } - FOLDER_SYNC_CONNECTION = { - "container": names.settings_stack_QStackedWidget, - "name": "_folderList", - "type": "QListView", - "visible": 1, - } - FOLDER_SYNC_CONNECTION_MENU_BUTTON = { - "container": names.quickWidget_scrollView_ScrollView, - "id": "moreButton", - "type": "Image", - "visible": True, - } - MENU = { - "checkable": False, - "container": names.quickWidget_Overlay, - "enabled": True, - "text": "", - "type": "MenuItem", - "unnamed": 1, - "visible": True, - } - SELECTIVE_SYNC_APPLY_BUTTON = { - "container": names.settings_stack_QStackedWidget, - "name": "selectiveSyncApply", - "type": "QPushButton", - "visible": 1, - } - CANCEL_FOLDER_SYNC_CONNECTION_DIALOG = { - "text": "Cancel", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.confirm_Folder_Sync_Connection_Removal_QMessageBox, - } - REMOVE_FOLDER_SYNC_CONNECTION_BUTTON = { - "text": "Remove Space", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": names.confirm_removal_of_Space_QMessageBox, - } - PERMISSION_ERROR_LABEL = { - "container": names.folderError_Container, - "type": "Label", - "visible": True, - } + ACCOUNT_CONNECTION_CONTAINER = SimpleNamespace( + by=By.NAME, selector="Sync connections" + ) + FOLDER_SYNC_CONNECTION_MENU_BUTTON = SimpleNamespace( + by=By.NAME, + selector="{sync_folder},Success,Local folder: {sync_path}{sync_folder}", + ) + MENU_ITEM = SimpleNamespace(by=By.NAME, selector=None) + SELECTIVE_SYNC_APPLY_BUTTON = SimpleNamespace(by=None, selector=None) + CANCEL_FOLDER_SYNC_CONNECTION_DIALOG = SimpleNamespace(by=None, selector=None) + CONFIRM_FOLDER_SYNC_CONNECTION_REMOVE = SimpleNamespace( + by=By.NAME, selector="Remove Space" + ) + PERMISSION_ERROR_LABEL = SimpleNamespace(by=None, selector=None) @staticmethod - def open_menu(): - menu_button = squish.waitForObject( - SyncConnection.FOLDER_SYNC_CONNECTION_MENU_BUTTON + def get_current_account_connection(): + connections = app().find_elements( + SyncConnection.ACCOUNT_CONNECTION_CONTAINER.by, + SyncConnection.ACCOUNT_CONNECTION_CONTAINER.selector, ) - squish.mouseClick(menu_button) + for connection in connections: + # use the active connection + if connection.get_attribute("showing") == "true": + return connection + return None + + @staticmethod + def open_menu(sync_folder=None): + if sync_folder is None: + sync_folder = get_config('syncConnectionName') + + connection = SyncConnection.get_current_account_connection() + menu_button = connection.find_element( + SyncConnection.FOLDER_SYNC_CONNECTION_MENU_BUTTON.by, + SyncConnection.FOLDER_SYNC_CONNECTION_MENU_BUTTON.selector.format( + sync_folder=sync_folder, + sync_path=get_config('currentUserSyncPath'), + ), + ) + # Cannot select sync folder menu button. + # This is a messy workaround to open the context menu using keyboard navigation. + # Ideally, we should be able to do: click() and send_keys(" ") to open the menu + # but it doesn't work for some reason. + # Also, send_keys(Keys.SPACE) doesn't work. + menu_button.click() + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(Keys.TAB) + menu_button.send_keys(" ") @staticmethod def perform_action(action): SyncConnection.open_menu() - selector = SyncConnection.MENU.copy() - selector["text"] = action - squish.mouseClick(squish.waitForObject(selector)) + app().find_element(SyncConnection.MENU_ITEM.by, action).click() @staticmethod def force_sync(): @@ -86,11 +80,11 @@ def resume_sync(): @staticmethod def has_menu_item(item): - return squish.waitForObjectItem(SyncConnection.MENU, item) + return squish.waitForObjectItem(SyncConnection.MENU_ITEM, item) @staticmethod def menu_item_exists(menu_item): - obj = SyncConnection.MENU.copy() + obj = SyncConnection.MENU_ITEM.copy() obj.update({"type": "QAction", "text": menu_item}) return object.exists(obj) @@ -100,8 +94,19 @@ def choose_what_to_sync(): SyncConnection.perform_action("Choose what to sync") @staticmethod - def get_folder_connection_count(): - return squish.waitForObject(SyncConnection.FOLDER_SYNC_CONNECTION_LIST).count + def has_sync_connection(sync_folder): + connection = SyncConnection.get_current_account_connection() + try: + connection.find_element( + SyncConnection.FOLDER_SYNC_CONNECTION_MENU_BUTTON.by, + SyncConnection.FOLDER_SYNC_CONNECTION_MENU_BUTTON.selector.format( + sync_folder=sync_folder, + sync_path=get_config('currentUserSyncPath'), + ), + ) + return True + except: + return False @staticmethod def remove_folder_sync_connection(): @@ -115,9 +120,10 @@ def cancel_folder_sync_connection_removal(): @staticmethod def confirm_folder_sync_connection_removal(): - squish.clickButton( - squish.waitForObject(SyncConnection.REMOVE_FOLDER_SYNC_CONNECTION_BUTTON) - ) + app().find_element( + SyncConnection.CONFIRM_FOLDER_SYNC_CONNECTION_REMOVE.by, + SyncConnection.CONFIRM_FOLDER_SYNC_CONNECTION_REMOVE.selector, + ).click() @staticmethod def wait_for_error_label(to_exist=True): diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 57115c062c..067d4df84c 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -3,7 +3,7 @@ import builtins import shutil import zipfile -from os.path import isfile, join, isdir +from os.path import isfile, join, isdir, exists from behave import when as When, then as Then from sure import ensure @@ -15,14 +15,14 @@ ) from helpers.ConfigHelper import get_config, is_windows from helpers.FilesHelper import ( - # build_conflicted_regex, + build_conflicted_regex, sanitize_path, - # can_read, - # can_write, - # read_file_content, + can_read, + can_write, + read_file_content, get_size_in_bytes, prefix_path_namespace, - # remember_path, + remember_path, convert_path_separators_for_os, get_file_for_upload, ) @@ -79,49 +79,49 @@ def wait_and_write_file(path, content): write_file(path, content) -# def wait_and_try_to_write_file(resource, content): -# wait_for_client_to_be_ready() -# listen_sync_status_for_item(get_resource_path(resource), 'FILE') -# try: -# write_file(resource, content) -# except: -# pass +def wait_and_try_to_write_file(resource, content): + wait_for_client_to_be_ready() + listen_sync_status_for_item(get_resource_path(resource), 'FILE') + try: + write_file(resource, content) + except: + pass + +def create_zip(resources, zip_file_name, cwd=''): + os.chdir(cwd) + with zipfile.ZipFile(zip_file_name, 'w') as zipped_file: + for resource in resources: + zipped_file.write(resource) -# def create_zip(resources, zip_file_name, cwd=''): -# os.chdir(cwd) -# with zipfile.ZipFile(zip_file_name, 'w') as zipped_file: -# for resource in resources: -# zipped_file.write(resource) +def extract_zip(zip_file_path, destination_dir): + with zipfile.ZipFile(zip_file_path, 'r') as zip_file: + zip_file.extractall(destination_dir) -# def extract_zip(zip_file_path, destination_dir): -# with zipfile.ZipFile(zip_file_path, 'r') as zip_file: -# zip_file.extractall(destination_dir) +def add_copy_suffix(resource_path, resource_type): + suffix = ' (Copy)' + if resource_type == 'file': + source_dir = resource_path.rsplit('.', 1) + return source_dir[0] + suffix + '.' + source_dir[-1] + return resource_path + suffix -# def add_copy_suffix(resource_path, resource_type): -# suffix = ' (Copy)' -# if resource_type == 'file': -# source_dir = resource_path.rsplit('.', 1) -# return source_dir[0] + suffix + '.' + source_dir[-1] -# return resource_path + suffix +def copy_resource(resource_type, source, destination, from_files_for_upload=False): + if from_files_for_upload: + source = get_file_for_upload(source) + else: + source = get_resource_path(source) + destination = get_resource_path(destination) + if source == destination and destination != '/': + destination = add_copy_suffix(source, resource_type) -# def copy_resource(resource_type, source, destination, from_files_for_upload=False): -# if from_files_for_upload: -# source = get_file_for_upload(source) -# else: -# source = get_resource_path(source) -# destination = get_resource_path(destination) -# if source == destination and destination != '/': -# destination = add_copy_suffix(source, resource_type) -# -# wait_for_client_to_be_ready() -# listen_sync_status_for_item(destination, resource_type) -# if resource_type == 'folder': -# return shutil.copytree(source, destination) -# return shutil.copy2(source, destination) + wait_for_client_to_be_ready() + listen_sync_status_for_item(destination, resource_type) + if resource_type == 'folder': + return shutil.copytree(source, destination) + return shutil.copy2(source, destination) def move_resource(username, resource_type, source, destination, is_temp_folder=False): @@ -148,9 +148,8 @@ def deleteResource(resource, resource_type): 'user "{username}" creates a file "{filename}" with the following content inside the sync folder' ) def step(context, username, filename): - file_content = context.text file = get_resource_path(filename, username) - wait_and_write_file(convert_path_separators_for_os(file), file_content) + wait_and_write_file(convert_path_separators_for_os(file), context.text) @When('user "{username}" creates a folder "{foldername}" inside the sync folder') @@ -164,19 +163,21 @@ def step(context, username, foldername): create_folder(foldername, username) -@When('user "{user}" creates a file "{filename}" with size "{filesize}" inside the sync folder') +@When( + 'user "{user}" creates a file "{filename}" with size "{filesize}" inside the sync folder' +) def step(context, user, filename, filesize): create_file_with_size(filename, filesize) -# @When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) -# def step(context, resource_type, resource_name, destination_dir): -# copy_resource(resource_type, resource_name, destination_dir, False) +@When(r'the user copies (file|folder) "([^"]*)" into folder "([^"]*)"', regexp=True) +def step(context, resource_type, resource_name, destination_dir): + copy_resource(resource_type, resource_name, destination_dir, False) -# @When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) -# def step(context, resource_type, resource_name): -# copy_resource(resource_type, resource_name, resource_name, False) +@When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) +def step(context, resource_type, resource_name): + copy_resource(resource_type, resource_name, resource_name, False) @When('the user renames a file "{source}" to "{destination}"') @@ -186,19 +187,19 @@ def step(context, source, destination): rename_file_folder(source, destination) -# @Then('the file "|any|" should exist on the file system with the following content') -# def step(context, file_path): -# expected = '\n'.join(context.multiLineText) -# file_path = get_resource_path(file_path) -# with open(file_path, 'r', encoding='utf-8') as f: -# contents = f.read() -# test.compare( -# expected, -# contents, -# 'file expected to exist with content ' -# + expected -# + ' but does not have the expected content', -# ) +@Then('the file "|any|" should exist on the file system with the following content') +def step(context, file_path): + expected = '\n'.join(context.multiLineText) + file_path = get_resource_path(file_path) + with open(file_path, 'r', encoding='utf-8') as f: + contents = f.read() + test.compare( + expected, + contents, + 'file expected to exist with content ' + + expected + + ' but does not have the expected content', + ) @Then('the {resource_type:ResourceType} "{resource}" should exist on the file system') @@ -224,65 +225,58 @@ def step(context, resource_type, resource): ) 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 + exists(resource_path).should.be.false -# @Given('the user has changed the content of local file "|any|" to:') -# def step(context, filename): -# file_content = '\n'.join(context.multiLineText) -# wait_and_write_file(get_resource_path(filename), file_content) +@Given('the user has changed the content of local file "|any|" to:') +def step(context, filename): + file_content = '\n'.join(context.multiLineText) + wait_and_write_file(get_resource_path(filename), file_content) -# @Then( -# 'a conflict file for "|any|" should exist on the file system with the following content' -# ) -# def step(context, filename): -# expected = '\n'.join(context.multiLineText) +@Then( + 'a conflict file for "|any|" should exist on the file system with the following content' +) +def step(context, filename): + expected = '\n'.join(context.multiLineText) -# onlyfiles = [ -# f for f in os.listdir(get_resource_path()) if isfile(get_resource_path(f)) -# ] -# found = False -# pattern = re.compile(build_conflicted_regex(filename)) -# for file in onlyfiles: -# if pattern.match(file): -# with open(get_resource_path(file), 'r', encoding='utf-8') as f: -# if f.read() == expected: -# found = True -# break + onlyfiles = [ + f for f in os.listdir(get_resource_path()) if isfile(get_resource_path(f)) + ] + found = False + pattern = re.compile(build_conflicted_regex(filename)) + for file in onlyfiles: + if pattern.match(file): + with open(get_resource_path(file), 'r', encoding='utf-8') as f: + if f.read() == expected: + found = True + break -# if not found: -# raise AssertionError('Conflict file not found with given name') + if not found: + raise AssertionError('Conflict file not found with given name') -# @When('the user overwrites the file "|any|" with content "|any|"') -# def step(context, resource, content): -# resource = get_resource_path(resource) -# wait_and_write_file(resource, content) +@When('the user overwrites the file "{resource}" with content "{content}"') +def step(context, resource, content): + resource = get_resource_path(resource) + wait_and_write_file(resource, content) -# @When('the user tries to overwrite the file "|any|" with content "|any|"') -# def step(context, resource, content): -# resource = get_resource_path(resource) -# wait_and_try_to_write_file(resource, content) +@When('the user tries to overwrite the file "|any|" with content "|any|"') +def step(context, resource, content): + resource = get_resource_path(resource) + wait_and_try_to_write_file(resource, content) -# @When('user "|any|" tries to overwrite the file "|any|" with content "|any|"') -# def step(context, user, resource, content): -# resource = get_resource_path(resource, user) -# wait_and_try_to_write_file(resource, content) +@When('user "|any|" tries to overwrite the file "|any|" with content "|any|"') +def step(context, user, resource, content): + resource = get_resource_path(resource, user) + wait_and_try_to_write_file(resource, content) @When('the user deletes the {resource_type:ResourceType} "{resource_name}"') @@ -292,62 +286,62 @@ def step(context, resource_type, resource_name): deleteResource(resource_name, resource_type) -# @When('user "|any|" creates the following files inside the sync folder:') -# def step(context, username): -# wait_for_client_to_be_ready() +@When('user "|any|" creates the following files inside the sync folder:') +def step(context, username): + wait_for_client_to_be_ready() -# for row in context.table[1:]: -# file = get_resource_path(row[0], username) -# write_file(file, '') + for row in context.table[1:]: + file = get_resource_path(row[0], username) + write_file(file, '') -# @Given('the user has created a folder "|any|" in temp folder') -# def step(context, folder_name): -# create_folder(folder_name, is_temp_folder=True) +@Given('the user has created a folder "|any|" in temp folder') +def step(context, folder_name): + create_folder(folder_name, is_temp_folder=True) -# @Given( -# 'the user has created "|any|" files each of size "|any|" bytes inside folder "|any|" in temp folder' -# ) -# def step(context, file_number, file_size, folder_name): -# current_sync_path = get_temp_resource_path(folder_name) -# if folder_exists(current_sync_path): -# file_size = builtins.int(file_size) -# for i in range(0, builtins.int(file_number)): -# file_name = f'file{i}.txt' -# create_file_with_size(join(current_sync_path, file_name), file_size, True) -# else: -# raise FileNotFoundError( -# f"Folder '{folder_name}' does not exist in the temp folder" -# ) +@Given( + 'the user has created "|any|" files each of size "|any|" bytes inside folder "|any|" in temp folder' +) +def step(context, file_number, file_size, folder_name): + current_sync_path = get_temp_resource_path(folder_name) + if folder_exists(current_sync_path): + file_size = builtins.int(file_size) + for i in range(0, builtins.int(file_number)): + file_name = f'file{i}.txt' + create_file_with_size(join(current_sync_path, file_name), file_size, True) + else: + raise FileNotFoundError( + f"Folder '{folder_name}' does not exist in the temp folder" + ) -# @When( -# r'user "([^"]*)" reads the content of file "([^"]*)"', -# regexp=True, -# ) -# def step(context, username, file): -# file_path = get_resource_path(file, username) -# with open(file_path, 'r') as f: -# f.read() +@When( + r'user "([^"]*)" reads the content of file "([^"]*)"', + regexp=True, +) +def step(context, username, file): + file_path = get_resource_path(file, username) + with open(file_path, 'r') as f: + f.read() -# @When( -# r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', -# regexp=True, -# ) -# def step(context, username, resource_type, resource_name): -# source_dir = join(get_config('tempFolderPath'), resource_name) -# move_resource(username, resource_type, source_dir, '/', True) +@When( + r'user "([^"]*)" moves (folder|file) "([^"]*)" from the temp folder into the sync folder', + regexp=True, +) +def step(context, username, resource_type, resource_name): + source_dir = join(get_config('tempFolderPath'), resource_name) + move_resource(username, resource_type, source_dir, '/', True) -# @When( -# r'user "([^"]*)" moves (folder|file) "([^"]*)" to the temp folder', -# regexp=True, -# ) -# def step(context, username, resource_type, resource_name): -# destination= join(get_config('tempFolderPath'), resource_name) -# move_resource(username, resource_type, resource_name, destination) +@When( + r'user "([^"]*)" moves (folder|file) "([^"]*)" to the temp folder', + regexp=True, +) +def step(context, username, resource_type, resource_name): + destination = join(get_config('tempFolderPath'), resource_name) + move_resource(username, resource_type, resource_name, destination) @When( @@ -357,93 +351,106 @@ def step(context, username, resource_type, source, destination): move_resource(username, resource_type, source, destination) -# @Then('user "|any|" should be able to open the file "|any|" on the file system') -# def step(context, user, file_name): -# file_path = get_resource_path(file_name, user) -# test.compare(can_read(file_path), True, 'File should be readable') - - -# @Then('as "|any|" the file "|any|" should have content "|any|" on the file system') -# def step(context, user, file_name, content): -# file_path = get_resource_path(file_name, user) -# file_content = read_file_content(file_path) -# test.compare(file_content, content, 'Comparing file content') - - -# @Then('user "|any|" should not be able to edit the file "|any|" on the file system') -# def step(context, user, file_name): -# file_path = get_resource_path(file_name, user) -# test.compare(not can_write(file_path), True, 'File should not be writable') - +@Then('user "{user}" should be able to open the file "{file_name}" on the file system') +def step(context, user, file_name): + file_path = get_resource_path(file_name, user) + with ensure( + 'File should be readable but user "{0}" cannot read file "{1}"', + user, + file_name, + ): + can_read(file_path).should.be.true -# @Given( -# 'the user has created a zip file "|any|" with the following resources in the temp folder' -# ) -# def step(context, zip_file_name): -# resource_list = [] -# for row in context.table[1:]: -# resource_list.append(row[0]) -# resource = join(get_config('tempFolderPath'), row[0]) -# if row[1] == 'folder': -# os.makedirs(resource) -# elif row[1] == 'file': -# content = '' -# if len(row) > 2 and row[2]: -# content = row[2] -# write_file(resource, content) -# create_zip(resource_list, zip_file_name, get_config('tempFolderPath')) +@Then( + 'as "{user}" the file "{file_name}" should have content "{content}" on the file system' +) +def step(context, user, file_name, content): + file_path = get_resource_path(file_name, user) + file_content = read_file_content(file_path) + with ensure( + 'File "{0}" should have content "{1}" but got "{2}"', + file_name, + content, + file_content, + ): + content.should.equal(file_content) -# @When('user "|any|" unzips the zip file "|any|" inside the sync root') -# def step(context, username, zip_file_name): -# destination_dir = get_resource_path('/', username) -# zip_file_path = join(destination_dir, zip_file_name) -# extract_zip(zip_file_path, destination_dir) +@Then('user "|any|" should not be able to edit the file "|any|" on the file system') +def step(context, user, file_name): + file_path = get_resource_path(file_name, user) + test.compare(not can_write(file_path), True, 'File should not be writable') -# @When('user "|any|" copies file "|any|" to temp folder') -# def step(context, username, source): -# wait_for_client_to_be_ready() -# source_dir = get_resource_path(source, username) -# destination_dir = get_temp_resource_path(source) -# shutil.copy2(source_dir, destination_dir) +@Given( + 'the user has created a zip file "|any|" with the following resources in the temp folder' +) +def step(context, zip_file_name): + resource_list = [] + + for row in context.table[1:]: + resource_list.append(row[0]) + resource = join(get_config('tempFolderPath'), row[0]) + if row[1] == 'folder': + os.makedirs(resource) + elif row[1] == 'file': + content = '' + if len(row) > 2 and row[2]: + content = row[2] + write_file(resource, content) + create_zip(resource_list, zip_file_name, get_config('tempFolderPath')) + + +@When('user "|any|" unzips the zip file "|any|" inside the sync root') +def step(context, username, zip_file_name): + destination_dir = get_resource_path('/', username) + zip_file_path = join(destination_dir, zip_file_name) + extract_zip(zip_file_path, destination_dir) + + +@When('user "|any|" copies file "|any|" to temp folder') +def step(context, username, source): + wait_for_client_to_be_ready() + source_dir = get_resource_path(source, username) + destination_dir = get_temp_resource_path(source) + shutil.copy2(source_dir, destination_dir) -# @Given('the user has created folder "|any|" in the default home path') -# def step(context, folder_name): -# folder_path = join(get_config('home_dir'), folder_name) -# os.makedirs(prefix_path_namespace(folder_path)) -# remember_path(folder_path) -# # when account is added, folder with suffix will be created -# remember_path(f'{folder_path} (2)') +@Given('the user has created folder "|any|" in the default home path') +def step(context, folder_name): + folder_path = join(get_config('home_dir'), folder_name) + os.makedirs(prefix_path_namespace(folder_path)) + remember_path(folder_path) + # when account is added, folder with suffix will be created + remember_path(f'{folder_path} (2)') -# @Given( -# r'the user has copied file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', -# regexp=True, -# ) -# def step(context, resource_name, destination): -# copy_resource('file', resource_name, destination, True) +@Given( + r'the user has copied file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', + regexp=True, +) +def step(context, resource_name, destination): + copy_resource('file', resource_name, destination, True) -# @When( -# r'the user copies file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', -# regexp=True, -# ) -# def step(context, resource_name, destination): -# copy_resource('file', resource_name, destination, True) +@When( + r'the user copies file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', + regexp=True, +) +def step(context, resource_name, destination): + copy_resource('file', resource_name, destination, True) -# @When('the user deletes the following files') -# def step(context): -# wait_for_client_to_be_ready() +@When('the user deletes the following files') +def step(context): + wait_for_client_to_be_ready() -# for row in context.table[1:]: -# filename = row[0] -# deleteResource(filename, 'file') + for row in context.table[1:]: + filename = row[0] + deleteResource(filename, 'file') -# @Given('user "|any|" has created a file "|any|" with size "|any|" in the sync folder') -# def step(context, _, filename, filesize): -# create_file_with_size(filename, filesize) +@Given('user "|any|" has created a file "|any|" with size "|any|" in the sync folder') +def step(context, _, filename, filesize): + create_file_with_size(filename, filesize) diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index 1143374dfe..afe2acede6 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -1,44 +1,66 @@ from behave import given as Given, then as Then from sure import ensure + from helpers.api import provisioning, webdav_helper as webdav +from helpers.TableParser import table_rows_hash @Given('user "{user}" has been created in the server with default attributes') def step(context, user): provisioning.create_user(user) -@Then('as "{user_name}" {resource_type:ResourceType} "{resource_name}" should not exist in the server') + +@Then( + 'as "{user_name}" {resource_type:ResourceType} "{resource_name}" should not exist in the server' +) def step(context, user_name, resource_type, resource_name): resource_exists = webdav.resource_exists(user_name, resource_name) - with ensure('{0} "{1}" should not exist, but it does', resource_type.capitalize(), resource_name): + with ensure( + '{0} "{1}" should not exist, but it does', + resource_type.capitalize(), + resource_name, + ): resource_exists.should.be.false -@Then('as "{user_name}" {resource_type:ResourceType} "{resource_name}" should exist in the server') +@Then( + 'as "{user_name}" {resource_type:ResourceType} "{resource_name}" should exist in the server' +) def step(context, user_name, resource_type, resource_name): resource_exists = webdav.resource_exists(user_name, resource_name) - with ensure('{0} "{1}" should exist, but it does not', resource_type.capitalize(), resource_name): + with ensure( + '{0} "{1}" should exist, but it does not', + resource_type.capitalize(), + resource_name, + ): resource_exists.should.be.true -@Then('as "{user_name}" the file "{file_name}" should have the content "{content}" in the server') +@Then( + 'as "{user_name}" the file "{file_name}" should have the content "{content}" in the server' +) def step(context, user_name, file_name, content): text_content = webdav.get_file_content(user_name, file_name) - with ensure('{0} should have content "{1}" but found "{2}"', file_name, content, text_content): + with ensure( + '{0} should have content "{1}" but found "{2}"', + file_name, + content, + text_content, + ): text_content.should.equal(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' -# ) +@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 "{user}" has created folder "{folder_name}" in the server') @@ -46,83 +68,81 @@ def step(context, user, folder_name): webdav.create_folder(user, folder_name) -# @Given('user "{user}" has been created in the server with default attributes') -# def step(context, user): -# provisioning.create_user(user) +@Given( + 'user "{user}" has uploaded file with content "{file_content}" to "{file_name}" 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() -@Given('user "{user}" has uploaded file with content "{file_content}" to "{file_name}" in the server') + +@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('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 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'], -# ) +@When('user "{user}" deletes the folder "{folder_name}" in the server') +def step(context, user, folder_name): + webdav.delete_resource(user, folder_name) + + +@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 "{user}" has sent the following resource share invitation:') +def step(context, user): + resource_details = table_rows_hash(context.table) + webdav.send_resource_share_invitation( + user, + resource_details['resource'], + resource_details['sharee'], + resource_details['permissionsRole'], + ) diff --git a/test/gui/steps/spaces_context.py b/test/gui/steps/spaces_context.py index 7150929451..d5d8c2e980 100644 --- a/test/gui/steps/spaces_context.py +++ b/test/gui/steps/spaces_context.py @@ -1,5 +1,6 @@ -from pageObjects.EnterPassword import EnterPassword +from sure import ensure +from pageObjects.EnterPassword import EnterPassword from helpers.UserHelper import get_password_for_user from helpers.SetupClientHelper import setup_client, get_resource_path from helpers.SyncHelper import wait_for_initial_sync_to_complete @@ -14,29 +15,31 @@ from helpers.ConfigHelper import get_config, set_config -@Given('the administrator has created a space "|any|"') +@Given('the administrator has created a space "{space_name}"') def step(context, space_name): create_space(space_name) -@Given('the administrator has created a folder "|any|" in space "|any|"') +@Given('the administrator has created a folder "{folder_name}" in space "{space_name}"') def step(context, folder_name, space_name): create_space_folder(space_name, folder_name) @Given( - 'the administrator has uploaded a file "|any|" with content "|any|" inside space "|any|"' + 'the administrator has uploaded a file "{file_name}" with content "{content}" inside space "{space_name}"' ) def step(context, file_name, content, space_name): create_space_file(space_name, file_name, content) -@Given('the administrator has added user "|any|" to space "|any|" with role "|any|"') +@Given( + 'the administrator has added user "{user}" to space "{space_name}" with role "{role}"' +) def step(context, user, space_name, role): add_user_to_space(user, space_name, role) -@Given('user "|any|" has set up a client with space "|any|"') +@Given('user "{user}" has set up a client with space "{space_name}"') def step(context, user, space_name): set_config('syncConnectionName', space_name) password = get_password_for_user(user) @@ -49,17 +52,27 @@ def step(context, user, space_name): @Then( - 'as "|any|" the file "|any|" in the space "|any|" should have content "|any|" in the server' + 'as "{user}" the file "{file_name}" in the space "{space_name}" should have content "{content}" in the server' ) def step(context, user, file_name, space_name, content): downloaded_content = get_file_content(space_name, file_name, user) - test.compare(downloaded_content, content, 'Comparing file content') + with ensure( + 'File "{0}" in space "{1}" should have content "{2}" but got "{3}"', + file_name, + space_name, + content, + downloaded_content, + ): + content.should.equal(downloaded_content) @Then( - r'as "([^"]*)" the space "([^"]*)" should have (?:folder|file) "([^"]*)" in the server', - regexp=True, + 'as "{user}" the space "{space_name}" should have file "{resource_name}" in the server' +) +@Then( + 'as "{user}" the space "{space_name}" should have folder "{resource_name}" in the server' ) def step(context, user, space_name, resource_name): exists = resource_exists(space_name, resource_name, user) - test.compare(exists, True, 'Resource exists') + with ensure('Resource "{0}" should exist but it does not', resource_name): + exists.should.be.true diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 971505bb77..63a7fb034b 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -1,41 +1,38 @@ from behave import when as When, then as Then +from sure import ensure 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 +from pageObjects.SyncConnection import SyncConnection +from pageObjects.Settings import Settings +from helpers.ConfigHelper import set_config from helpers.SyncHelper import ( wait_for_resource_to_sync, - # wait_for_resource_to_have_sync_error, + wait_for_resource_to_have_sync_error, ) from helpers.SetupClientHelper import ( - # get_temp_resource_path, - # set_current_user_sync_path, + get_temp_resource_path, + set_current_user_sync_path, substitute_inline_codes, get_resource_path, ) - from helpers.FilesHelper import convert_path_separators_for_os -# @Given('the user has paused the file sync') -# def step(context): -# SyncConnection.pause_sync() +@Given('the user has paused the file sync') +def step(context): + SyncConnection.pause_sync() -# @When('the user resumes the file sync on the client') -# def step(context): -# SyncConnection.resume_sync() +@When('the user resumes the file sync on the client') +def step(context): + SyncConnection.resume_sync() -# @When('the user force syncs the files') -# def step(context): -# SyncConnection.force_sync() +@When('the user force syncs the files') +def step(context): + SyncConnection.force_sync() @When('the user waits for the files to sync') @@ -49,34 +46,34 @@ def step(context, resource_type, resource): wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) -# @When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) -# def step(context, resource_type, resource): -# resource = get_resource_path(resource) -# wait_for_resource_to_have_sync_error(resource, resource_type) +@When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) +def step(context, resource_type, resource): + resource = get_resource_path(resource) + wait_for_resource_to_have_sync_error(resource, resource_type) -# @When( -# r'user "([^"]*)" waits for (file|folder) "([^"]*)" to have sync error', regexp=True -# ) -# def step(context, username, resource_type, resource): -# resource = get_resource_path(resource, username) -# wait_for_resource_to_have_sync_error(resource, resource_type) +@When( + r'user "([^"]*)" waits for (file|folder) "([^"]*)" to have sync error', regexp=True +) +def step(context, username, resource_type, resource): + resource = get_resource_path(resource, username) + wait_for_resource_to_have_sync_error(resource, resource_type) -# @Then('the "|any|" button should be available') -# def step(context, item): -# SyncConnection.open_menu() -# SyncConnection.has_menu_item(item) +@Then('the "|any|" button should be available') +def step(context, item): + SyncConnection.open_menu() + SyncConnection.has_menu_item(item) -# @Then('the "|any|" button should not be available') -# def step(context, item): -# SyncConnection.open_menu() -# test.compare( -# SyncConnection.menu_item_exists(item), -# False, -# f'Menu item "{item}" does not exist.', -# ) +@Then('the "|any|" button should not be available') +def step(context, item): + SyncConnection.open_menu() + test.compare( + SyncConnection.menu_item_exists(item), + False, + f'Menu item "{item}" does not exist.', + ) @When('the user clicks on the activity tab') @@ -84,26 +81,26 @@ def step(context): Toolbar.open_activity() -# @Then('the table of conflict warnings should include file "|any|"') -# def step(context, filename): -# Activity.check_file_exist(filename) +@Then('the table of conflict warnings should include file "|any|"') +def step(context, filename): + Activity.check_file_exist(filename) -# @Then('the file "|any|" should be blacklisted') -# def step(context, filename): -# test.compare( -# True, Activity.is_resource_blacklisted(filename), 'File is Blacklisted' -# ) +@Then('the file "|any|" should be blacklisted') +def step(context, filename): + test.compare( + True, Activity.is_resource_blacklisted(filename), 'File is Blacklisted' + ) -# @Then('the file "|any|" should be ignored') -# def step(context, filename): -# test.compare(True, Activity.is_resource_ignored(filename), 'File is Ignored') +@Then('the file "|any|" should be ignored') +def step(context, filename): + test.compare(True, Activity.is_resource_ignored(filename), 'File is Ignored') -# @Then('the file "|any|" should be excluded') -# def step(context, filename): -# test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') +@Then('the file "|any|" should be excluded') +def step(context, filename): + test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') @When('the user selects "{tab_name}" tab in the activity') @@ -111,67 +108,69 @@ def step(context, tab_name): Activity.click_tab(tab_name) -# @Then('the toolbar should have the following tabs:') -# def step(context): -# for tab_name in context.table: -# Toolbar.has_item(tab_name[0]) +@Then('the toolbar should have the following tabs:') +def step(context): + for tab_name in context.table: + Toolbar.has_item(tab_name[0]) -# @When('the user selects the following folders to sync:') -# def step(context): -# folders = [] -# for row in context.table[1:]: -# folders.append(row[0]) -# SyncConnectionWizard.select_folders_to_sync(folders, new_sync_connection_wizard=True) +@When('the user selects the following folders to sync:') +def step(context): + folders = [] + for row in context.table[1:]: + folders.append(row[0]) + SyncConnectionWizard.select_folders_to_sync( + folders, new_sync_connection_wizard=True + ) -# @When('the user sorts the folder list by "|any|"') -# def step(context, header_text): -# if (header_text := header_text.capitalize()) in ['Size', 'Name']: -# SyncConnectionWizard.sort_by(header_text) -# else: -# raise ValueError("Sorting by '" + header_text + "' is not supported.") +@When('the user sorts the folder list by "|any|"') +def step(context, header_text): + if (header_text := header_text.capitalize()) in ['Size', 'Name']: + SyncConnectionWizard.sort_by(header_text) + else: + raise ValueError("Sorting by '" + header_text + "' is not supported.") -# @Then('the sync all checkbox should be checked') -# def step(context): -# test.compare( -# SyncConnectionWizard.is_root_folder_checked(), -# True, -# 'Sync all checkbox is checked', -# ) +@Then('the sync all checkbox should be checked') +def step(context): + test.compare( + SyncConnectionWizard.is_root_folder_checked(), + True, + 'Sync all checkbox is checked', + ) -# @Then('the folders should be in the following order:') -# def step(context): -# row_index = 0 -# for row in context.table[1:]: -# expected_folder = row[0] -# actual_folder = SyncConnectionWizard.get_item_name_from_row(row_index) -# test.compare(actual_folder, expected_folder) +@Then('the folders should be in the following order:') +def step(context): + row_index = 0 + for row in context.table[1:]: + expected_folder = row[0] + actual_folder = SyncConnectionWizard.get_item_name_from_row(row_index) + test.compare(actual_folder, expected_folder) -# row_index += 1 + row_index += 1 -# @When('the user selects "|any|" space in sync connection wizard') -# def step(context, space_name): -# SyncConnectionWizard.select_space(space_name) -# SyncConnectionWizard.next_step() -# set_config('syncConnectionName', space_name) +@When('the user selects "|any|" space in sync connection wizard') +def step(context, space_name): + SyncConnectionWizard.select_space(space_name) + SyncConnectionWizard.next_step() + set_config('syncConnectionName', space_name) -# @When('the user sets the sync path in sync connection wizard') -# def step(context): -# SyncConnectionWizard.set_sync_path() +@When('the user sets the sync path in sync connection wizard') +def step(context): + SyncConnectionWizard.set_sync_path() -# @When( -# 'the user sets the temp folder "|any|" as local sync path in sync connection wizard' -# ) -# def step(context, folder_name): -# sync_path = get_temp_resource_path(folder_name) -# SyncConnectionWizard.set_sync_path(sync_path) -# set_current_user_sync_path(sync_path) +@When( + 'the user sets the temp folder "|any|" as local sync path in sync connection wizard' +) +def step(context, folder_name): + sync_path = get_temp_resource_path(folder_name) + SyncConnectionWizard.set_sync_path(sync_path) + set_current_user_sync_path(sync_path) @When('the user syncs the "{space_name}" space') @@ -179,81 +178,82 @@ def step(context, space_name): SyncConnectionWizard.sync_space(space_name) -# @Then('the settings tab should have the following options in the general section:') -# def step(context): -# for item in context.table: -# Settings.check_general_option(item[0]) +@Then('the settings tab should have the following options in the general section:') +def step(context): + for item in context.table: + Settings.check_general_option(item[0]) -# @Then('the settings tab should have the following options in the advanced section:') -# def step(context): -# for item in context.table: -# Settings.check_advanced_option(item[0]) +@Then('the settings tab should have the following options in the advanced section:') +def step(context): + for item in context.table: + Settings.check_advanced_option(item[0]) -# @Then('the settings tab should have the following options in the network section:') -# def step(context): -# for item in context.table: -# Settings.check_network_option(item[0]) +@Then('the settings tab should have the following options in the network section:') +def step(context): + for item in context.table: + Settings.check_network_option(item[0]) -# @When('the user opens the about dialog') -# def step(context): -# Settings.open_about_button() +@When('the user opens the about dialog') +def step(context): + Settings.open_about_button() -# @Then('the about dialog should be opened') -# def step(context): -# Settings.wait_for_about_dialog_to_be_visible() +@Then('the about dialog should be opened') +def step(context): + Settings.wait_for_about_dialog_to_be_visible() -# @When('the user adds the folder sync connection') -# def step(context): -# SyncConnectionWizard.add_sync_connection() +@When('the user adds the folder sync connection') +def step(context): + SyncConnectionWizard.add_sync_connection() -# @When('user unselects all the remote folders') -# def step(context): -# SyncConnectionWizard.deselect_all_remote_folders() +@When('user unselects all the remote folders') +def step(context): + SyncConnectionWizard.deselect_all_remote_folders() -# @Then('the sync folder list should be empty') -# def step(context): -# test.compare( -# 0, -# SyncConnection.get_folder_connection_count(), -# 'Sync connections should be empty', -# ) +@Then('for user "{user}" sync folder "{sync_folder}" should not be displayed') +def step(context, user, sync_folder): + Toolbar.open_account(user) + has_sync_connection = SyncConnection.has_sync_connection(sync_folder) + with ensure( + 'There should not be "{0}" folder sync connection, but found.', sync_folder + ): + has_sync_connection.should.be.false -# @When('the user navigates back in the sync connection wizard') -# def step(context): -# SyncConnectionWizard.back() +@When('the user navigates back in the sync connection wizard') +def step(context): + SyncConnectionWizard.back() -# @When('the user removes the folder sync connection') -# def step(context): -# SyncConnection.remove_folder_sync_connection() -# SyncConnection.confirm_folder_sync_connection_removal() +@When('the user removes the folder sync connection') +def step(context): + SyncConnection.remove_folder_sync_connection() + SyncConnection.confirm_folder_sync_connection_removal() -# @Then('the file "|any|" should have status "|any|" in the activity tab') -# def step(context, file_name, status): -# Activity.has_sync_status(file_name, status) +@Then('the file "|any|" should have status "|any|" in the activity tab') +def step(context, file_name, status): + Activity.has_sync_status(file_name, status) -# @When('the user opens the sync connection wizard') -# def step(context): -# SyncConnectionWizard.open_sync_connection_wizard() +@When('the user opens the sync connection wizard') +def step(context): + SyncConnectionWizard.open_sync_connection_wizard() -# @Then('the button to open sync connection wizard should be disabled') -# def step(context): -# test.compare( -# False, -# SyncConnectionWizard.is_add_sync_folder_button_enabled(), -# 'Button to open sync connection wizard should be disabled', -# ) +@Then('the button to open sync connection wizard should be disabled') +def step(context): + test.compare( + False, + SyncConnectionWizard.is_add_sync_folder_button_enabled(), + 'Button to open sync connection wizard should be disabled', + ) @When('the user checks the activities of account "{account}"') @@ -271,50 +271,57 @@ def step(context): Activity.check_synced_table(resource, action, account) -# @Then(r'the following activities (should|should not) be displayed in not synced table', regexp=True) -# def step(context, should_or_should_not): -# expected = should_or_should_not == "should" -# for row in context.table[1:]: -# resource = row[0] -# status = row[1] -# account = substitute_inline_codes(row[2]) -# test.compare( -# Activity.check_not_synced_table(resource, status, account), -# expected, -# 'Resource should be displayed in the not synced table', -# ) +@Then( + r'the following activities (should|should not) be displayed in not synced table', + regexp=True, +) +def step(context, should_or_should_not): + expected = should_or_should_not == "should" + for row in context.table[1:]: + resource = row[0] + status = row[1] + account = substitute_inline_codes(row[2]) + test.compare( + Activity.check_not_synced_table(resource, status, account), + expected, + 'Resource should be displayed in the not synced table', + ) -# @When('the user unchecks the "|any|" filter') -# def step(context, filter_option): -# Activity.select_not_synced_filter(filter_option) +@When('the user unchecks the "|any|" filter') +def step(context, filter_option): + Activity.select_not_synced_filter(filter_option) -# @Then('the following error message should appear in the client') -# def step(context): -# expected_error_message = '\n'.join(context.multiLineText) +@Then('the following error message should appear in the client') +def step(context): + expected_error_message = '\n'.join(context.multiLineText) -# actual_error_message = SyncConnection.get_permission_error_message() + actual_error_message = SyncConnection.get_permission_error_message() -# # wait for error message to disappear -# SyncConnection.wait_for_error_label(False) + # wait for error message to disappear + SyncConnection.wait_for_error_label(False) -# test.compare( -# actual_error_message, -# expected_error_message, -# f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"' -# ) + test.compare( + actual_error_message, + expected_error_message, + f'Expected error message: "{expected_error_message}" but got: "{actual_error_message}"', + ) -# @Given('the user has waited for "|any|" seconds') -# def step(context, wait_for): -# squish.snooze(float(wait_for)) +@Given('the user has waited for "|any|" seconds') +def step(context, wait_for): + squish.snooze(float(wait_for)) -# @When('the user unselects the following folders to sync in "Choose what to sync" window:') -# def step(context): -# SyncConnection.choose_what_to_sync() -# folders = [] -# for row in context.table[1:]: -# folders.append(row[0]) -# SyncConnectionWizard.unselect_folders_to_sync(folders, new_sync_connection_wizard=False) +@When( + 'the user unselects the following folders to sync in "Choose what to sync" window:' +) +def step(context): + SyncConnection.choose_what_to_sync() + folders = [] + for row in context.table[1:]: + folders.append(row[0]) + SyncConnectionWizard.unselect_folders_to_sync( + folders, new_sync_connection_wizard=False + ) diff --git a/test/gui/tst_spaces/test.py b/test/gui/tst_spaces/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_spaces/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 0e4bc3f6ff950d6e4823c8d4d2c71aef68bba59b Mon Sep 17 00:00:00 2001 From: Pradip Subedi Date: Tue, 28 Apr 2026 10:04:40 +0545 Subject: [PATCH 37/75] test: port tst_edit_files (#875) * test: port tst_deleteFilesFolders suite * test: port tst_editFiles * test: remove resource_types registration from file * test: fix indentation in feature file * test: remove resource_types registration from file --- .../edit-files/edit.feature} | 34 +++++++++---------- test/gui/tst_editFiles/test.py | 8 ----- 2 files changed, 17 insertions(+), 25 deletions(-) rename test/gui/{tst_editFiles/test.feature => features/edit-files/edit.feature} (55%) delete mode 100644 test/gui/tst_editFiles/test.py diff --git a/test/gui/tst_editFiles/test.feature b/test/gui/features/edit-files/edit.feature similarity index 55% rename from test/gui/tst_editFiles/test.feature rename to test/gui/features/edit-files/edit.feature index 308446fdb4..11215dba43 100644 --- a/test/gui/tst_editFiles/test.feature +++ b/test/gui/features/edit-files/edit.feature @@ -6,7 +6,7 @@ Feature: edit files Background: Given user "Alice" has been created in the server with default attributes - @smoke @skip + @smoke Scenario: Modify original content of a file with special character Given user "Alice" has uploaded file with content "openCloud test text file 0" to "S@mpleFile!With,$pecial&Characters.txt" in the server And user "Alice" has set up a client with default settings @@ -14,7 +14,7 @@ Feature: edit files And the user waits for file "S@mpleFile!With,$pecial&Characters.txt" to be synced Then as "Alice" the file "S@mpleFile!With,$pecial&Characters.txt" should have the content "overwrite openCloud test text file" in the server - @smoke @skip + @smoke Scenario: Modify original content of a file Given user "Alice" has set up a client with default settings When user "Alice" creates a file "testfile.txt" with the following content inside the sync folder @@ -27,18 +27,18 @@ Feature: edit files Then as "Alice" the file "testfile.txt" should have the content "overwrite openCloud test text file" in the server - Scenario Outline: Replace and modify the content of a file multiple times - Given user "Alice" has set up a client with default settings - And the user has copied file "" from outside the sync folder to "/" in the sync folder - When the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for the files to sync - And the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for the files to sync - And the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for the files to sync - Then as "Alice" the content of file "" in the server should match the content of local file "" - Examples: - | initialFile | updateFile1 | updateFile2 | updateFile3 | - | simple.pdf | simple1.pdf | simple2.pdf | simple3.pdf | - | simple.docx | simple1.docx | simple2.docx | simple3.docx | - | simple.xlsx | simple1.xlsx | simple2.xlsx | simple3.xlsx | + Scenario Outline: Replace and modify the content of a file multiple times + Given user "Alice" has set up a client with default settings + And the user has copied file "" from outside the sync folder to "/" in the sync folder + When the user copies file "" from outside the sync folder to "/" in the sync folder + And the user waits for the files to sync + And the user copies file "" from outside the sync folder to "/" in the sync folder + And the user waits for the files to sync + And the user copies file "" from outside the sync folder to "/" in the sync folder + And the user waits for the files to sync + Then as "Alice" the content of file "" in the server should match the content of local file "" + Examples: + | initialFile | updateFile1 | updateFile2 | updateFile3 | + | simple.pdf | simple1.pdf | simple2.pdf | simple3.pdf | + | simple.docx | simple1.docx | simple2.docx | simple3.docx | + | simple.xlsx | simple1.xlsx | simple2.xlsx | simple3.xlsx | diff --git a/test/gui/tst_editFiles/test.py b/test/gui/tst_editFiles/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_editFiles/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From d70efc93aaa9c1ece535eb9eeb565f13e45c2ef9 Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:30:45 +0545 Subject: [PATCH 38/75] test(gui): enable more syncing tests in appium (#887) * enable more syncing tests in appium Signed-off-by: prashant-gurung899 * enable more syncing tests in appium Signed-off-by: prashant-gurung899 --------- Signed-off-by: prashant-gurung899 --- .../sync-resources/syncResources.feature | 12 +++++------ test/gui/helpers/ConfigHelper.py | 2 +- test/gui/pageObjects/Activity.py | 13 ++++++------ test/gui/pageObjects/SyncConnectionWizard.py | 11 +++++----- test/gui/steps/account_context.py | 1 + test/gui/steps/file_context.py | 20 +++++++++---------- test/gui/steps/server_context.py | 2 +- test/gui/steps/sync_context.py | 2 +- 8 files changed, 31 insertions(+), 32 deletions(-) diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 77c3f1eb69..44cd09a3a6 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -6,7 +6,7 @@ Feature: Syncing files Background: Given user "Alice" has been created in the server with default attributes - @issue-9281 @smoke @skip + @issue-9281 @smoke Scenario: Syncing a file to the server Given user "Alice" has set up a client with default settings When user "Alice" creates a file "lorem-for-upload.txt" with the following content inside the sync folder @@ -19,7 +19,7 @@ Feature: Syncing files Then the file "lorem-for-upload.txt" should have status "Uploaded" in the activity tab And as "Alice" the file "lorem-for-upload.txt" should have the content "test content" in the server - @smoke @skip + @smoke Scenario: Syncing all files and folders from the server Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -235,7 +235,7 @@ Feature: Syncing files And as "Alice" folder "parent/subfolder4" should exist in the server And as "Alice" folder "parent/subfolder5" should exist in the server - @smoke @skip + @smoke Scenario: Both original and copied folders can be synced Given user "Alice" has set up a client with default settings When user "Alice" creates a folder "original" inside the sync folder @@ -299,7 +299,7 @@ Feature: Syncing files | foldername | | An empty folder which name is obviously more than 59 characters | - @skipOnWindows @smoke @skip + @skipOnWindows @smoke Scenario: Invalid system names are synced (Linux only) Given user "Alice" has created folder "CON" in the server And user "Alice" has created folder "test%" in the server @@ -327,7 +327,7 @@ Feature: Syncing files But the folder "CON" should not exist on the file system And the file "PRN" should not exist on the file system - @smoke @skip + @smoke Scenario: various types of files can be synced from server to client Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has uploaded file "testavatar.png" to "simple-folder/testavatar.png" in the server @@ -428,7 +428,7 @@ Feature: Syncing files And as "Alice" folder "folder3" should exist in the server And as user "Alice" folder "folder3" should contain "1000" items in the server - @smoke @skip + @smoke Scenario: Skip sync folder configuration Given the user has started the client And the user has entered the following account information: diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index 74809ceb45..e6cd69c90b 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -113,7 +113,7 @@ def get_app_env(): 'clientConfigFile': os.path.join(get_config_home(), "OpenCloud", APP_CONFIG_FILE), 'guiTestReportDir': os.path.abspath('../reports'), 'record_video_on_failure': False, - 'files_for_upload': os.path.join(CURRENT_DIR.parent.parent, 'files-for-upload'), + 'files_for_upload': os.path.join(CURRENT_DIR.parent, 'files-for-upload'), 'syncConnectionName': 'Personal', } diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 96cb57d066..2586ff77ec 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -18,6 +18,7 @@ class Activity: NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) + SYNCED_ACTIVITY_STATUS = SimpleNamespace(by=By.NAME, selector=None) @staticmethod @@ -83,13 +84,11 @@ def is_resource_excluded(filename): @staticmethod def has_sync_status(filename, status): try: - file_row = squish.waitForObject( - Activity.get_not_synced_file_selector(filename), - get_config("lowestSyncTimeout") * 1000, - )["row"] - if Activity.get_not_synced_status(file_row) == status: - return True - return False + app().find_element( + Activity.SYNCED_ACTIVITY_STATUS.by, + status + ) + return True except: return False diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 3ed99b67c6..89963e5f0a 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -20,7 +20,7 @@ class SyncConnectionWizard: ) 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) + CANCEL_FOLDER_SYNC_CONNECTION_WIZARD = SimpleNamespace(by=By.NAME, selector="Cancel") 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) @@ -119,11 +119,10 @@ def is_root_folder_checked(): @staticmethod def cancel_folder_sync_connection_wizard(): - squish.clickButton( - squish.waitForObject( - SyncConnectionWizard.CANCEL_FOLDER_SYNC_CONNECTION_WIZARD - ) - ) + app().find_element( + SyncConnectionWizard.CANCEL_FOLDER_SYNC_CONNECTION_WIZARD.by, + SyncConnectionWizard.CANCEL_FOLDER_SYNC_CONNECTION_WIZARD.selector, + ).click() @staticmethod def select_space(space_name): diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index c10294245d..f3bd9b90b0 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -4,6 +4,7 @@ from sure import expect from pageObjects.AccountConnectionWizard import AccountConnectionWizard +from pageObjects.SyncConnectionWizard import SyncConnectionWizard from pageObjects.AccountSetting import AccountSetting from pageObjects.Toolbar import Toolbar from pageObjects.EnterPassword import EnterPassword diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 067d4df84c..4cb5605034 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -175,7 +175,7 @@ def step(context, resource_type, resource_name, destination_dir): copy_resource(resource_type, resource_name, destination_dir, False) -@When(r'the user copies (file|folder) "([^"]*)" into the same directory', regexp=True) +@When('the user copies {resource_type:ResourceType} "{resource_name}" into the same directory') def step(context, resource_type, resource_name): copy_resource(resource_type, resource_name, resource_name, False) @@ -187,19 +187,19 @@ def step(context, source, destination): rename_file_folder(source, destination) -@Then('the file "|any|" should exist on the file system with the following content') +@Then('the file "{file_path}" should exist on the file system with the following content') def step(context, file_path): - expected = '\n'.join(context.multiLineText) + expected = context.text file_path = get_resource_path(file_path) with open(file_path, 'r', encoding='utf-8') as f: contents = f.read() - test.compare( - expected, - contents, - 'file expected to exist with content ' - + expected - + ' but does not have the expected content', - ) + with ensure( + '{0} expected to exist with content "{1}" but has content "{2}"', + file_path, + expected, + contents, + ): + contents.should.equal(expected) @Then('the {resource_type:ResourceType} "{resource}" should exist on the file system') diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index afe2acede6..a51c8197d1 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -90,7 +90,7 @@ def step(context, user, folder_name): webdav.delete_resource(user, folder_name) -@Given('user "|any|" has uploaded file "|any|" to "|any|" in the server') +@Given('user "{user}" has uploaded file "{file_name}" to "{destination}" in the server') def step(context, user, file_name, destination): webdav.upload_file(user, file_name, destination) diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 63a7fb034b..2923ce780a 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -237,7 +237,7 @@ def step(context): SyncConnection.confirm_folder_sync_connection_removal() -@Then('the file "|any|" should have status "|any|" in the activity tab') +@Then('the file "{file_name}" should have status "{status}" in the activity tab') def step(context, file_name, status): Activity.has_sync_status(file_name, status) From ad4e2afc3d862872bb152f11519c70000ac284f6 Mon Sep 17 00:00:00 2001 From: Pradip Subedi Date: Tue, 28 Apr 2026 17:30:07 +0545 Subject: [PATCH 39/75] test: port tst_loginLogout (#877) * test: port tst_loginLogout * test: fix closing of client * test: Add reusable close_and_kill_app() for Appium session and process cleanup * test: remove test.py * test: use exception handlers in sync helper --- test/gui/environment.py | 11 ++----- .../login-logout/login-logout.feature} | 4 +-- test/gui/helpers/SetupClientHelper.py | 22 ++++++++++++++ test/gui/helpers/SyncHelper.py | 16 +++++----- test/gui/pageObjects/AccountSetting.py | 15 ++++++---- test/gui/pageObjects/Toolbar.py | 29 +++++++++++------- test/gui/steps/account_context.py | 30 +++++++++---------- test/gui/tst_loginLogout/test.py | 8 ----- 8 files changed, 79 insertions(+), 56 deletions(-) rename test/gui/{tst_loginLogout/test.feature => features/login-logout/login-logout.feature} (96%) delete mode 100644 test/gui/tst_loginLogout/test.py diff --git a/test/gui/environment.py b/test/gui/environment.py index 26764d37ac..3f9b9bc64a 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -1,4 +1,3 @@ -import psutil import shutil import os @@ -7,7 +6,7 @@ from helpers.SpaceHelper import delete_project_spaces from helpers.ConfigHelper import set_config, get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths -from helpers.SetupClientHelper import app +from helpers.SetupClientHelper import close_and_kill_app from step_types.types import * # register all step types @@ -37,10 +36,4 @@ def after_scenario(context, scenario): delete_project_spaces() delete_created_users() # quit the application - if app() is not None: - app().quit() - for process in psutil.process_iter(['pid', 'exe']): - if process.info['exe'] == get_config("app_path"): - print("Closing desktop client...") - psutil.Process(process.info['pid']).kill() - break + close_and_kill_app() diff --git a/test/gui/tst_loginLogout/test.feature b/test/gui/features/login-logout/login-logout.feature similarity index 96% rename from test/gui/tst_loginLogout/test.feature rename to test/gui/features/login-logout/login-logout.feature index aedd31706f..1b08bd854f 100644 --- a/test/gui/tst_loginLogout/test.feature +++ b/test/gui/features/login-logout/login-logout.feature @@ -6,13 +6,13 @@ Feature: Logout users Background: Given user "Alice" has been created in the server with default attributes - @smoke @skip + @smoke Scenario: logging out Given user "Alice" has set up a client with default settings When the user "Alice" logs out using the client-UI Then user "Alice" should be signed out - @smoke @skip + @smoke Scenario: login after logging out Given user "Alice" has set up a client with default settings And user "Alice" has logged out from the client-UI diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index d8e935969d..45392fb8b8 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -270,3 +270,25 @@ def run_sys_command(command=None, shell=False): check=False, ) return cmd.stdout, cmd.stderr, cmd.returncode + + +def close_and_kill_app(): + """ + Close Appium session and kill the desktop client process. + Use this for both mid-scenario and end-of-scenario cleanup. + """ + global app_driver + # Quit Appium session + if app_driver is not None: + app_driver.quit() + + # Kill remaining process by exe path + app_path = get_config("app_path") + for process in psutil.process_iter(['pid', 'exe']): + if process.info['exe'] == app_path: + print("Closing desktop client...") + psutil.Process(process.info['pid']).kill() + break + + # Reset driver for reuse + app_driver = None diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 3504bdee46..d1c1460561 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -2,6 +2,8 @@ import re import time import urllib.request +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import TimeoutException from helpers.ConfigHelper import get_config, is_linux, is_windows from helpers.FilesHelper import sanitize_path @@ -357,10 +359,10 @@ def make_available_locally(resource_path): def wait_for(condition, timeout, interval=0.5): - start = time.time() * 1000 - while True: - if condition(): - return True - if time.time() * 1000 - start > timeout: - return False - time.sleep(interval) + from helpers.SetupClientHelper import app + wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval) + try: + wait.until(lambda _: condition()) + return True + except TimeoutException: + return False diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 2418eb406a..3d8acc919a 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -1,5 +1,7 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By +from helpers.UserHelper import get_displayname_for_user +from helpers.SetupClientHelper import substitute_inline_codes, app from pageObjects.Toolbar import Toolbar from helpers.UserHelper import get_displayname_for_user @@ -16,7 +18,10 @@ class AccountSetting: CONFIRM_REMOVE_CONNECTION_BUTTON = SimpleNamespace( by=By.NAME, selector="Remove connection" ) - ACCOUNT_CONNECTION_LABEL = SimpleNamespace(by=None, selector=None) + ACCOUNT_CONNECTION_LABEL = SimpleNamespace( + by=By.XPATH, + selector="//list[@name='Folder Sync']//label", + ) LOG_BROWSER_WINDOW = SimpleNamespace(by=None, selector=None) ACCOUNT_LOADING = SimpleNamespace(by=None, selector=None) DIALOG_STACK = SimpleNamespace(by=None, selector=None) @@ -61,9 +66,8 @@ def login(): @staticmethod def get_account_connection_label(): - return str( - squish.waitForObjectExists(AccountSetting.ACCOUNT_CONNECTION_LABEL).text - ) + label = app().find_element(AccountSetting.ACCOUNT_CONNECTION_LABEL.by, AccountSetting.ACCOUNT_CONNECTION_LABEL.selector).text + return label @staticmethod def is_connecting(): @@ -93,7 +97,7 @@ def wait_until_connection_is_configured(timeout=5000): @staticmethod def wait_until_account_is_connected(timeout=5000): - result = squish.waitFor( + result = wait_for( AccountSetting.is_user_signed_in, timeout, ) @@ -106,6 +110,7 @@ def wait_until_account_is_connected(timeout=5000): ) return result + @staticmethod def wait_until_sync_folder_is_configured(timeout=5000): result = squish.waitFor( diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index d36410d8bd..6a88acb065 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -3,9 +3,8 @@ from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys -from helpers.SetupClientHelper import wait_until_app_killed +from helpers.SetupClientHelper import app, close_and_kill_app from helpers.ConfigHelper import get_config -from helpers.SetupClientHelper import app from helpers.UserHelper import get_displayname_for_user @@ -15,8 +14,14 @@ class Toolbar: ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") ACTIVITY_BUTTON = SimpleNamespace(by=By.NAME, selector="Activity") SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) - QUIT_BUTTON = SimpleNamespace(by=None, selector=None) - CONFIRM_QUIT_BUTTON = SimpleNamespace(by=None, selector=None) + QUIT_BUTTON = SimpleNamespace( + by=By.NAME, + selector="Quit" + ) + CONFIRM_QUIT_BUTTON = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.QMessageBox.qt_msgbox_buttonbox.QPushButton" + ) TOOLBAR_ITEMS = ["Add Account", "Activity", "Settings", "Quit"] @@ -75,12 +80,16 @@ def open_settings_tab(): @staticmethod def quit_opencloud(): - squish.mouseClick(squish.waitForObject(Toolbar.QUIT_BUTTON)) - squish.clickButton(squish.waitForObject(Toolbar.CONFIRM_QUIT_BUTTON)) - for ctx in squish.applicationContextList(): - pid = ctx.pid - ctx.detach() - wait_until_app_killed(pid) + app().find_element( + Toolbar.QUIT_BUTTON.by, + Toolbar.QUIT_BUTTON.selector + ).click() + app().find_element( + Toolbar.CONFIRM_QUIT_BUTTON.by, + Toolbar.CONFIRM_QUIT_BUTTON.selector + ).click() + close_and_kill_app() + @staticmethod def get_accounts(): diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index f3bd9b90b0..bd92e94bdb 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -1,13 +1,14 @@ import shutil import os from behave import given as Given, when as When, then as Then -from sure import expect +from sure import expect, ensure from pageObjects.AccountConnectionWizard import AccountConnectionWizard from pageObjects.SyncConnectionWizard import SyncConnectionWizard from pageObjects.AccountSetting import AccountSetting from pageObjects.Toolbar import Toolbar from pageObjects.EnterPassword import EnterPassword +from pageObjects.AccountSetting import AccountSetting from helpers.SetupClientHelper import ( start_client, setup_client, @@ -15,6 +16,7 @@ get_client_details, generate_account_config, get_resource_path, + app, ) from helpers.SyncHelper import ( wait_for_initial_sync_to_complete, @@ -113,28 +115,27 @@ def step(context): AccountConnectionWizard.add_account_information(account_details) -@When('the user "|any|" logs out using the client-UI') -def step(context, _): +@When('the user "{username}" logs out using the client-UI') +def step(context, username): AccountSetting.logout() -@Then('user "|any|" should be signed out') +@Then('user "{username}" should be signed out') def step(context, username): - test.compare( - AccountSetting.is_user_signed_out(), - True, - f'User "{username}" is signed out', - ) + user_signed_out = AccountSetting.is_user_signed_out() + + with ensure('User "{0}" should be signed out, but is still signed in', username): + user_signed_out.should.be.true -@Given('user "|any|" has logged out from the client-UI') +@Given('user "{username}" 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') +@When('user "{username}" logs in using the client-UI') def step(context, username): AccountSetting.login() password = get_password_for_user(username) @@ -150,10 +151,9 @@ def step(context, _): AccountSetting.login() -@Then('user "|any|" should be connected to the server') -def step(context, _): +@Then('user "{username}" should be connected to the server') +def step(context, username): AccountSetting.wait_until_account_is_connected() - AccountSetting.wait_until_sync_folder_is_configured() @When('the user removes the connection for user "{username}"') @@ -233,7 +233,7 @@ def step(context): test.compare(True, AccountSetting.is_log_dialog_visible(), 'Log dialog is opened') -@Step('the user cancels the sync connection wizard') +@When('the user cancels the sync connection wizard') def step(context): SyncConnectionWizard.cancel_folder_sync_connection_wizard() diff --git a/test/gui/tst_loginLogout/test.py b/test/gui/tst_loginLogout/test.py deleted file mode 100644 index 83b0a5275a..0000000000 --- a/test/gui/tst_loginLogout/test.py +++ /dev/null @@ -1,8 +0,0 @@ -source(findFile('scripts', 'python/bdd.py')) - -setupHooks('../shared/scripts/bdd_hooks.py') -collectStepDefinitions('./steps', '../shared/steps') - - -def main(): - runFeatureFile('test.feature') From 2cd3e84ee09fd4e65e637c4b002904783f45bcee Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Wed, 6 May 2026 12:06:55 +0545 Subject: [PATCH 40/75] test(gui): enable syncing tests (#889) * test(gui): enable syncing tests Signed-off-by: prashant-gurung899 * test: using pyautogui library for selecting checkboxes in resource selection --------- Signed-off-by: prashant-gurung899 Co-authored-by: pradip --- .../sync-resources/syncResources.feature | 5 +- test/gui/pageObjects/SyncConnectionWizard.py | 88 +++++++------------ test/gui/requirements.txt | 3 +- test/gui/steps/server_context.py | 2 +- test/gui/steps/sync_context.py | 6 +- 5 files changed, 43 insertions(+), 61 deletions(-) diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 44cd09a3a6..97d0480b24 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -82,7 +82,7 @@ Feature: Syncing files But the folder "simple-folder" should not exist on the file system And the folder "large-folder" should not exist on the file system - @skipOnWindows @smoke @skip + @skipOnWindows @smoke Scenario: Sync only one folder from the server Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -571,7 +571,7 @@ Feature: Syncing files And as "Brian" file "Shares/simple-folder/simple.pdf" should exist in the server And as "Brian" the file "Shares/simple-folder/uploaded-lorem.txt" should have the content "overwrite openCloud test text file" in the server - @skipOnWindows @smoke @skip + @skipOnWindows @smoke Scenario: Unselected subfolders are excluded from local sync Given user "Alice" has created folder "test-folder" in the server And user "Alice" has created folder "test-folder/sub-folder1" in the server @@ -580,6 +580,7 @@ Feature: Syncing files When the user unselects the following folders to sync in "Choose what to sync" window: | folder | | test-folder/sub-folder2 | + And the user waits for the files to sync Then the folder "test-folder/sub-folder1" should exist on the file system But the folder "test-folder/sub-folder2" should not exist on the file system When user "Alice" uploads file with content "some content" to "test-folder/sub-folder2/lorem.txt" in the server diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 89963e5f0a..3e4d1a33e8 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -2,6 +2,7 @@ from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys import time +import pyautogui from helpers.SetupClientHelper import get_current_user_sync_path from helpers.SetupClientHelper import app @@ -14,7 +15,6 @@ 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) - 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']" ) @@ -71,15 +71,12 @@ def select_remote_destination_folder(folder): @staticmethod def deselect_all_remote_folders(): - # NOTE: checkbox does not have separate object - # click on (11,11) which is a checkbox - squish.mouseClick( - squish.waitForObject(SyncConnectionWizard.SELECTIVE_SYNC_ROOT_FOLDER), - 11, - 11, - squish.Qt.NoModifier, - squish.Qt.LeftButton, + element = app().find_element( + By.NAME, + "Add Space" ) + element.send_keys(Keys.ARROW_DOWN) + element.send_keys(" ") @staticmethod def sort_by(header_text): @@ -201,55 +198,46 @@ def is_add_sync_folder_button_enabled(): @staticmethod def select_or_unselect_folders_to_sync( - folders, should_select=True, new_sync_connection_wizard=False + folders, + should_select=True ): if should_select: - # First deselect all SyncConnectionWizard.deselect_all_remote_folders() - folder_tree_locator = SyncConnectionWizard.get_folder_tree_locator( - new_sync_connection_wizard - ) + for folder in folders: - folder_levels = folder.strip("/").split("/") - parent_selector = None - for sub_folder in folder_levels: - if not parent_selector: - folder_tree_locator["text"] = sub_folder - parent_selector = folder_tree_locator - selector = parent_selector - else: - selector = { - "column": "0", - "container": parent_selector, - "text": sub_folder, - "type": "QModelIndex", - } - if ( - len(folder_levels) == 1 - or folder_levels.index(sub_folder) == len(folder_levels) - 1 - ): - # NOTE: checkbox does not have separate object - # click on (11,11) which is a checkbox - squish.mouseClick( - squish.waitForObject(selector), - 11, - 11, - squish.Qt.NoModifier, - squish.Qt.LeftButton, - ) - else: - squish.doubleClick(squish.waitForObject(selector)) + path_parts = folder.strip("/").split("/") + + for i in range(len(path_parts) - 1): + parent_folder_name = path_parts[i] + parent_elements = app().find_elements(By.NAME, parent_folder_name) + + for element in parent_elements: + isChecked = element.get_attribute("checked") + if isChecked == "true": + parent_bounds = element.rect + center_x = int(parent_bounds['x'] + parent_bounds['width'] // 2) + center_y = int(parent_bounds['y'] + parent_bounds['height'] // 2) + pyautogui.doubleClick(center_x, center_y) + + target_folder_name = path_parts[-1] + folder_element = app().find_element(By.NAME, target_folder_name) + + element_bounds = folder_element.rect + checkbox_x_position = int(element_bounds['x'] + 10) + checkbox_y_position = int(element_bounds['y'] + element_bounds['height'] // 2) + + pyautogui.moveTo(checkbox_x_position, checkbox_y_position, duration=0.1) + pyautogui.click() @staticmethod def confirm_choose_what_to_sync_selection(): - squish.clickButton(squish.waitForObject(names.stackedWidget_OK_QPushButton)) + app().find_element(By.NAME, "OK").click() @staticmethod 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, + should_select=should_select ) if new_sync_connection_wizard: @@ -272,11 +260,3 @@ def select_folders_to_sync(folders, new_sync_connection_wizard=False): should_select=True, new_sync_connection_wizard=new_sync_connection_wizard, ) - - @staticmethod - def get_folder_tree_locator(new_sync_connection_wizard=False): - return ( - SyncConnectionWizard.ADD_SPACE_FOLDER_TREE.copy() - if new_sync_connection_wizard - else SyncConnectionWizard.CHOOSE_WHAT_TO_SYNC_FOLDER_TREE.copy() - ) diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 958646f7db..942e0f72c4 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -15,4 +15,5 @@ behave==1.3.*; python_version >= "3.10" Appium-Python-Client==5.3.*; python_version >= "3.10" Flask==3.0.*; python_version >= "3.10" and sys_platform == 'linux' numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' -sure==2.0.*; python_version >= "3.10" \ No newline at end of file +sure==2.0.*; python_version >= "3.10" +pyautogui==0.9.*; python_version >= "3.10" diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index a51c8197d1..ef21698a61 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -80,7 +80,7 @@ def step(context): Toolbar.open_settings_tab() -@When('user "|any|" uploads file with content "|any|" to "|any|" in the server') +@When('user "{user}" uploads file with content "{file_content}" to "{file_name}" in the server') def step(context, user, file_content, file_name): webdav.create_file(user, file_name, file_content) diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 2923ce780a..92f4b21bb7 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -117,7 +117,7 @@ def step(context): @When('the user selects the following folders to sync:') def step(context): folders = [] - for row in context.table[1:]: + for row in context.table: folders.append(row[0]) SyncConnectionWizard.select_folders_to_sync( folders, new_sync_connection_wizard=True @@ -152,7 +152,7 @@ def step(context): row_index += 1 -@When('the user selects "|any|" space in sync connection wizard') +@When('the user selects "{space_name}" space in sync connection wizard') def step(context, space_name): SyncConnectionWizard.select_space(space_name) SyncConnectionWizard.next_step() @@ -320,7 +320,7 @@ def step(context, wait_for): def step(context): SyncConnection.choose_what_to_sync() folders = [] - for row in context.table[1:]: + for row in context.table: folders.append(row[0]) SyncConnectionWizard.unselect_folders_to_sync( folders, new_sync_connection_wizard=False From 9adebf5fd61c313841b41d927797e9a2186b7c98 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 7 May 2026 12:33:50 +0545 Subject: [PATCH 41/75] ci: run appium gui tests (#891) * test: fix make targets Signed-off-by: Saw-jan * ci: run appium GUI tests Signed-off-by: Saw-jan * ci: use workspace env Signed-off-by: Saw-jan * ci: fix cache path Signed-off-by: Saw-jan * ci: use dot to source file * ci: install browsers Signed-off-by: Saw-jan * ci: run GUI tests Signed-off-by: Saw-jan * ci: run GUI tests Signed-off-by: Saw-jan * ci: provide behave test dir Signed-off-by: Saw-jan * test: do not initialize socket connection Signed-off-by: Saw-jan * ci: run with verbose Signed-off-by: Saw-jan * ci: update ui-tests.yaml * ci: run a scenario * ci: run webdriver-atspi server * ci: update webdriver url * ci: fix path Signed-off-by: Saw-jan * test: use script to run the webdriver Signed-off-by: Saw-jan * ci: run all tests Signed-off-by: Saw-jan * ci: run failing tests Signed-off-by: Saw-jan * ci: fix cache Signed-off-by: Saw-jan * ci: run all smoke tests Signed-off-by: Saw-jan * ci: update build image Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 24 +-- .woodpecker/cache-pnpm.yaml | 93 ----------- .woodpecker/cache-python.yaml | 45 +++-- .woodpecker/purge-cache.yaml | 2 +- .woodpecker/ui-tests.yaml | 183 +++++++++------------ test/gui/.gitignore | 4 +- test/gui/.woodpecker.env | 1 + test/gui/Makefile | 18 +- test/gui/behave.ini | 5 +- test/gui/helpers/SetupClientHelper.py | 7 +- test/gui/helpers/SyncHelper.py | 17 +- test/gui/woodpecker/run_atspi_webdriver.sh | 38 +++++ test/gui/woodpecker/script.sh | 38 +++-- 13 files changed, 219 insertions(+), 256 deletions(-) delete mode 100644 .woodpecker/cache-pnpm.yaml create mode 100644 test/gui/.woodpecker.env create mode 100644 test/gui/woodpecker/run_atspi_webdriver.sh diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index 0178becc94..c296eb5437 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -1,5 +1,6 @@ variables: - - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' + - &alpine_image 'alpine:3.22.4' + - &build_image 'opencloudeu/desktop-client-build:ubuntu-24.04-qt6.10' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - &minio_environment AWS_ACCESS_KEY_ID: @@ -34,9 +35,6 @@ when: path: <<: *trigger_path - event: pull_request - branch: - - main - - stable-* path: <<: *trigger_path evaluate: | @@ -51,21 +49,25 @@ workspace: steps: - name: fix-permissions - image: owncloud/ubuntu:20.04 + image: *alpine_image commands: - - chmod o+w /woodpecker/desktop/ -R + - chmod o+w $CI_WORKSPACE/ -R + - name: build-client-for-ui-tests - image: *squish_image + image: *build_image commands: - - mkdir -p /woodpecker/desktop/build - - cd /woodpecker/desktop/build - - cmake %s -S .. -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + - mkdir -p build + - cd build + # currently, vfs plugin is broken so disable it to be able to build the client and run tests + # TODO: fix the vfs plugin and enable it back + - cmake -GNinja -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Debug -DVIRTUAL_FILE_SYSTEM_PLUGINS=off -S .. - ninja + - name: upload-desktop-client-cache image: *minio_image environment: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/build/bin s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ + - mc cp -a -r $CI_WORKSPACE/build/bin s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ - mc ls --recursive s3/$CACHE_BUCKET/desktop/client-build diff --git a/.woodpecker/cache-pnpm.yaml b/.woodpecker/cache-pnpm.yaml deleted file mode 100644 index 8f33e5f55b..0000000000 --- a/.woodpecker/cache-pnpm.yaml +++ /dev/null @@ -1,93 +0,0 @@ -variables: - - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - - &minio_environment - AWS_ACCESS_KEY_ID: - from_secret: cache_s3_access_key - AWS_SECRET_ACCESS_KEY: - from_secret: cache_s3_secret_key - CACHE_BUCKET: - from_secret: cache_s3_bucket - MC_HOST: 'https://s3.ci.opencloud.eu' - - &trigger_path - include: - - test/gui/** - - src/** - - cmake/** - - THEME.cmake - - VERSION.cmake - - CMakeLists.txt - - OPENCLOUD.cmake - - .woodpecker/** - exclude: - - .woodpecker/ready-release-go.yaml - - .woodpecker/translation.yaml - - test/gui/manual-test-plan - -when: - - branch: - - main - - stable-* - event: - - push - - manual - path: - <<: *trigger_path - - event: tag - - event: pull_request - branch: - - main - - stable-* - path: - <<: *trigger_path - evaluate: | - !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - - event: cron - cron: nightly* - -workspace: - base: /woodpecker/ - path: desktop - -steps: - - name: check-browsers-cache - image: *minio_image - environment: - <<: *minio_environment - commands: - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc ls --recursive s3/$CACHE_BUCKET/web - - bash test/gui/woodpecker/script.sh check_browsers_cache - - - name: pnpm-install - image: owncloudci/nodejs:20 - commands: - - . ./.woodpecker.env - - if $BROWSER_CACHE_FOUND; then exit 0; fi - - cd test/gui/ - - npm i -s -g -f "$(jq -r ".packageManager" < webUI/package.json)" - - pnpm config set store-dir ./.pnpm-store - - make pnpm-install - - - name: install-browsers - image: owncloudci/nodejs:20 - environment: - PLAYWRIGHT_BROWSERS_PATH: .playwright - commands: - - . ./.woodpecker.env - - if $BROWSER_CACHE_FOUND; then exit 0; fi - - cd test/gui/ - - make pnpm-install-chromium - - cd webUI - - tar -czvf /woodpecker/desktop/playwright-browsers.tar.gz .playwright - - - name: upload-browsers-cache - image: *minio_image - environment: - <<: *minio_environment - commands: - - . ./.woodpecker.env - - if $BROWSER_CACHE_FOUND; then exit 0; fi - - playwright_version=$(bash test/gui/woodpecker/script.sh get_playwright_version) - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -r -a /woodpecker/desktop/playwright-browsers.tar.gz s3/$CACHE_BUCKET/web/browsers-cache/$playwright_version/ - - mc ls --recursive s3/$CACHE_BUCKET/web diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index da43eb3bee..014f0f5796 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -1,5 +1,5 @@ variables: - - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' + - &build_image 'opencloudeu/desktop-client-build:ubuntu-24.04-qt6.10' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - &minio_environment AWS_ACCESS_KEY_ID: @@ -35,9 +35,6 @@ when: <<: *trigger_path - event: tag - event: pull_request - branch: - - main - - stable-* path: <<: *trigger_path evaluate: | @@ -50,26 +47,46 @@ workspace: path: desktop steps: - - name: check-python-cache + - name: check-caches image: *minio_image environment: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc ls s3/$CACHE_BUCKET/desktop/python-cache/ - bash test/gui/woodpecker/script.sh check_python_cache + - bash test/gui/woodpecker/script.sh check_browsers_cache - name: install-python-modules - image: *squish_image + image: *build_image environment: - PYTHONUSERBASE: /woodpecker/desktop + PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/test/gui/.playwright commands: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi - - make -C test/gui/ pip-install - - python3.10 -m pip list -v - - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - - tar -czvf /woodpecker/desktop/python-cache-$requirements_sha.tar.gz lib/python3.10/site-packages + - cd test/gui/ + - python3 -m venv .venv --system-site-packages + - . .venv/bin/activate + - make install + - tar -czf python-cache.tar.gz .venv + + - name: archive-browsers + image: *build_image + commands: + - . ./.woodpecker.env + - if $BROWSER_CACHE_FOUND; then exit 0; fi + - cd test/gui/ + - tar -czf playwright-browsers.tar.gz .playwright + + - name: upload-browsers-cache + image: *minio_image + environment: + <<: *minio_environment + commands: + - . ./.woodpecker.env + - if $BROWSER_CACHE_FOUND; then exit 0; fi + - playwright_version=$(bash test/gui/woodpecker/script.sh get_playwright_version) + - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + - mc cp -r -a $CI_WORKSPACE/test/gui/playwright-browsers.tar.gz s3/$CACHE_BUCKET/desktop/browsers-cache/$playwright_version/ - name: upload-python-cache image: *minio_image @@ -78,6 +95,6 @@ steps: commands: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi + - requirements_sha=$(bash test/gui/woodpecker/script.sh get_requirementstxt_hash) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -r -a /woodpecker/desktop/python-cache*.tar.gz s3/$CACHE_BUCKET/desktop/python-cache/ - - mc ls s3/$CACHE_BUCKET/desktop/python-cache/ + - mc cp -r -a $CI_WORKSPACE/test/gui/python-cache*.tar.gz s3/$CACHE_BUCKET/desktop/python-cache/$requirements_sha/ diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index a7bac0bfc9..a9eb9c6d53 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -37,7 +37,7 @@ matrix: PURGE_PATH: desktop/python-cache/ TTL: 14d - JOB_NAME: purge-browsers-cache - PURGE_PATH: web/browsers-cache/ + PURGE_PATH: desktop/browsers-cache/ TTL: 14d - JOB_NAME: purge-logs PURGE_PATH: desktop/testlogs/ diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 6e81b95050..b45f38401d 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -1,7 +1,8 @@ variables: - - &squish_image 'opencloudeu/squish@sha256:6eaecc218044020f49f24fd29b6bdc052e8170699a762687b10398b353e5fcda' + - &opencloud_image 'quay.io/opencloudeu/opencloud-rolling:latest' + - &build_image 'opencloudeu/desktop-client-build:ubuntu-24.04-qt6.10' - &minio_image 'minio/mc:RELEASE.2021-10-07T04-19-58Z' - - &ubuntu_image 'owncloud/ubuntu:20.04' + - &alpine_image 'alpine:3.22.4' - &minio_environment AWS_ACCESS_KEY_ID: from_secret: cache_s3_access_key @@ -20,16 +21,12 @@ when: - push - manual - event: pull_request - branch: - - main - - stable-* - event: tag - event: cron cron: nightly* depends_on: - build - - cache-pnpm - cache-python workspace: @@ -37,14 +34,6 @@ workspace: path: desktop steps: - - name: pnpm-install - image: owncloudci/nodejs:20 - commands: - - cd test/gui/ - - npm i -s -g -f "$(jq -r ".packageManager" < webUI/package.json)" - - pnpm config set store-dir ./.pnpm-store - - make pnpm-install - - name: restore-python-cache image: *minio_image environment: @@ -52,16 +41,7 @@ steps: commands: - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a s3/$CACHE_BUCKET/desktop/python-cache/python-cache-$requirements_sha.tar.gz /woodpecker/desktop - - - name: install-python-modules - image: *squish_image - environment: - PYTHONUSERBASE: /woodpecker/desktop/ - commands: - - tar -xvf python-cache-*.tar.gz -C . - - make -C test/gui/ pip-install - - python3.10 -m pip list -v + - mc cp -a s3/$CACHE_BUCKET/desktop/python-cache/$requirements_sha/python-cache.tar.gz $CI_WORKSPACE - name: restore-browsers-cache image: *minio_image @@ -70,15 +50,28 @@ steps: commands: - playwright_version=$(bash test/gui/woodpecker/script.sh get_playwright_version) - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -r -a s3/$CACHE_BUCKET/web/browsers-cache/$playwright_version/playwright-browsers.tar.gz /woodpecker/desktop + - mc cp -r -a s3/$CACHE_BUCKET/desktop/browsers-cache/$playwright_version/playwright-browsers.tar.gz $CI_WORKSPACE - name: unzip-browsers-cache image: owncloud/ubuntu:20.04 commands: - - tar -xvf /woodpecker/desktop/playwright-browsers.tar.gz -C . + - cd test/gui + - tar -xf $CI_WORKSPACE/playwright-browsers.tar.gz -C ./ + + - name: install-python-modules + image: *build_image + environment: + PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/test/gui/.playwright + commands: + - cd test/gui + - tar -xf $CI_WORKSPACE/python-cache.tar.gz -C ./ + - python3 -m venv .venv --system-site-packages + - . .venv/bin/activate + - make install + - python3 -m pip list -v - name: opencloud - image: quay.io/opencloudeu/opencloud-rolling:latest + image: *opencloud_image detach: true environment: FRONTEND_SEARCH_MIN_LENGTH: '2' @@ -108,81 +101,69 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ /woodpecker/desktop/build - - ls -lh /woodpecker/desktop/build/bin - - - name: create-extra-directories - image: *ubuntu_image - commands: - - mkdir /woodpecker/desktop/test/gui/guiReportUpload/screenshots -p - - mkdir /woodpecker/desktop/test/gui/tmp -p - - mkdir /woodpecker/desktop/test/gui/clientLog -p - - chmod 777 /woodpecker/desktop/test/gui/ -R - - - name: squish-server-logs - image: *ubuntu_image - detach: true - commands: - - touch /woodpecker/desktop/test/gui/guiReportUpload/serverlog.log - - chmod 777 /woodpecker/desktop/test/gui/guiReportUpload/serverlog.log - - tail -f /woodpecker/desktop/test/gui/guiReportUpload/serverlog.log + - mc cp -a -r s3/$CACHE_BUCKET/desktop/client-build/${CI_COMMIT_SHA}/ $CI_WORKSPACE/build + - ls -lh $CI_WORKSPACE/build/bin - - name: UI-tests - image: *squish_image + - name: gui-tests + image: *build_image + pull: true environment: - PYTHONUSERBASE: /woodpecker/desktop/ - TMPDIR: /woodpecker/desktop/test/gui/tmp/ - CLIENT_LOG_DIR: /woodpecker/desktop/test/gui/clientLog/ - PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/.playwright - SQUISH_LICENSE_SERVER: - from_secret: squish_license_key - SQUISH_LICENSE_SERVER_API: squish.jankari.tech:49346 - SQUISH_LICENSE_SERVER_API_TOKEN: phzq4o1tJIVebL1kgSTAeKqZ5AoIDJfci + # system cache environment variables + PYTHONUSERBASE: /woodpecker/desktop/test/gui/.venv + PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/test/gui/.playwright + # webdriver environment variables + WEBDRIVER_HOST: 0.0.0.0 + WEBDRIVER_PORT: 4723 + WEBDRIVER_RUNNER: /woodpecker/desktop/test/gui/woodpecker/run_atspi_webdriver.sh + # test environment variables + APP_PATH: /woodpecker/desktop/build/bin/opencloud BACKEND_HOST: https://opencloud:9200 SECURE_BACKEND_HOST: https://opencloud:9200 - GUI_TEST_REPORT_DIR: /woodpecker/desktop/test/gui/guiReportUpload - SERVER_INI: /woodpecker/desktop/test/gui/woodpecker/server.ini - SQUISH_PARAMETERS: --testsuite /woodpecker/desktop/test/gui --reportgen html,/woodpecker/desktop/test/gui/guiReportUpload --envvar QT_LOGGING_RULES=sync.httplogger=true;gui.socketapi=false --tags ~@skip --tags ~@skipOnLinux - - - name: crash-log - image: *ubuntu_image - when: - - status: - - success - - failure - commands: - - cat /woodpecker/desktop/test/gui/tmp/OpenCloud-crash.log 2>/dev/null || exit 0 - - - name: upload-test-reports - image: *minio_image - when: - - status: failure - environment: - <<: *minio_environment - commands: - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r /woodpecker/desktop/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER - - mc cp -a -r /woodpecker/desktop/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/ - - - name: gui-test-reports - image: *minio_image - when: - - status: failure - environment: - <<: *minio_environment - commands: - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - bash test/gui/woodpecker/gui_test_reports.sh - - - name: client-log - image: *minio_image - when: - - status: failure - environment: - <<: *minio_environment - commands: - - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - cd /woodpecker/desktop/test/gui/clientLog/ - - echo "To download the logs, access the following links:" - - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/) - - "for f in $logs; do echo \"$MC_HOST/$f \n \" | cut -d '/' -f1-3,5-99; done" + CLIENT_LOG_DIR: /woodpecker/desktop/test/gui/clientLog/ + GUI_TEST_REPORT_DIR: /woodpecker/desktop/test/gui/reports + BEHAVE_TEST_DIR: /woodpecker/desktop/test/gui + # Cannot handle this tags format inside a container: --tags='@smoke and not @skip' + BEHAVE_PARAMETERS: '--tags=@smoke --tags=~@skip features' + + # - name: crash-log + # image: *alpine_image + # when: + # - status: + # - success + # - failure + # commands: + # - cat test/gui/tmp/OpenCloud-crash.log 2>/dev/null || exit 0 + + # - name: upload-test-reports + # image: *minio_image + # when: + # - status: failure + # environment: + # <<: *minio_environment + # commands: + # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + # - mc cp -a -r $CI_WORKSPACE/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER + # - mc cp -a -r $CI_WORKSPACE/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/ + + # - name: gui-test-reports + # image: *minio_image + # when: + # - status: failure + # environment: + # <<: *minio_environment + # commands: + # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + # - bash test/gui/woodpecker/gui_test_reports.sh + + # - name: client-log + # image: *minio_image + # when: + # - status: failure + # environment: + # <<: *minio_environment + # commands: + # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + # - cd test/gui/clientLog/ + # - echo "To download the logs, access the following links:" + # - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/) + # - "for f in $logs; do echo \"$MC_HOST/$f \n \" | cut -d '/' -f1-3,5-99; done" diff --git a/test/gui/.gitignore b/test/gui/.gitignore index e10e7e0638..1dc267c1e5 100644 --- a/test/gui/.gitignore +++ b/test/gui/.gitignore @@ -1,3 +1,5 @@ custom_lib/ reports -venv/ \ No newline at end of file +venv/ +__webdriver/ +reports/ \ No newline at end of file diff --git a/test/gui/.woodpecker.env b/test/gui/.woodpecker.env new file mode 100644 index 0000000000..aa7d7ed52d --- /dev/null +++ b/test/gui/.woodpecker.env @@ -0,0 +1 @@ +ATSPI_WEBDRIVER_VERSION="6682aeb734730c50949f0fda69827b6c5b50dbc7" \ No newline at end of file diff --git a/test/gui/Makefile b/test/gui/Makefile index 97435514ad..6c00d87e67 100644 --- a/test/gui/Makefile +++ b/test/gui/Makefile @@ -1,29 +1,25 @@ PYTHON_LINT_PATHS:="./**/*.py" .PHONY: install -install: pnpm-install pnpm-install-chromium pip-install +install: pip-install install-chromium -.PHONY: pnpm-install -pnpm-install: - cd webUI && pnpm install - -.PHONY: pnpm-install-chromium -pnpm-install-chromium: - cd webUI && pnpm exec playwright install chromium +.PHONY: install-chromium +install-chromium: + playwright install chromium .PHONY: pip-install pip-install: - python3.10 -m pip install -r requirements.txt + python3 -m pip install -r requirements.txt .PHONY: python-lint python-lint: black --check --diff . - python3.10 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS) + python3 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS) .PHONY: python-lint-fix python-lint-fix: black . - python3.10 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS) + python3 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS) .PHONY: gherkin-lint gherkin-lint: diff --git a/test/gui/behave.ini b/test/gui/behave.ini index 867a69bf4d..dc35d58323 100644 --- a/test/gui/behave.ini +++ b/test/gui/behave.ini @@ -1,8 +1,9 @@ [behave] paths=features default_format = plain -default_tags = not (@skip) +default_tags = not @skip logging_level = WARNING capture_stderr = false capture_stdout = false -capture_log = false \ No newline at end of file +capture_log = false +show_skipped = false \ No newline at end of file diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index 45392fb8b8..de47d281d9 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -17,7 +17,6 @@ from helpers.UserHelper import get_displayname_for_user from helpers.api import provisioning - app_driver = None @@ -123,7 +122,7 @@ def start_client(): ) options.set_capability('appium:environ', get_app_env()) app_driver = webdriver.Remote( - command_executor='http://127.0.0.1:4723', options=options + command_executor='http://localhost:4723', options=options ) app_driver.implicitly_wait = 10 @@ -281,7 +280,7 @@ def close_and_kill_app(): # Quit Appium session if app_driver is not None: app_driver.quit() - + # Kill remaining process by exe path app_path = get_config("app_path") for process in psutil.process_iter(['pid', 'exe']): @@ -289,6 +288,6 @@ def close_and_kill_app(): print("Closing desktop client...") psutil.Process(process.info['pid']).kill() break - + # Reset driver for reuse app_driver = None diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index d1c1460561..f77e2aefb2 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -8,7 +8,6 @@ from helpers.ConfigHelper import get_config, is_linux, is_windows from helpers.FilesHelper import sanitize_path - if is_windows(): from helpers.WinPipeHelper import WinPipeConnect as SocketConnect else: @@ -22,9 +21,20 @@ if not os.path.exists(syncstate_lib_file): urllib.request.urlretrieve( 'https://raw.githubusercontent.com/opencloud-eu/desktop-shell-integration-nautilus/refs/heads/main/src/syncstate.py', - os.path.join(custom_lib, 'syncstate.py'), + syncstate_lib_file, + ) + # do not instantiate SocketConnect in the script. + with open(syncstate_lib_file, 'r') as f: + content = f.read() + content = content.replace('socketConnect = SocketConnect()', '') + content = content.replace( + 'from gi.repository import GObject, Nautilus', + 'import gi\n\ngi.require_version(\'Nautilus\', \'4.0\')\nfrom gi.repository import GObject, Nautilus', ) + with open(syncstate_lib_file, 'w') as f: + f.write(content) + # the script needs to use the system-wide python # to switch from the built-in interpreter # see https://kb.froglogic.com/squish/howto/using-external-python-interpreter-squish-6-6/ @@ -176,7 +186,7 @@ def generate_sync_pattern_from_messages(messages): # excludes ":/tmp/client-bdd/Alice/" # adds only "STATUS:OK" to the pattern list if match := re.search(r':(/|[A-Za-z]:[\\/]).*', message): - (end, _) = match.span() + end, _ = match.span() # shared resources will have status like "STATUS:OK+SWM" status = message[:end].replace('+SWM', '') pattern.append(status) @@ -360,6 +370,7 @@ def make_available_locally(resource_path): def wait_for(condition, timeout, interval=0.5): from helpers.SetupClientHelper import app + wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval) try: wait.until(lambda _: condition()) diff --git a/test/gui/woodpecker/run_atspi_webdriver.sh b/test/gui/woodpecker/run_atspi_webdriver.sh new file mode 100644 index 0000000000..a9cec89fa9 --- /dev/null +++ b/test/gui/woodpecker/run_atspi_webdriver.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -e + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../ && pwd)" +WEBDRIVER_DIR="$TEST_DIR/__webdriver" + +mkdir -p "$WEBDRIVER_DIR" + +DRIVER_FILE="atspi-webdriver.py" +DRIVER_URL="https://raw.githubusercontent.com/KDE/selenium-webdriver-at-spi" + +# shellcheck disable=SC1091 +. "$TEST_DIR/.woodpecker.env" + +if [ -z "$ATSPI_WEBDRIVER_VERSION" ]; then + ATSPI_WEBDRIVER_VERSION="master" +fi + +if [ ! -f "$WEBDRIVER_DIR/$DRIVER_FILE" ]; then + curl -sSL --fail "$DRIVER_URL/$ATSPI_WEBDRIVER_VERSION/selenium-webdriver-at-spi.py" -o "$WEBDRIVER_DIR/$DRIVER_FILE" +fi + +if [ ! -f "$WEBDRIVER_DIR/app_roles.py" ]; then + curl -sSL --fail "$DRIVER_URL/$ATSPI_WEBDRIVER_VERSION/app_roles.py" -o "$WEBDRIVER_DIR/app_roles.py" +fi + +if [ -z "$WEBDRIVER_HOST" ]; then + WEBDRIVER_HOST="0.0.0.0" +fi +if [ -z "$WEBDRIVER_PORT" ]; then + WEBDRIVER_PORT="4723" +fi + +# run webdriver server +export FLASK_ENV=production +export FLASK_APP="$WEBDRIVER_DIR/$DRIVER_FILE" +flask run --host="$WEBDRIVER_HOST" --port="$WEBDRIVER_PORT" --no-reload \ No newline at end of file diff --git a/test/gui/woodpecker/script.sh b/test/gui/woodpecker/script.sh index d983f6c539..821f21f738 100644 --- a/test/gui/woodpecker/script.sh +++ b/test/gui/woodpecker/script.sh @@ -2,16 +2,18 @@ touch .woodpecker.env -# get playwright version from package.json +PY_REQUIREMENTS_PATH="test/gui/requirements.txt" + +# get playwright version from requirements.txt get_playwright_version() { - PACKAGE_JSON_PATH="test/gui/webUI/package.json" - if [[ ! -f "$PACKAGE_JSON_PATH" ]]; then - echo "Error: package.json file not found." + if [[ ! -f "$PY_REQUIREMENTS_PATH" ]]; then + echo "Error: file not found: $PY_REQUIREMENTS_PATH" fi - playwright_version=$(grep '"@playwright/test":' "$PACKAGE_JSON_PATH" | cut -d':' -f2 | tr -d '", ') + playwright_version=$(grep 'playwright==' "$PY_REQUIREMENTS_PATH" | cut -d'=' -f3 | cut -d'.' -f1-2) + playwright_version=${playwright_version//[^0-9.]/} if [[ -z "$playwright_version" ]]; then - echo "Error: Playwright package not found in package.json." >&2 + echo "Error: Playwright package not found in requirements.txt" >&2 exit 78 fi @@ -20,30 +22,35 @@ get_playwright_version() { # Function to check if the cache exists for the given commit ID check_browsers_cache() { - get_playwright_version + playwright_version=$(get_playwright_version) - playwright_cache=$(mc find s3/$CACHE_BUCKET/web/browsers-cache/$playwright_version/playwright-browsers.tar.gz 2>&1 | grep 'Object does not exist') + playwright_cache=$(mc find s3/$CACHE_BUCKET/desktop/browsers-cache/$playwright_version/playwright-browsers.tar.gz 2>&1 | grep 'Object does not exist') if [[ "$playwright_cache" != "" ]] then - echo "Playwright v$playwright_version supported browsers doesn't exist in cache." + echo "Browsers cache for playwright v$playwright_version not found in cache." ENV="BROWSER_CACHE_FOUND=false\n" else - echo "Playwright v$playwright_version supported browsers found in cache." + echo "Browsers cache for playwright v$playwright_version found in cache." ENV="BROWSER_CACHE_FOUND=true\n" fi } +get_requirementstxt_hash() { + requirements_sha=$(sha1sum $PY_REQUIREMENTS_PATH | cut -d" " -f1) + echo "$requirements_sha" +} + check_python_cache() { - requirements_sha=$(sha1sum test/gui/requirements.txt | cut -d" " -f1) - python_cache=$(mc find s3/$CACHE_BUCKET/desktop/python-cache/python-cache-$requirements_sha.tar.gz 2>&1 | grep 'Object does not exist') + requirements_sha=$(get_requirementstxt_hash) + python_cache=$(mc find s3/$CACHE_BUCKET/desktop/python-cache/$requirements_sha/python-cache.tar.gz 2>&1 | grep 'Object does not exist') if [[ "$python_cache" != "" ]] then - echo "Python cache of requirements with hash $requirements_sha doesn't exist in cache." + echo "Python cache for '$requirements_sha' hash not found in cache." ENV="PYTHON_CACHE_FOUND=false\n" else - echo "Python cache of requirements with hash $requirements_sha found in cache." + echo "Python cache for '$requirements_sha' hash found in cache." ENV="PYTHON_CACHE_FOUND=true\n" fi } @@ -51,7 +58,8 @@ check_python_cache() { if [[ "$1" == "" ]]; then echo "Usage: $0 [COMMAND]" echo "Commands:" - echo -e " get_playwright_version \t get the playwright version from package.json" + echo -e " get_playwright_version \t get the playwright version from requirements.txt" + echo -e " get_requirementstxt_hash \t get the hash of the current requirements.txt" echo -e " check_browsers_cache \t check if the browsers cache exists for the given playwright version" echo -e " check_python_cache \t check if a cache for the current requirements.txt exists" exit 1 From c24adc3cab24462489986b30948c91b30db4a807 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Thu, 7 May 2026 16:19:27 +0545 Subject: [PATCH 42/75] test: remove obsolete test files (#894) * test: remove obsolete files Signed-off-by: Saw-jan * test: move release test plan to gui dir Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- test/gui/envs.txt | 1 - test/gui/helpers/ConfigHelper.py | 26 +- .../images/overlay-icons.png | Bin .../release-test-plan}/testplan.md | 0 test/gui/shared/scripts/bdd_hooks.py | 268 ------------------ test/gui/shared/scripts/names.py | 80 ------ ...icLinkExpirationProgressIndicatorInvisible | 93 ------ ...blicLinkPasswordProgressIndicatorInvisible | 93 ------ test/gui/suite.conf | 8 - test/gui/webUI/.editorconfig | 11 - test/gui/webUI/.gitignore | 8 - test/gui/webUI/login.spec.js | 41 --- test/gui/webUI/package.json | 11 - test/gui/webUI/playwright.config.js | 23 -- test/gui/webUI/pnpm-lock.yaml | 44 --- 15 files changed, 4 insertions(+), 703 deletions(-) delete mode 100644 test/gui/envs.txt rename test/{manual/test_plan => gui/release-test-plan}/images/overlay-icons.png (100%) rename test/{manual/test_plan => gui/release-test-plan}/testplan.md (100%) delete mode 100644 test/gui/shared/scripts/bdd_hooks.py delete mode 100644 test/gui/shared/scripts/names.py delete mode 100644 test/gui/shared/verificationPoints/publicLinkExpirationProgressIndicatorInvisible delete mode 100644 test/gui/shared/verificationPoints/publicLinkPasswordProgressIndicatorInvisible delete mode 100644 test/gui/suite.conf delete mode 100644 test/gui/webUI/.editorconfig delete mode 100644 test/gui/webUI/.gitignore delete mode 100644 test/gui/webUI/login.spec.js delete mode 100644 test/gui/webUI/package.json delete mode 100644 test/gui/webUI/playwright.config.js delete mode 100644 test/gui/webUI/pnpm-lock.yaml diff --git a/test/gui/envs.txt b/test/gui/envs.txt deleted file mode 100644 index 1ba398efa8..0000000000 --- a/test/gui/envs.txt +++ /dev/null @@ -1 +0,0 @@ -XDG_CONFIG_HOME=/tmp/opencloudtest/.config diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index e6cd69c90b..6168d07867 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -9,27 +9,6 @@ CURRENT_DIR = Path(__file__).resolve().parent APP_CONFIG_FILE = "opencloud.cfg" -def read_env_file(): - envs = {} - script_path = os.path.dirname(os.path.realpath(__file__)) - env_path = os.path.abspath(os.path.join(script_path, '..', 'envs.txt')) - with open(env_path, 'rt', encoding='UTF-8') as f: - for line in f: - if not line.strip(): - continue - if line.startswith('#'): - continue - key, value = line.split('=', 1) - envs[key] = value.strip() - return envs - - -def get_config_from_env_file(env): - envs = read_env_file() - if env in envs: - return envs[env] - raise KeyError(f'Environment "{env}" not found in envs.txt') - def is_windows(): return platform.system() == 'Windows' @@ -54,7 +33,9 @@ def get_config_home_linux(): def get_config_home_win(): - return os.path.join(get_win_user_home(), 'AppData', 'Local', 'Temp', 'opencloudtest', '.config') + return os.path.join( + get_win_user_home(), 'AppData', 'Local', 'Temp', 'opencloudtest', '.config' + ) def get_config_home(): @@ -75,6 +56,7 @@ def get_app_env(): 'APPDATA': get_config_home(), } + # map environment variables to config keys CONFIG_ENV_MAP = { 'app_path': 'APP_PATH', diff --git a/test/manual/test_plan/images/overlay-icons.png b/test/gui/release-test-plan/images/overlay-icons.png similarity index 100% rename from test/manual/test_plan/images/overlay-icons.png rename to test/gui/release-test-plan/images/overlay-icons.png diff --git a/test/manual/test_plan/testplan.md b/test/gui/release-test-plan/testplan.md similarity index 100% rename from test/manual/test_plan/testplan.md rename to test/gui/release-test-plan/testplan.md diff --git a/test/gui/shared/scripts/bdd_hooks.py b/test/gui/shared/scripts/bdd_hooks.py deleted file mode 100644 index 5d5c1ee7ee..0000000000 --- a/test/gui/shared/scripts/bdd_hooks.py +++ /dev/null @@ -1,268 +0,0 @@ -# -*- coding: utf-8 -*- - -# This file contains hook functions to run as the .feature file is executed. -# -# A common use-case is to use the OnScenarioStart/OnScenarioEnd hooks to -# start and stop an AUT, e.g. -# -# @OnScenarioStart -# def hook(context): -# startApplication("addressbook") -# -# @OnScenarioEnd -# def hook(context): -# currentApplicationContext().detach() -# -# See the section 'Performing Actions During Test Execution Via Hooks' in the Squish -# manual for a complete reference of the available API. -import shutil -import os -from datetime import datetime -from types import SimpleNamespace - -from helpers.StacktraceHelper import get_core_dumps, generate_stacktrace -from helpers.SyncHelper import close_socket_connection, clear_waited_after_sync -from helpers.SpaceHelper import delete_project_spaces -from helpers.api.provisioning import delete_created_users -from helpers.SetupClientHelper import wait_until_app_killed, unlock_keyring -from helpers.ConfigHelper import ( - init_config, - get_config, - set_config, - clear_scenario_config, - is_windows, - is_linux, -) -from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths -from helpers.ReportHelper import save_video_recording, take_screenshot, is_video_enabled - -from pageObjects.Toolbar import Toolbar -from pageObjects.AccountSetting import AccountSetting -from pageObjects.AccountConnectionWizard import AccountConnectionWizard -import names - -# Squish test settings: -# This controls whether a test (scenario) should stop execution on failure or not -# If set to True, the scenario will stop on the first step failure and remaining steps will not be executed -# If set to False, the scenario will continue to execute all steps and report all failures at the end -testSettings.throwOnFailure = True - -# this will reset in every test suite -PREVIOUS_FAIL_RESULT_COUNT = 0 -PREVIOUS_ERROR_RESULT_COUNT = 0 - - -# runs before a feature -# Order: 1 -@OnFeatureStart -def hook(context): - init_config() - - -# runs before every scenario -# Order: 1 -@OnScenarioStart -def hook(context): - unlock_keyring() - clear_scenario_config() - - -# runs before every scenario -# Order: 2 -@OnScenarioStart -def hook(context): - # set opencloud config file path - config_dir = get_config("clientConfigDir") - if os.path.exists(config_dir): - if len(os.listdir(config_dir)) and is_windows(): - raise FileExistsError( - "Looks like you have previous client config in '" - + config_dir - + "'\n[DANGER] Delete it and try again." - + "\n[DANGER] Removing config file will make client to lost the previously added accounts." - ) - # clean previous configs - shutil.rmtree(config_dir) - os.makedirs(config_dir, 0o0755) - - # create reports dir if not exists - test_report_dir = get_config("guiTestReportDir") - if not os.path.exists(test_report_dir): - os.makedirs(test_report_dir) - - # log tests scenario title on serverlog file - if os.getenv("CI"): - with open(test_report_dir + "/serverlog.log", "a", encoding="utf-8") as f: - f.write( - str((datetime.now()).strftime("%H:%M:%S:%f")) - + "\tBDD Scenario: " - + context.title - + "\n" - ) - - # this path will be changed according to the user added to the client - # e.g.: /tmp/client-bdd/Alice - set_config("currentUserSyncPath", "") - - root_sync_dir = get_config("clientRootSyncPath") - if not os.path.exists(root_sync_dir): - os.makedirs(root_sync_dir) - - tmp_dir = get_config("tempFolderPath") - if not os.path.exists(tmp_dir): - os.makedirs(tmp_dir) - - -# determines if the test scenario failed or not -# Currently, this workaround is needed because we cannot find out -# a way to determine the pass/fail status of currently running test scenario. -# And, resultCount("errors") and resultCount("fails") -# return the total number of error/failed test scenarios of a test suite. -def scenario_failed(): - return ( - test.resultCount("fails") - PREVIOUS_FAIL_RESULT_COUNT > 0 - or test.resultCount("errors") - PREVIOUS_ERROR_RESULT_COUNT > 0 - ) - - -def scenario_title_to_filename(title): - # scenario name can have "/" which is invalid filename - return title.replace(" ", "_").replace("/", "_").strip(".") - - -# runs after every scenario -# Order: 1 -# server cleanup -@OnScenarioEnd -def hook(context): - delete_project_spaces() - delete_created_users() - - -# runs after every scenario -# Order: 2 -@OnScenarioEnd -def hook(context): - clear_waited_after_sync() - close_socket_connection() - - # generate screenshot and video reports - if is_linux(): - filename = scenario_title_to_filename(context.title) - if scenario_failed(): - take_screenshot(f"{filename}.png") - - if is_video_enabled(): - save_video_recording(f"{filename}.mp4", scenario_failed()) - - # teardown accounts and configs - teardown_client() - - # search coredumps after every test scenario - # CI pipeline might fail although all tests are passing - if coredumps := get_core_dumps(): - try: - generate_stacktrace(context.title, coredumps) - test.log("Stacktrace generated!") - except OSError as err: - test.log("Exception occured:" + str(err)) - elif scenario_failed(): - test.log("No coredump found!") - - global PREVIOUS_FAIL_RESULT_COUNT, PREVIOUS_ERROR_RESULT_COUNT - PREVIOUS_FAIL_RESULT_COUNT = test.resultCount("fails") - PREVIOUS_ERROR_RESULT_COUNT = test.resultCount("errors") - - -def get_active_widget(): - dialog_widgets = object.children(squish.waitForObject(AccountSetting.DIALOG_STACK, get_config('minSyncTimeout') * 100)) - for child_widget in dialog_widgets: - if hasattr(child_widget, "objectName") and child_widget.objectName and child_widget.objectName != "page": - return child_widget - - # return empty object if not found - return SimpleNamespace(objectName="") - - -def teardown_client(): - # Cleanup user accounts from UI for Windows platform - # It is not needed for Linux so skipping it in order to save CI time - if is_windows(): - # remove account from UI - # In Windows, removing only config and sync folders won't help - # so to work around that, remove the account connection - close_dialogs() - close_widgets() - active_widget = get_active_widget() - if active_widget.objectName and active_widget.objectName != names.setupWizardWindow_OCC_Wizard_SetupWizardWindow["name"]: - accounts, selectors = Toolbar.get_accounts() - for display_name in selectors: - _, account_objects = Toolbar.get_accounts() - squish.mouseClick(squish.waitForObject(account_objects[display_name])) - AccountSetting.remove_account_connection() - - # re-fetch accounts after removing from UI - accounts, _ = Toolbar.get_accounts() - if accounts: - squish.waitForObject(AccountConnectionWizard.SERVER_ADDRESS_BOX) - - # Detach (i.e. potentially terminate) all AUTs at the end of a scenario - for ctx in squish.applicationContextList(): - # get pid before detaching - pid = ctx.pid - ctx.detach() - wait_until_app_killed(pid) - - # clean up config files - shutil.rmtree(get_config("clientConfigDir")) - - # delete test files/folders - for entry in os.scandir(get_config("clientRootSyncPath")): - try: - if entry.is_file() or entry.is_symlink(): - test.log("Deleting file: " + entry.name) - os.unlink(prefix_path_namespace(entry.path)) - elif entry.is_dir(): - test.log("Deleting folder: " + entry.name) - shutil.rmtree(prefix_path_namespace(entry.path)) - except OSError as e: - test.log(f"Failed to delete '{entry.name}'.\nReason: {e}.") - # cleanup paths created outside of the temporary directory during the test - cleanup_created_paths() - - -def close_dialogs(): - # close the current active dailog if it's not a main client window - while True: - active_window = QApplication.activeModalWidget() - if str(active_window) == "": - break - test.log(f"Closing '{active_window.objectName}' window") - if not active_window.close(): - confirm_dialog = QApplication.activeModalWidget() - if confirm_dialog.visible: - squish.clickButton( - squish.waitForObject(AccountSetting.CONFIRMATION_YES_BUTTON) - ) - - -def close_widgets(): - try: - ch = object.children(squish.waitForObject(AccountSetting.DIALOG_STACK, 500)) - for obj in ch: - if ( - hasattr(obj, "objectName") - and obj.objectName - and obj.objectName != "page" - ): - obj.close() - # if the dialog has a confirmation dialog, confirm it - confirm_dialog = QApplication.activeModalWidget() - if str(confirm_dialog) != "" and confirm_dialog.visible: - squish.clickButton( - squish.waitForObject(AccountSetting.CONFIRMATION_YES_BUTTON) - ) - except LookupError: - # nothing to close if DIALOG_STACK is not found - # required for client versions <= 5 - pass diff --git a/test/gui/shared/scripts/names.py b/test/gui/shared/scripts/names.py deleted file mode 100644 index b2f2691792..0000000000 --- a/test/gui/shared/scripts/names.py +++ /dev/null @@ -1,80 +0,0 @@ -# encoding: UTF-8 -# fmt: off - -from objectmaphelper import * - -settings_OCC_SettingsDialog = {"name": "Settings", "type": "OCC::SettingsDialog", "visible": 1} -opencloudWizard_OCC_OpencloudWizard = {"name": "opencloudWizard", "type": "OCC::OpencloudWizard", "visible": 1} -qFileDialog_QFileDialog = {"name": "QFileDialog", "type": "QFileDialog", "visible": 1} -settings_stack_QStackedWidget = {"name": "stack", "type": "QStackedWidget", "visible": 1, "window": settings_OCC_SettingsDialog} -settings_dialogStack_QStackedWidget = {"name": "dialogStack", "type": "QStackedWidget", "visible": 1, "window": settings_OCC_SettingsDialog} -qFileDialog_fileNameLabel_QLabel = {"name": "fileNameLabel", "type": "QLabel", "visible": 1, "window": qFileDialog_QFileDialog} -sharingDialog_OCC_ShareDialog = {"name": "SharingDialog", "type": "OCC::ShareDialog", "visible": 1} -sharingDialog_qt_tabwidget_stackedwidget_QStackedWidget = {"name": "qt_tabwidget_stackedwidget", "type": "QStackedWidget", "visible": 1, "window": sharingDialog_OCC_ShareDialog} -qt_tabwidget_stackedwidget_SharingDialogUG_OCC_ShareUserGroupWidget = {"container": sharingDialog_qt_tabwidget_stackedwidget_QStackedWidget, "name": "SharingDialogUG", "type": "OCC::ShareUserGroupWidget", "visible": 1} -sharingDialogUG_scrollArea_QScrollArea = {"container": qt_tabwidget_stackedwidget_SharingDialogUG_OCC_ShareUserGroupWidget, "name": "scrollArea", "type": "QScrollArea", "visible": 1} -settings_settingsdialog_toolbutton_Quit_OpenCloud_QToolButton = {"name": "settingsdialog_toolbutton_Quit openCloud", "type": "QToolButton", "visible": 1, "window": settings_OCC_SettingsDialog} -settings_settingsdialog_toolbutton_Settings_QToolButton = {"name": "settingsdialog_toolbutton_Settings", "type": "QToolButton", "visible": 1, "window": settings_OCC_SettingsDialog} -stack_qt_tabwidget_stackedwidget_QStackedWidget = {"container": settings_stack_QStackedWidget, "name": "qt_tabwidget_stackedwidget", "type": "QStackedWidget", "visible": 1} -qt_tabwidget_stackedwidget_OCC_IssuesWidget_OCC_IssuesWidget = {"container": stack_qt_tabwidget_stackedwidget_QStackedWidget, "name": "OCC__IssuesWidget", "type": "OCC::IssuesWidget", "visible": 1} -sharingDialog_qt_tabwidget_tabbar_QTabBar = {"name": "qt_tabwidget_tabbar", "type": "QTabBar", "visible": 1, "window": sharingDialog_OCC_ShareDialog} -qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget = {"container": sharingDialog_qt_tabwidget_stackedwidget_QStackedWidget, "name": "OCC__ShareLinkWidget", "type": "OCC::ShareLinkWidget", "visible": 1} -oCC_ShareLinkWidget_checkBox_password_QCheckBox = {"container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "name": "checkBox_password", "type": "QCheckBox", "visible": 1} -oCC_ShareLinkWidget_widget_editing_QWidget = {"container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "name": "widget_editing", "type": "QWidget", "visible": 1} -oCC_ShareLinkWidget_checkBox_password_QProgressIndicator = {"aboveWidget": oCC_ShareLinkWidget_widget_editing_QWidget, "container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "leftWidget": oCC_ShareLinkWidget_checkBox_password_QCheckBox, "type": "QProgressIndicator", "unnamed": 1, "visible": 1} -oCC_ShareLinkWidget_linkShares_QTableWidget = {"container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "name": "linkShares", "type": "QTableWidget", "visible": 1} -oCC_ShareLinkWidget_lineEdit_password_QLineEdit = {"container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "name": "lineEdit_password", "type": "QLineEdit", "visible": 1} -oCC_ShareLinkWidget_checkBox_expire_QCheckBox = {"container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "name": "checkBox_expire", "type": "QCheckBox", "visible": 1} -oCC_ShareLinkWidget_checkBox_expire_QProgressIndicator = {"aboveWidget": oCC_ShareLinkWidget_lineEdit_password_QLineEdit, "container": qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget, "leftWidget": oCC_ShareLinkWidget_checkBox_expire_QCheckBox, "type": "QProgressIndicator", "unnamed": 1, "visible": 1} -settings_settingsdialog_toolbutton_Add_account_QToolButton = {"name": "settingsdialog_toolbutton_Add account", "type": "QToolButton", "visible": 1, "window": settings_OCC_SettingsDialog} -settings_settingsdialog_toolbutton_Activity_QToolButton = {"name": "settingsdialog_toolbutton_Activity", "type": "QToolButton", "visible": 1, "window": settings_OCC_SettingsDialog} -opencloudWizard_urlLabel_QLabel = {"name": "urlLabel", "type": "QLabel", "visible": 1, "window": opencloudWizard_OCC_OpencloudWizard} -setupWizardWindow_OCC_Wizard_SetupWizardWindow = {"name": "SetupWizardWidget", "type": "OCC::Wizard::SetupWizardWidget", "visible": 1} -setupWizardWindow_contentWidget_QStackedWidget = {"name": "contentWidget", "type": "QStackedWidget", "visible": 1, "window": setupWizardWindow_OCC_Wizard_SetupWizardWindow} -insecure_connection_QMessageBox = {"type": "QMessageBox", "unnamed": 1, "visible": 1, "windowTitle": "Insecure connection"} -contentWidget_advancedConfigGroupBox_QGroupBox = {"container": setupWizardWindow_contentWidget_QStackedWidget, "name": "advancedConfigGroupBox", "type": "QGroupBox", "visible": 1} -advancedConfigGroupBox_localDirectoryGroupBox_QGroupBox = {"container": contentWidget_advancedConfigGroupBox_QGroupBox, "name": "localDirectoryGroupBox", "type": "QGroupBox", "visible": 1} -advancedConfigGroupBox_syncModeGroupBox_QGroupBox = {"container": contentWidget_advancedConfigGroupBox_QGroupBox, "name": "syncModeGroupBox", "type": "QGroupBox", "visible": 1} -add_Folder_Sync_Connection_OCC_FolderWizard = {"type": "OCC::FolderWizard", "unnamed": 1, "visible": 1, "windowTitle": "Add Folder Sync Connection"} -add_Folder_Sync_Connection_groupBox_QGroupBox = {"name": "groupBox", "type": "QGroupBox", "visible": 1, "window": add_Folder_Sync_Connection_OCC_FolderWizard} -loginRequiredDialog_OCC_LoginRequiredDialog = {"name": "LoginRequiredDialog", "type": "OCC::LoginRequiredDialog", "visible": 1} -loginRequiredDialog_contentWidget_QStackedWidget = {"name": "contentWidget", "type": "QStackedWidget", "visible": 1, "window": loginRequiredDialog_OCC_LoginRequiredDialog} -contentWidget_contentWidget_QStackedWidget = {"container": setupWizardWindow_contentWidget_QStackedWidget, "name": "contentWidget", "type": "QStackedWidget", "visible": 1} -add_Folder_Sync_Connection_tableView_QTableView = {"name": "tableView","type": "QTableView","visible": 1,"window": add_Folder_Sync_Connection_OCC_FolderWizard} -stack_scrollArea_QScrollArea = {"container": settings_stack_QStackedWidget, "name": "scrollArea", "type": "QScrollArea", "visible": 1} -stack_stackedWidget_QStackedWidget = {"container": settings_stack_QStackedWidget, "name": "stackedWidget", "type": "QStackedWidget", "visible": 1} -stackedWidget_quickWidget_QQuickWidget = {"container": stack_stackedWidget_QStackedWidget, "name": "quickWidget", "type": "QQuickWidget", "visible": 1} -quickWidget_scrollView_ScrollView = {"container": stackedWidget_quickWidget_QQuickWidget, "id": "scrollView", "type": "ScrollView", "unnamed": 1, "visible": True} -scrollView_ListView = {"container": quickWidget_scrollView_ScrollView, "type": "ListView", "unnamed": 1, "visible": True} -dialogStack_quickWidget_QQuickWidget = {"container": settings_dialogStack_QStackedWidget, "name": "quickWidget", "type": "QQuickWidget", "visible": 1} -create_Remote_Folder_QInputDialog = {"type": "QInputDialog", "unnamed": 1, "visible": 1, "windowTitle": "Create Remote Folder"} -create_Remote_Folder_Enter_the_name_of_the_new_folder_to_be_created_below_QLabel = {"text": "Enter the name of the new folder to be created below '/':", "type": "QLabel", "unnamed": 1, "visible": 1, "window": create_Remote_Folder_QInputDialog} -groupBox_folderTreeWidget_QTreeWidget = {"container": add_Folder_Sync_Connection_groupBox_QGroupBox, "name": "folderTreeWidget", "type": "QTreeWidget", "visible": 1} -confirm_Folder_Sync_Connection_Removal_QMessageBox = {"type": "QMessageBox", "unnamed": 1, "visible": 1, "windowTitle": "Confirm Folder Sync Connection Removal"} -stackedWidget_quickWidget_OCC_QmlUtils_OCQuickWidget = {"container": stack_stackedWidget_QStackedWidget, "name": "quickWidget", "type": "OCC::QmlUtils::OCQuickWidget", "visible": 1} -qt_tabwidget_stackedwidget_OCC_ProtocolWidget_OCC_ProtocolWidget = {"container": stack_qt_tabwidget_stackedwidget_QStackedWidget, "name": "OCC__ProtocolWidget", "type": "OCC::ProtocolWidget", "visible": 1} -oCC_ProtocolWidget_tableView_QTableView = {"container": qt_tabwidget_stackedwidget_OCC_ProtocolWidget_OCC_ProtocolWidget, "name": "_tableView", "type": "QTableView", "visible": 1} -oCC_IssuesWidget_tableView_QTableView = {"container": qt_tabwidget_stackedwidget_OCC_IssuesWidget_OCC_IssuesWidget, "name": "_tableView", "type": "QTableView", "visible": 1} -dialogStack_quickWidget_OCC_QmlUtils_OCQuickWidget = {"container": settings_dialogStack_QStackedWidget, "name": "quickWidget", "type": "OCC::QmlUtils::OCQuickWidget", "visible": 1} -contentWidget_OCC_QmlUtils_OCQuickWidget = {"container": contentWidget_contentWidget_QStackedWidget, "type": "OCC::QmlUtils::OCQuickWidget", "unnamed": 1, "visible": 1} -stackedWidget_Add_Folder_Sync_Connection_QGroupBox = {"container": stack_stackedWidget_QStackedWidget, "title": "Add Folder Sync Connection", "type": "QGroupBox", "unnamed": 1, "visible": 1} -stackedWidget_groupBox_QGroupBox = {"container": settings_stack_QStackedWidget, "name": "groupBox", "type": "QGroupBox", "visible": 1} -groupBox_OCC_QmlUtils_OCQuickWidget = {"container": stackedWidget_groupBox_QGroupBox, "type": "OCC::QmlUtils::OCQuickWidget", "unnamed": 1, "visible": 1} -quickWidget_Overlay = {"container": stackedWidget_quickWidget_OCC_QmlUtils_OCQuickWidget, "type": "Overlay", "unnamed": 1, "visible": True} -scrollView_moreButton_Image = {"container": quickWidget_scrollView_ScrollView, "id": "moreButton", "source": "image://opencloud?theme=fontawesome&icon=&enabled=true&size=undefined", "type": "Image", "unnamed": 1, "visible": True} -pause_sync_MenuItem = {"checkable": False, "container": quickWidget_Overlay, "enabled": True, "text": "Pause sync", "type": "MenuItem", "unnamed": 1, "visible": True} -quit_OpenCloud_Desktop_QMessageBox = {"type": "QMessageBox", "unnamed": 1, "visible": 1, "windowTitle": "Quit OpenCloud Desktop"} -stackedWidget_Add_Space_QGroupBox = {"container": stack_stackedWidget_QStackedWidget, "title": "Add Space", "type": "QGroupBox", "unnamed": 1, "visible": 1} -add_Space_label_QLabel = {"container": stackedWidget_Add_Space_QGroupBox, "name": "label", "type": "QLabel", "visible": 1} -add_Space_qt_passive_wizardbutton0_QPushButton = {"container": stackedWidget_Add_Space_QGroupBox, "name": "__qt__passive_wizardbutton0", "type": "QPushButton", "visible": 1} -add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QLabel = {"container": stackedWidget_Add_Space_QGroupBox, "text": "Deselect remote folders you do not wish to synchronize.", "type": "QLabel", "unnamed": 1, "visible": 1} -add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget = {"aboveWidget": add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QLabel, "container": stackedWidget_Add_Space_QGroupBox, "type": "QTreeWidget", "unnamed": 1, "visible": 1} -add_Space_qt_passive_wizardbutton1_QPushButton = {"container": stackedWidget_Add_Space_QGroupBox, "name": "__qt__passive_wizardbutton1", "type": "QPushButton", "visible": 1} -remove_Space_MenuItem = {"checkable": False, "container": quickWidget_Overlay, "enabled": True, "text": "Remove Space", "type": "MenuItem", "unnamed": 1, "visible": True} -confirm_removal_of_Space_QMessageBox = {"type": "QMessageBox", "unnamed": 1, "visible": 1, "windowTitle": "Confirm removal of Space"} -deselect_remote_folders_you_do_not_wish_to_synchronize_OpenCloud_QModelIndex = {"column": 0, "container": add_Space_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget, "text": "Personal", "type": "QModelIndex"} -folderError_Container = {"container": quickWidget_scrollView_ScrollView, "type": "FolderError"} -groupBox_Deselect_remote_folders_you_do_not_wish_to_synchronize_QLabel = {"container": stackedWidget_groupBox_QGroupBox, "text": "Deselect remote folders you do not wish to synchronize.", "type": "QLabel", "unnamed": 1, "visible": 1} -groupBox_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget = {"aboveWidget": groupBox_Deselect_remote_folders_you_do_not_wish_to_synchronize_QLabel, "container": stackedWidget_groupBox_QGroupBox, "type": "QTreeWidget", "unnamed": 1, "visible": 1} -deselect_remote_folders_you_do_not_wish_to_synchronize_Personal_QModelIndex = {"column": 0, "container": groupBox_Deselect_remote_folders_you_do_not_wish_to_synchronize_QTreeWidget, "text": "Personal", "type": "QModelIndex"} -stackedWidget_OK_QPushButton = {"container": stack_stackedWidget_QStackedWidget, "text": "OK", "type": "QPushButton", "unnamed": 1, "visible": 1} diff --git a/test/gui/shared/verificationPoints/publicLinkExpirationProgressIndicatorInvisible b/test/gui/shared/verificationPoints/publicLinkExpirationProgressIndicatorInvisible deleted file mode 100644 index cd9a1357e4..0000000000 --- a/test/gui/shared/verificationPoints/publicLinkExpirationProgressIndicatorInvisible +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAIklEQVQ4jWN8//79fwYqAiZqGjZq4KiBowaOGjhq4FAyEACDpgP0OKH6SQAAAABJRU5ErkJggg== - - QWidget - QObject - - - - - 528 - 604 - 20 - 20 - - - - - true - - - false - - - 0 - - - false - - - false - - - false - - - false - - - 40 - - - - - - true - - - 1 - - - -1 - - - - - false - - - - 0 - - - 0 - - - false - - - - - - - - - - - - - - - - - - - diff --git a/test/gui/shared/verificationPoints/publicLinkPasswordProgressIndicatorInvisible b/test/gui/shared/verificationPoints/publicLinkPasswordProgressIndicatorInvisible deleted file mode 100644 index 27f93dad69..0000000000 --- a/test/gui/shared/verificationPoints/publicLinkPasswordProgressIndicatorInvisible +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAIklEQVQ4jWN8//79fwYqAiZqGjZq4KiBowaOGjhq4FAyEACDpgP0OKH6SQAAAABJRU5ErkJggg== - - QWidget - QObject - - - - - 2739 - 457 - 20 - 20 - - - - - false - - - false - - - - 40 - - - -1 - - - false - - - - 0 - - - - 1 - - - true - - - 0 - - - true - - - - - - - 0 - - - false - - - false - - - false - - - false - - - - - - - - - - - - - - - - - - diff --git a/test/gui/suite.conf b/test/gui/suite.conf deleted file mode 100644 index 16dbe6dc4d..0000000000 --- a/test/gui/suite.conf +++ /dev/null @@ -1,8 +0,0 @@ -AUT=opencloud --showsettings -ENVVARS=envs.txt -HOOK_SUB_PROCESSES=false -IMPLICITAUTSTART=false -LANGUAGE=Python -OBJECTMAPSTYLE=script -VERSION=3 -WRAPPERS=Qt diff --git a/test/gui/webUI/.editorconfig b/test/gui/webUI/.editorconfig deleted file mode 100644 index 9d08b338a6..0000000000 --- a/test/gui/webUI/.editorconfig +++ /dev/null @@ -1,11 +0,0 @@ -[*] -end_of_line = lf -insert_final_newline = true - -[*.js] -indent_style = space -indent_size = 2 - -[*.json] -indent_style = space -indent_size = 4 diff --git a/test/gui/webUI/.gitignore b/test/gui/webUI/.gitignore deleted file mode 100644 index 48413d4739..0000000000 --- a/test/gui/webUI/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ -.pnpm-store/ -package-lock.json -yarn.lock -.playwright diff --git a/test/gui/webUI/login.spec.js b/test/gui/webUI/login.spec.js deleted file mode 100644 index e46fc0214f..0000000000 --- a/test/gui/webUI/login.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -// clang-format off -const { test, expect } = require("@playwright/test"); - -const config = { - auth_url: "", - username: "", - password: "", -}; - -test.beforeEach(async ({ page }) => { - config.auth_url = process.env.OC_AUTH_URL; - config.username = process.env.OC_USERNAME; - config.password = process.env.OC_PASSWORD; - if (!config.auth_url || !config.username || !config.password) { - throw new Error( - "Some of the following envs are not set:\n" + - ` OC_AUTH_URL: ${config.auth_url}\n` + - ` OC_USERNAME: ${config.username}\n` + - ` OC_PASSWORD: ${config.password}` - ); - } - console.info( - "Login info:\n" + - ` OC_AUTH_URL: ${config.auth_url}\n` + - ` OC_USERNAME: ${config.username}\n` + - ` OC_PASSWORD: ${config.password}` - ); - - await page.goto(config.auth_url); -}); - -test("oc login @oidc", async ({ page }) => { - // login - await page.fill("#oc-login-username", config.username); - await page.fill("#oc-login-password", config.password); - await page.click("button[type=submit]"); - // allow permissions - await page.click("button >> text=Allow"); - // confirm successful login - await page.waitForSelector("text=Login Successful"); -}); diff --git a/test/gui/webUI/package.json b/test/gui/webUI/package.json deleted file mode 100644 index a30416fa4a..0000000000 --- a/test/gui/webUI/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "oidc-login", - "version": "0.0.1", - "scripts": { - "oidc-login": "playwright test --grep @oidc" - }, - "devDependencies": { - "@playwright/test": "1.45.0" - }, - "packageManager": "pnpm@8.15.8" -} diff --git a/test/gui/webUI/playwright.config.js b/test/gui/webUI/playwright.config.js deleted file mode 100644 index cb7eb04787..0000000000 --- a/test/gui/webUI/playwright.config.js +++ /dev/null @@ -1,23 +0,0 @@ -// clang-format off -const { devices } = require("@playwright/test"); - -const config = { - testDir: "./", - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - use: { - headless: true, - ignoreHTTPSErrors: true, - }, - - projects: [ - { - name: "chromium", - use: { - ...devices["Desktop Chrome"], - }, - }, - ], -}; - -module.exports = config; diff --git a/test/gui/webUI/pnpm-lock.yaml b/test/gui/webUI/pnpm-lock.yaml deleted file mode 100644 index 9dbccb52ca..0000000000 --- a/test/gui/webUI/pnpm-lock.yaml +++ /dev/null @@ -1,44 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@playwright/test': - specifier: 1.45.0 - version: 1.45.0 - -packages: - - /@playwright/test@1.45.0: - resolution: {integrity: sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==} - engines: {node: '>=18'} - hasBin: true - dependencies: - playwright: 1.45.0 - dev: true - - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /playwright-core@1.45.0: - resolution: {integrity: sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==} - engines: {node: '>=18'} - hasBin: true - dev: true - - /playwright@1.45.0: - resolution: {integrity: sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==} - engines: {node: '>=18'} - hasBin: true - dependencies: - playwright-core: 1.45.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true From 37104d3586488de8031e266f135b59e2d442865f Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 11 May 2026 14:47:03 +0545 Subject: [PATCH 43/75] test(gui): add GUI test reports (#895) * test: add html reporter Signed-off-by: Saw-jan * test: store client logs on scenario failure Signed-off-by: Saw-jan * ci: show test report links Signed-off-by: Saw-jan * ci: separate python modules and browser install Signed-off-by: Saw-jan * test: process client log only if available Signed-off-by: Saw-jan * test: check app file Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/cache-python.yaml | 10 +++-- .woodpecker/ui-tests.yaml | 52 +++++++++---------------- test/gui/behave.ini | 22 ++++++++--- test/gui/config.sample.ini | 3 +- test/gui/environment.py | 40 ++++++++++++++++--- test/gui/helpers/ConfigHelper.py | 39 ++++++++++--------- test/gui/helpers/SetupClientHelper.py | 11 ++---- test/gui/requirements.txt | 1 + test/gui/woodpecker/gui_test_reports.sh | 6 ++- 9 files changed, 106 insertions(+), 78 deletions(-) diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index 014f0f5796..d1d90924d2 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -58,23 +58,25 @@ steps: - name: install-python-modules image: *build_image - environment: - PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/test/gui/.playwright commands: - . ./.woodpecker.env - if $PYTHON_CACHE_FOUND; then exit 0; fi - cd test/gui/ - python3 -m venv .venv --system-site-packages - . .venv/bin/activate - - make install + - make pip-install - tar -czf python-cache.tar.gz .venv - - name: archive-browsers + - name: install-browsers image: *build_image + environment: + PLAYWRIGHT_BROWSERS_PATH: /woodpecker/desktop/test/gui/.playwright commands: - . ./.woodpecker.env - if $BROWSER_CACHE_FOUND; then exit 0; fi - cd test/gui/ + - . .venv/bin/activate + - make install-chromium - tar -czf playwright-browsers.tar.gz .playwright - name: upload-browsers-cache diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index b45f38401d..05fb280210 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -118,8 +118,6 @@ steps: # test environment variables APP_PATH: /woodpecker/desktop/build/bin/opencloud BACKEND_HOST: https://opencloud:9200 - SECURE_BACKEND_HOST: https://opencloud:9200 - CLIENT_LOG_DIR: /woodpecker/desktop/test/gui/clientLog/ GUI_TEST_REPORT_DIR: /woodpecker/desktop/test/gui/reports BEHAVE_TEST_DIR: /woodpecker/desktop/test/gui # Cannot handle this tags format inside a container: --tags='@smoke and not @skip' @@ -134,36 +132,22 @@ steps: # commands: # - cat test/gui/tmp/OpenCloud-crash.log 2>/dev/null || exit 0 - # - name: upload-test-reports - # image: *minio_image - # when: - # - status: failure - # environment: - # <<: *minio_environment - # commands: - # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - # - mc cp -a -r $CI_WORKSPACE/test/gui/guiReportUpload s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER - # - mc cp -a -r $CI_WORKSPACE/test/gui/clientLog/* s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/ - - # - name: gui-test-reports - # image: *minio_image - # when: - # - status: failure - # environment: - # <<: *minio_environment - # commands: - # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - # - bash test/gui/woodpecker/gui_test_reports.sh + - name: upload-test-reports + image: *minio_image + when: + - status: failure + environment: + <<: *minio_environment + commands: + - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + - mc cp -a -r $CI_WORKSPACE/test/gui/reports s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER - # - name: client-log - # image: *minio_image - # when: - # - status: failure - # environment: - # <<: *minio_environment - # commands: - # - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - # - cd test/gui/clientLog/ - # - echo "To download the logs, access the following links:" - # - logs=$(mc find s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/logs/) - # - "for f in $logs; do echo \"$MC_HOST/$f \n \" | cut -d '/' -f1-3,5-99; done" + - name: gui-test-reports + image: *minio_image + when: + - status: failure + environment: + <<: *minio_environment + commands: + - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY + - bash $CI_WORKSPACE/test/gui/woodpecker/gui_test_reports.sh diff --git a/test/gui/behave.ini b/test/gui/behave.ini index dc35d58323..7c8819720c 100644 --- a/test/gui/behave.ini +++ b/test/gui/behave.ini @@ -1,9 +1,21 @@ [behave] paths=features -default_format = plain +default_format = pretty +; NOTE: 'pretty' formatter eats up the last print statement in the test, +; so switch to 'plain' formatter for better debugging. +format = pretty + html-pretty +outfiles = reports/report.log + reports/report.html default_tags = not @skip logging_level = WARNING -capture_stderr = false -capture_stdout = false -capture_log = false -show_skipped = false \ No newline at end of file +capture = false +capture_hooks = false +show_skipped = false + +[behave.formatters] +html-pretty = behave_html_pretty_formatter:PrettyHTMLFormatter + +[behave.userdata] +behave.formatter.html-pretty.title_string = GUI Test Report +behave.formatter.html-pretty.pretty_output = false diff --git a/test/gui/config.sample.ini b/test/gui/config.sample.ini index 3d970bd9d0..549027e75d 100644 --- a/test/gui/config.sample.ini +++ b/test/gui/config.sample.ini @@ -1,11 +1,10 @@ [DEFAULT] +APP_PATH= BACKEND_HOST= CLIENT_ROOT_SYNC_PATH= MAX_SYNC_TIMEOUT= MIN_SYNC_TIMEOUT= LOWEST_SYNC_TIMEOUT= -CLIENT_LOG_FILE= -CLIENT_LOG_DIR= TEMP_FOLDER_PATH= GUI_TEST_REPORT_DIR= RECORD_VIDEO_ON_FAILURE=false diff --git a/test/gui/environment.py b/test/gui/environment.py index 3f9b9bc64a..f455d5812f 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -1,21 +1,43 @@ import shutil import os +from behave.model_core import Status from helpers.ConfigHelper import init_config from helpers.api.provisioning import delete_created_users from helpers.SpaceHelper import delete_project_spaces -from helpers.ConfigHelper import set_config, get_config +from helpers.ConfigHelper import get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths from helpers.SetupClientHelper import close_and_kill_app from step_types.types import * # register all step types -def before_feature(context, feature): - init_config() +def append_scenario_to_app_log(scenario): + with open(get_config('appLogFile'), 'a') as log_file: + logs = ["=" * 80] + logs.append( + f"Scenario: {scenario.name}\nLocation: {scenario.filename}:{scenario.line}" + ) + logs.append("-" * 80) + logs.append("") # extra line break + log_file.write("\n".join(logs)) + +def store_app_log(): + with open(get_config('appLogFile'), 'a') as log_file: + # client log is stored in utf-16. + with open( + get_config('currentAppLogFile'), 'r', encoding='utf-16' + ) as current_log: + log_file.write(f"{current_log.read()}\n\n") -def before_scenario(context, feature): - set_config("currentUserSyncPath", "") + +def cleanup_app_log(): + if os.path.exists(get_config('currentAppLogFile')): + os.remove(get_config('currentAppLogFile')) + + +def before_feature(context, feature): + init_config() def after_scenario(context, scenario): @@ -37,3 +59,11 @@ def after_scenario(context, scenario): delete_created_users() # quit the application close_and_kill_app() + + # store app log on scenario failure + if scenario.status in [Status.failed, Status.error] and os.path.exists( + get_config('currentAppLogFile') + ): + append_scenario_to_app_log(scenario) + store_app_log() + cleanup_app_log() diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index 6168d07867..e83717cf9a 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -8,6 +8,8 @@ CURRENT_DIR = Path(__file__).resolve().parent APP_CONFIG_FILE = "opencloud.cfg" +CUMULATIVE_APP_LOG_FILE = "opencloud.log" +CURRENT_APP_LOG_FILE = "app.log" def is_windows(): @@ -64,8 +66,6 @@ def get_app_env(): 'maxSyncTimeout': 'MAX_SYNC_TIMEOUT', 'minSyncTimeout': 'MIN_SYNC_TIMEOUT', 'lowestSyncTimeout': 'LOWEST_SYNC_TIMEOUT', - 'clientLogFile': 'CLIENT_LOG_FILE', - 'clientLogDir': 'CLIENT_LOG_DIR', 'clientRootSyncPath': 'CLIENT_ROOT_SYNC_PATH', 'tempFolderPath': 'TEMP_FOLDER_PATH', 'guiTestReportDir': 'GUI_TEST_REPORT_DIR', @@ -88,12 +88,10 @@ def get_app_env(): 'maxSyncTimeout': 60, 'minSyncTimeout': 5, 'lowestSyncTimeout': 1, - 'clientLogFile': '', - 'clientLogDir': '', 'clientRootSyncPath': get_client_root_path(), - 'tempFolderPath': os.path.join(get_client_root_path(), 'temp'), 'clientConfigFile': os.path.join(get_config_home(), "OpenCloud", APP_CONFIG_FILE), - 'guiTestReportDir': os.path.abspath('../reports'), + 'guiTestReportDir': os.path.join(CURRENT_DIR.parent, 'reports'), + 'tempFolderPath': os.path.join(get_client_root_path(), 'temp'), 'record_video_on_failure': False, 'files_for_upload': os.path.join(CURRENT_DIR.parent, 'files-for-upload'), 'syncConnectionName': 'Personal', @@ -109,8 +107,6 @@ def get_app_env(): READONLY_CONFIG = list(CONFIG_ENV_MAP.keys()) + list(DEFAULT_PATH_CONFIG.keys()) -SCENARIO_CONFIGS = {} - def read_cfg_file(cfg_path): cfg = ConfigParser() @@ -127,8 +123,7 @@ def read_cfg_file(cfg_path): def init_config(): # try reading configs from config.ini try: - script_path = os.path.dirname(os.path.realpath(__file__)) - cfg_path = os.path.abspath(os.path.join(script_path, '..', 'config.ini')) + cfg_path = os.path.abspath(os.path.join(CURRENT_DIR.parent, 'config.ini')) read_cfg_file(cfg_path) except: pass @@ -162,6 +157,22 @@ def init_config(): if 'app_path' not in CONFIG or not CONFIG['app_path']: raise KeyError('APP_PATH must be set in config.ini or environment variables') + if not os.path.exists(CONFIG['app_path']): + raise KeyError(f'App not found: {CONFIG["app_path"]}') + + ### initialize dynamic config values + # file to store app logs for the current scenario run + CONFIG['currentAppLogFile'] = os.path.join( + CONFIG["guiTestReportDir"], CURRENT_APP_LOG_FILE + ) + # file to store cumulative app logs for the entire test run + CONFIG['appLogFile'] = os.path.join( + CONFIG["guiTestReportDir"], CUMULATIVE_APP_LOG_FILE + ) + # create report dir if it not exist + if not os.path.exists(CONFIG['guiTestReportDir']): + os.makedirs(CONFIG['guiTestReportDir']) + CONFIG['currentUserSyncPath'] = '' def get_config(key): @@ -171,12 +182,4 @@ def get_config(key): def set_config(key, value): if key in READONLY_CONFIG: raise KeyError(f'Cannot set read-only config: {key}') - # save the initial config value - if key not in SCENARIO_CONFIGS: - SCENARIO_CONFIGS[key] = CONFIG.get(key) CONFIG[key] = value - - -def clear_scenario_config(): - for key, value in SCENARIO_CONFIGS.items(): - CONFIG[key] = value diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index de47d281d9..dbd1763e9c 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -107,18 +107,13 @@ def get_current_user_sync_path(): def start_client(): global app_driver - log_command_suffix = "" - logfile = get_config("clientLogFile") - logdir = get_config("clientLogDir") - if logfile != "": - log_command_suffix = f' --logfile {logfile}' - elif logdir != "": - log_command_suffix = f' --logdir {logdir}' + logfile = get_config("currentAppLogFile") + command_args = f' --logfile {logfile}' options = AppiumOptions() options.set_capability( 'app', - f'{get_config("app_path")} -s {log_command_suffix} --logdebug', + f'{get_config("app_path")} -s {command_args} --logdebug', ) options.set_capability('appium:environ', get_app_env()) app_driver = webdriver.Remote( diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 942e0f72c4..4f11b69785 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -17,3 +17,4 @@ Flask==3.0.*; python_version >= "3.10" and sys_platform == 'linux' numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' sure==2.0.*; python_version >= "3.10" pyautogui==0.9.*; python_version >= "3.10" +behave-html-pretty-formatter==1.16.*; python_version >= "3.10" \ No newline at end of file diff --git a/test/gui/woodpecker/gui_test_reports.sh b/test/gui/woodpecker/gui_test_reports.sh index e429ee2b1a..4b0841bec7 100644 --- a/test/gui/woodpecker/gui_test_reports.sh +++ b/test/gui/woodpecker/gui_test_reports.sh @@ -1,11 +1,13 @@ #!/bin/bash -REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/guiReportUpload" +REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/reports" REPORT_URL="$MC_HOST/$REPORT_PATH" echo "" echo "--- GUI Test Reports ---" -echo "GUI Test Report: $REPORT_URL/index.html" +echo "Test Report: $REPORT_URL/report.html" +echo "Client Log: $REPORT_URL/opencloud.log" +echo "AT_SPI Driver Log: $REPORT_URL/atspi_webdriver.log" screenshots=$(mc find s3/$REPORT_PATH/screenshots/ 2>/dev/null || true) if [[ -n "$screenshots" ]]; then From aaa903ff0fdca32e6ff203d508b755cdaf4431c2 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Mon, 11 May 2026 17:00:55 +0545 Subject: [PATCH 44/75] ci: run GUI test suites parallelly using a matrix (#896) * ci: group test suites and run separately Signed-off-by: Saw-jan * ci: fix gui test reports path Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/ui-tests.yaml | 11 +++++++++-- test/gui/woodpecker/gui_test_reports.sh | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 05fb280210..f48654bf08 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -33,6 +33,13 @@ workspace: base: /woodpecker path: desktop +matrix: + include: + - MATRIX_NAME: gui-tests-1 + SUITES: 'features/activity features/add-account features/delete-files-folders features/edit-files features/login-logout features/move-files-folders' + - MATRIX_NAME: gui-tests-2 + SUITES: 'features/remove-account-connection features/spaces features/sync-resources features/tabs-settings features/vfs' + steps: - name: restore-python-cache image: *minio_image @@ -121,7 +128,7 @@ steps: GUI_TEST_REPORT_DIR: /woodpecker/desktop/test/gui/reports BEHAVE_TEST_DIR: /woodpecker/desktop/test/gui # Cannot handle this tags format inside a container: --tags='@smoke and not @skip' - BEHAVE_PARAMETERS: '--tags=@smoke --tags=~@skip features' + BEHAVE_PARAMETERS: '--tags=@smoke --tags=~@skip ${SUITES}' # - name: crash-log # image: *alpine_image @@ -140,7 +147,7 @@ steps: <<: *minio_environment commands: - mc alias set s3 $MC_HOST $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY - - mc cp -a -r $CI_WORKSPACE/test/gui/reports s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER + - mc cp -a -r $CI_WORKSPACE/test/gui/reports s3/$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/$MATRIX_NAME - name: gui-test-reports image: *minio_image diff --git a/test/gui/woodpecker/gui_test_reports.sh b/test/gui/woodpecker/gui_test_reports.sh index 4b0841bec7..62b51c377d 100644 --- a/test/gui/woodpecker/gui_test_reports.sh +++ b/test/gui/woodpecker/gui_test_reports.sh @@ -1,6 +1,6 @@ #!/bin/bash -REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/reports" +REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/$MATRIX_NAME/reports" REPORT_URL="$MC_HOST/$REPORT_PATH" echo "" From 2fd4b4ab16a53a2365b927c1a786a655e79e5d6a Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Tue, 12 May 2026 16:21:18 +0545 Subject: [PATCH 45/75] test: implement screenshot reports (#897) Signed-off-by: prashant-gurung899 --- test/gui/environment.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/gui/environment.py b/test/gui/environment.py index f455d5812f..56394d9d32 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -1,6 +1,9 @@ import shutil import os +import re +import pyautogui from behave.model_core import Status +from datetime import datetime from helpers.ConfigHelper import init_config from helpers.api.provisioning import delete_created_users @@ -40,6 +43,18 @@ def before_feature(context, feature): init_config() +def after_step(context, step): + if step.status in [Status.failed, Status.error]: + + step_name = re.sub(r'[^a-zA-Z0-9_]', '_', step.name) + timestamp = datetime.now().strftime("%d-%b-%Y_%H-%M-%S") + screenshots_dir = os.path.join(get_config("guiTestReportDir"), "screenshots") + os.makedirs(screenshots_dir, exist_ok=True) + + file_path = os.path.join(screenshots_dir, f"{step_name}_{timestamp}.png") + pyautogui.screenshot(file_path) + + def after_scenario(context, scenario): # clean up sync dir if os.path.exists(get_config("clientRootSyncPath")): From 70463d6eec63d05f9f8b9068e3fa790a36027efa Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 13 May 2026 10:47:18 +0545 Subject: [PATCH 46/75] refactor(gui-test): bind pyautogui methods to appium element (#901) * test: add and use element helper Signed-off-by: Saw-jan * test: use mouse right click for context menu Signed-off-by: Saw-jan * test: implement app helper Signed-off-by: Saw-jan * test: use custom mouse and keyboard events methods Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- test/gui/environment.py | 2 +- test/gui/helpers/AppHelper.py | 71 ++++++++++++++++ test/gui/helpers/ElementHelper.py | 5 ++ test/gui/helpers/SetupClientHelper.py | 67 +-------------- test/gui/helpers/SyncHelper.py | 2 +- test/gui/helpers/keys/keys_map.py | 76 +++++++++++++++++ .../pageObjects/AccountConnectionWizard.py | 2 +- test/gui/pageObjects/AccountSetting.py | 16 ++-- test/gui/pageObjects/Activity.py | 26 ++---- test/gui/pageObjects/EnterPassword.py | 19 +++-- test/gui/pageObjects/SyncConnection.py | 16 +--- test/gui/pageObjects/SyncConnectionWizard.py | 82 +++++++++---------- test/gui/pageObjects/Toolbar.py | 19 ++--- test/gui/steps/account_context.py | 3 +- 14 files changed, 239 insertions(+), 167 deletions(-) create mode 100644 test/gui/helpers/AppHelper.py create mode 100644 test/gui/helpers/ElementHelper.py create mode 100644 test/gui/helpers/keys/keys_map.py diff --git a/test/gui/environment.py b/test/gui/environment.py index 56394d9d32..1ff0f5aaa4 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -10,7 +10,7 @@ from helpers.SpaceHelper import delete_project_spaces from helpers.ConfigHelper import get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths -from helpers.SetupClientHelper import close_and_kill_app +from helpers.AppHelper import close_and_kill_app from step_types.types import * # register all step types diff --git a/test/gui/helpers/AppHelper.py b/test/gui/helpers/AppHelper.py new file mode 100644 index 0000000000..2bdba52136 --- /dev/null +++ b/test/gui/helpers/AppHelper.py @@ -0,0 +1,71 @@ +import pyautogui +import psutil +from appium.webdriver import Remote, WebElement +from appium.options.common.base import AppiumOptions + +from helpers.ConfigHelper import get_config, get_app_env +from helpers.ElementHelper import get_element_center_xy +from helpers.keys.keys_map import get_key + + +def native_click(self, **kwargs): + x, y = get_element_center_xy(self) + pyautogui.click(x, y, **kwargs) + + +def native_double_click(self, **kwargs): + x, y = get_element_center_xy(self) + pyautogui.doubleClick(x, y, **kwargs) + + +def native_send_keys(self, key): + pyautogui.press(get_key(key)) + + +# bind custom element methods +WebElement.native_click = native_click +WebElement.native_double_click = native_double_click +WebElement.native_send_keys = native_send_keys + +app_driver = None + + +def app(): + return app_driver + + +def create_app_session(): + global app_driver + logfile = get_config("currentAppLogFile") + command_args = f' --logfile {logfile}' + + options = AppiumOptions() + options.set_capability( + 'app', + f'{get_config("app_path")} -s {command_args} --logdebug', + ) + options.set_capability('appium:environ', get_app_env()) + app_driver = Remote(command_executor='http://localhost:4723', options=options) + app_driver.implicitly_wait = 10 + + +def close_and_kill_app(): + """ + Close Appium session and kill the desktop client process. + Use this for both mid-scenario and end-of-scenario cleanup. + """ + global app_driver + # Quit Appium session + if app_driver is not None: + app_driver.quit() + + # Kill remaining process by exe path + app_path = get_config("app_path") + for process in psutil.process_iter(['pid', 'exe']): + if process.info['exe'] == app_path: + print("Closing desktop client...") + psutil.Process(process.info['pid']).kill() + break + + # Reset driver for reuse + app_driver = None diff --git a/test/gui/helpers/ElementHelper.py b/test/gui/helpers/ElementHelper.py new file mode 100644 index 0000000000..86839b37fb --- /dev/null +++ b/test/gui/helpers/ElementHelper.py @@ -0,0 +1,5 @@ +def get_element_center_xy(element): + rect = element.rect + x = int(rect['x'] + (rect['width'] // 2)) + y = int(rect['y'] + (rect['height'] // 2)) + return x, y diff --git a/test/gui/helpers/SetupClientHelper.py b/test/gui/helpers/SetupClientHelper.py index dbd1763e9c..dc0967d8a4 100644 --- a/test/gui/helpers/SetupClientHelper.py +++ b/test/gui/helpers/SetupClientHelper.py @@ -2,26 +2,18 @@ import os import subprocess import test -import psutil from urllib.parse import urlparse from os import makedirs from os.path import exists, join from PySide6.QtCore import QSettings, QUuid, QUrl, QJsonValue -from appium import webdriver -from appium.options.common.base import AppiumOptions from helpers.SpaceHelper import get_space_id, get_personal_space_id -from helpers.ConfigHelper import get_config, set_config, is_windows, get_app_env +from helpers.ConfigHelper import get_config, set_config, is_windows from helpers.SyncHelper import listen_sync_status_for_item from helpers.api.utils import url_join from helpers.UserHelper import get_displayname_for_user from helpers.api import provisioning - -app_driver = None - - -def app(): - return app_driver +from helpers.AppHelper import create_app_session def substitute_inline_codes(value): @@ -106,20 +98,7 @@ def get_current_user_sync_path(): def start_client(): - global app_driver - logfile = get_config("currentAppLogFile") - command_args = f' --logfile {logfile}' - - options = AppiumOptions() - options.set_capability( - 'app', - f'{get_config("app_path")} -s {command_args} --logdebug', - ) - options.set_capability('appium:environ', get_app_env()) - app_driver = webdriver.Remote( - command_executor='http://localhost:4723', options=options - ) - app_driver.implicitly_wait = 10 + create_app_session() def get_polling_interval(): @@ -198,24 +177,6 @@ def setup_client(username, space='Personal'): listen_sync_status_for_item(sync_path) -def is_app_killed(pid): - try: - psutil.Process(pid) - return False - except psutil.NoSuchProcess: - return True - - -def wait_until_app_killed(pid=0): - timeout = 5 * 1000 - killed = squish.waitFor( - lambda: is_app_killed(pid), - timeout, - ) - if not killed: - test.log(f'Application was not terminated within {timeout} milliseconds') - - def generate_uuidv4(): return str(uuid.uuid4()) @@ -264,25 +225,3 @@ def run_sys_command(command=None, shell=False): check=False, ) return cmd.stdout, cmd.stderr, cmd.returncode - - -def close_and_kill_app(): - """ - Close Appium session and kill the desktop client process. - Use this for both mid-scenario and end-of-scenario cleanup. - """ - global app_driver - # Quit Appium session - if app_driver is not None: - app_driver.quit() - - # Kill remaining process by exe path - app_path = get_config("app_path") - for process in psutil.process_iter(['pid', 'exe']): - if process.info['exe'] == app_path: - print("Closing desktop client...") - psutil.Process(process.info['pid']).kill() - break - - # Reset driver for reuse - app_driver = None diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index f77e2aefb2..e3d52c5844 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -369,7 +369,7 @@ def make_available_locally(resource_path): def wait_for(condition, timeout, interval=0.5): - from helpers.SetupClientHelper import app + from helpers.AppHelper import app wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval) try: diff --git a/test/gui/helpers/keys/keys_map.py b/test/gui/helpers/keys/keys_map.py new file mode 100644 index 0000000000..459dbe3576 --- /dev/null +++ b/test/gui/helpers/keys/keys_map.py @@ -0,0 +1,76 @@ +from selenium.webdriver.common.keys import Keys + +# Key mapping from Selenium's Keys to pyautogui's key names +# See: +# - https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.keys +# - https://pyautogui.readthedocs.io/en/latest/keyboard.html?highlight=keys#keyboard-keys +KEY_MAP = { + Keys.ADD: 'add', + Keys.ALT: 'alt', + Keys.ARROW_DOWN: 'down', + Keys.ARROW_LEFT: 'left', + Keys.ARROW_RIGHT: 'right', + Keys.ARROW_UP: 'up', + Keys.BACKSPACE: 'backspace', + Keys.BACK_SPACE: 'backspace', + Keys.CLEAR: 'clear', + Keys.COMMAND: 'command', + Keys.CONTROL: 'ctrl', + Keys.DECIMAL: 'decimal', + Keys.DELETE: 'delete', + Keys.DIVIDE: 'divide', + Keys.DOWN: 'down', + Keys.END: 'end', + Keys.ENTER: 'enter', + Keys.EQUALS: '=', + Keys.ESCAPE: 'escape', + Keys.F1: 'f1', + Keys.F10: 'f10', + Keys.F11: 'f11', + Keys.F12: 'f12', + Keys.F2: 'f2', + Keys.F3: 'f3', + Keys.F4: 'f4', + Keys.F5: 'f5', + Keys.F6: 'f6', + Keys.F7: 'f7', + Keys.F8: 'f8', + Keys.F9: 'f9', + Keys.HELP: 'help', + Keys.HOME: 'home', + Keys.INSERT: 'insert', + Keys.LEFT: 'left', + Keys.LEFT_ALT: 'altleft', + Keys.LEFT_CONTROL: 'ctrlleft', + Keys.LEFT_SHIFT: 'shiftleft', + Keys.META: 'win', + Keys.MULTIPLY: 'multiply', + Keys.NUMPAD0: 'num0', + Keys.NUMPAD1: 'num1', + Keys.NUMPAD2: 'num2', + Keys.NUMPAD3: 'num3', + Keys.NUMPAD4: 'num4', + Keys.NUMPAD5: 'num5', + Keys.NUMPAD6: 'num6', + Keys.NUMPAD7: 'num7', + Keys.NUMPAD8: 'num8', + Keys.NUMPAD9: 'num9', + Keys.PAGE_DOWN: 'pagedown', + Keys.PAGE_UP: 'pageup', + Keys.PAUSE: 'pause', + Keys.RETURN: 'return', + Keys.RIGHT: 'right', + Keys.SEMICOLON: ';', + Keys.SEPARATOR: 'separator', + Keys.SHIFT: 'shift', + Keys.SPACE: 'space', + Keys.SUBTRACT: 'subtract', + Keys.TAB: 'tab', + Keys.UP: 'up', +} + + +def get_key(key): + if key in KEY_MAP: + return KEY_MAP[key] + return key diff --git a/test/gui/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py index 5ee90cb6a7..bdbc3dcad3 100644 --- a/test/gui/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -10,7 +10,7 @@ set_current_user_sync_path, ) from helpers.SyncHelper import listen_sync_status_for_item -from helpers.SetupClientHelper import app +from helpers.AppHelper import app class AccountConnectionWizard: diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 3d8acc919a..50fca8a719 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -1,11 +1,11 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By -from helpers.UserHelper import get_displayname_for_user -from helpers.SetupClientHelper import substitute_inline_codes, app from pageObjects.Toolbar import Toolbar from helpers.UserHelper import get_displayname_for_user -from helpers.SetupClientHelper import app, substitute_inline_codes +from helpers.SetupClientHelper import substitute_inline_codes +from helpers.UserHelper import get_displayname_for_user +from helpers.AppHelper import app from helpers.SyncHelper import wait_for @@ -66,7 +66,14 @@ def login(): @staticmethod def get_account_connection_label(): - label = app().find_element(AccountSetting.ACCOUNT_CONNECTION_LABEL.by, AccountSetting.ACCOUNT_CONNECTION_LABEL.selector).text + label = ( + app() + .find_element( + AccountSetting.ACCOUNT_CONNECTION_LABEL.by, + AccountSetting.ACCOUNT_CONNECTION_LABEL.selector, + ) + .text + ) return label @staticmethod @@ -110,7 +117,6 @@ def wait_until_account_is_connected(timeout=5000): ) return result - @staticmethod def wait_until_sync_folder_is_configured(timeout=5000): result = squish.waitFor( diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 2586ff77ec..54bfb832fa 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -4,7 +4,7 @@ from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config -from helpers.SetupClientHelper import app +from helpers.AppHelper import app class Activity: @@ -20,7 +20,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 { @@ -44,10 +43,7 @@ def get_not_synced_status(row): @staticmethod def click_tab(tab_name): selector = Activity.SUBTAB_CONTAINER.selector.format(tab_name=tab_name) - app().find_element( - Activity.SUBTAB_CONTAINER.by, - selector - ).click() + app().find_element(Activity.SUBTAB_CONTAINER.by, selector).click() @staticmethod def check_file_exist(filename): @@ -62,7 +58,7 @@ def is_resource_blacklisted(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "Blacklisted"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod @@ -70,7 +66,7 @@ def is_resource_ignored(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "File Ignored"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod @@ -78,16 +74,13 @@ def is_resource_excluded(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "Excluded"), get_config("maxSyncTimeout") * 1000, - ) + ) return result @staticmethod def has_sync_status(filename, status): try: - app().find_element( - Activity.SYNCED_ACTIVITY_STATUS.by, - status - ) + app().find_element(Activity.SYNCED_ACTIVITY_STATUS.by, status) return True except: return False @@ -96,11 +89,10 @@ def has_sync_status(filename, status): def select_synced_filter(sync_filter): app().find_element( Activity.LOCAL_ACTIVITY_FILTER_BUTTON.by, - Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector + Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector, ).click() app().find_element( - Activity.SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR.by, - sync_filter + Activity.SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR.by, sync_filter ) @staticmethod @@ -155,7 +147,7 @@ def check_not_synced_table(resource, status, account): file_row = squish.waitForObject( Activity.get_not_synced_file_selector(resource), get_config("lowestSyncTimeout") * 1000, - )["row"] + )["row"] squish.waitForObjectExists( { "column": Activity.get_not_synced_table_column_number_by_name( diff --git a/test/gui/pageObjects/EnterPassword.py b/test/gui/pageObjects/EnterPassword.py index 4d211d6e49..50bcdbd6a6 100644 --- a/test/gui/pageObjects/EnterPassword.py +++ b/test/gui/pageObjects/EnterPassword.py @@ -3,21 +3,28 @@ from pageObjects.AccountConnectionWizard import AccountConnectionWizard from helpers.WebUIHelper import authorize_via_webui -from helpers.SetupClientHelper import app +from helpers.AppHelper import app class EnterPassword: LOGIN_CONTAINER = SimpleNamespace(by=None, selector=None) - LOGIN_USER_LABEL = SimpleNamespace(by=By.XPATH, selector="//filler[@name='Login required']//label[contains(@name, 'Connecting')]") + LOGIN_USER_LABEL = SimpleNamespace( + by=By.XPATH, + selector="//filler[@name='Login required']//label[contains(@name, 'Connecting')]", + ) USERNAME_BOX = SimpleNamespace(by=None, selector=None) LOGOUT_BUTTON = SimpleNamespace(by=None, selector=None) def get_username(self): # Parse username from the login label: - label = app().find_element( - EnterPassword.LOGIN_USER_LABEL.by, - EnterPassword.LOGIN_USER_LABEL.selector - ).text + label = ( + app() + .find_element( + EnterPassword.LOGIN_USER_LABEL.by, + EnterPassword.LOGIN_USER_LABEL.selector, + ) + .text + ) username = label.split(" ", maxsplit=2)[1] return username.capitalize() diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index 745ef17070..c4bf004b9c 100644 --- a/test/gui/pageObjects/SyncConnection.py +++ b/test/gui/pageObjects/SyncConnection.py @@ -3,7 +3,7 @@ from selenium.webdriver.common.keys import Keys from helpers.ConfigHelper import get_config -from helpers.SetupClientHelper import app +from helpers.AppHelper import app class SyncConnection: @@ -47,19 +47,7 @@ def open_menu(sync_folder=None): sync_path=get_config('currentUserSyncPath'), ), ) - # Cannot select sync folder menu button. - # This is a messy workaround to open the context menu using keyboard navigation. - # Ideally, we should be able to do: click() and send_keys(" ") to open the menu - # but it doesn't work for some reason. - # Also, send_keys(Keys.SPACE) doesn't work. - menu_button.click() - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(Keys.TAB) - menu_button.send_keys(" ") + menu_button.native_click(button='right') @staticmethod def perform_action(action): diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 3e4d1a33e8..9c497dc6b1 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -1,11 +1,9 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys -import time -import pyautogui from helpers.SetupClientHelper import get_current_user_sync_path -from helpers.SetupClientHelper import app +from helpers.AppHelper import app class SyncConnectionWizard: @@ -20,7 +18,9 @@ class SyncConnectionWizard: ) REMOTE_FOLDER_TREE = SimpleNamespace(by=None, selector=None) SELECTIVE_SYNC_TREE_HEADER = SimpleNamespace(by=None, selector=None) - CANCEL_FOLDER_SYNC_CONNECTION_WIZARD = SimpleNamespace(by=By.NAME, selector="Cancel") + CANCEL_FOLDER_SYNC_CONNECTION_WIZARD = SimpleNamespace( + by=By.NAME, selector="Cancel" + ) 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) @@ -71,12 +71,9 @@ def select_remote_destination_folder(folder): @staticmethod def deselect_all_remote_folders(): - element = app().find_element( - By.NAME, - "Add Space" - ) + element = app().find_element(By.NAME, "Add Space") element.send_keys(Keys.ARROW_DOWN) - element.send_keys(" ") + element.native_send_keys(Keys.SPACE) # uncheck the root folder @staticmethod def sort_by(header_text): @@ -197,37 +194,41 @@ def is_add_sync_folder_button_enabled(): ).enabled @staticmethod - def select_or_unselect_folders_to_sync( - folders, - should_select=True - ): - if should_select: - SyncConnectionWizard.deselect_all_remote_folders() - - for folder in folders: - path_parts = folder.strip("/").split("/") - - for i in range(len(path_parts) - 1): - parent_folder_name = path_parts[i] - parent_elements = app().find_elements(By.NAME, parent_folder_name) + def select_or_unselect_folders_to_sync(folders, select=True): + expected_state = "true" if select else "false" - for element in parent_elements: - isChecked = element.get_attribute("checked") - if isChecked == "true": - parent_bounds = element.rect - center_x = int(parent_bounds['x'] + parent_bounds['width'] // 2) - center_y = int(parent_bounds['y'] + parent_bounds['height'] // 2) - pyautogui.doubleClick(center_x, center_y) - - target_folder_name = path_parts[-1] - folder_element = app().find_element(By.NAME, target_folder_name) - - element_bounds = folder_element.rect - checkbox_x_position = int(element_bounds['x'] + 10) - checkbox_y_position = int(element_bounds['y'] + element_bounds['height'] // 2) + if select: + SyncConnectionWizard.deselect_all_remote_folders() - pyautogui.moveTo(checkbox_x_position, checkbox_y_position, duration=0.1) - pyautogui.click() + for folder_path in folders: + parents = folder_path.strip("/").split("/") + target_folder = parents.pop() + + parent_element = None + for parent in parents: + if parent_element: + p_elements = parent_element.find_elements(By.NAME, parent) + else: + p_elements = app().find_elements(By.NAME, parent) + for p_element in p_elements: + if p_element.get_attribute("checked") == 'true': + parent_element = p_element + parent_element.native_double_click() # expand the folder + + folder_element = app().find_element(By.NAME, target_folder) + is_checked = folder_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() + folder_element.native_send_keys(Keys.SPACE) # select the folder + + is_checked = folder_element.get_attribute("checked") + if is_checked != expected_state: + raise AssertionError( + f"Failed to {'select' if select else 'unselect'} folder: {folder_path}" + ) @staticmethod def confirm_choose_what_to_sync_selection(): @@ -235,10 +236,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=should_select - ) + SyncConnectionWizard.select_or_unselect_folders_to_sync(folders, should_select) if new_sync_connection_wizard: SyncConnectionWizard.add_sync_connection() diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 6a88acb065..243dd75220 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -3,7 +3,7 @@ from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys -from helpers.SetupClientHelper import app, close_and_kill_app +from helpers.AppHelper import app from helpers.ConfigHelper import get_config from helpers.UserHelper import get_displayname_for_user @@ -14,13 +14,10 @@ class Toolbar: ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") ACTIVITY_BUTTON = SimpleNamespace(by=By.NAME, selector="Activity") SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) - QUIT_BUTTON = SimpleNamespace( - by=By.NAME, - selector="Quit" - ) + QUIT_BUTTON = SimpleNamespace(by=By.NAME, selector="Quit") CONFIRM_QUIT_BUTTON = SimpleNamespace( by=By.ACCESSIBILITY_ID, - selector="QApplication.QMessageBox.qt_msgbox_buttonbox.QPushButton" + selector="QApplication.QMessageBox.qt_msgbox_buttonbox.QPushButton", ) TOOLBAR_ITEMS = ["Add Account", "Activity", "Settings", "Quit"] @@ -80,16 +77,10 @@ def open_settings_tab(): @staticmethod def quit_opencloud(): + app().find_element(Toolbar.QUIT_BUTTON.by, Toolbar.QUIT_BUTTON.selector).click() app().find_element( - Toolbar.QUIT_BUTTON.by, - Toolbar.QUIT_BUTTON.selector - ).click() - app().find_element( - Toolbar.CONFIRM_QUIT_BUTTON.by, - Toolbar.CONFIRM_QUIT_BUTTON.selector + Toolbar.CONFIRM_QUIT_BUTTON.by, Toolbar.CONFIRM_QUIT_BUTTON.selector ).click() - close_and_kill_app() - @staticmethod def get_accounts(): diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index bd92e94bdb..a3c338da58 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -16,7 +16,6 @@ get_client_details, generate_account_config, get_resource_path, - app, ) from helpers.SyncHelper import ( wait_for_initial_sync_to_complete, @@ -123,7 +122,7 @@ def step(context, username): @Then('user "{username}" should be signed out') def step(context, username): user_signed_out = AccountSetting.is_user_signed_out() - + with ensure('User "{0}" should be signed out, but is still signed in', username): user_signed_out.should.be.true From 86923d2ef35240ca6c189a4733c077eb074cd731 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 13 May 2026 17:46:53 +0545 Subject: [PATCH 47/75] ci: build client with vfs (#902) * ci: build client with vfs Signed-off-by: Saw-jan * ci: drop deprecated runs_on config Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/build.yaml | 4 +--- .woodpecker/notification.yaml | 7 +++---- .woodpecker/purge-cache.yaml | 7 +++---- .woodpecker/ui-tests.yaml | 1 - 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml index c296eb5437..c2ac8200ae 100644 --- a/.woodpecker/build.yaml +++ b/.woodpecker/build.yaml @@ -58,9 +58,7 @@ steps: commands: - mkdir -p build - cd build - # currently, vfs plugin is broken so disable it to be able to build the client and run tests - # TODO: fix the vfs plugin and enable it back - - cmake -GNinja -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Debug -DVIRTUAL_FILE_SYSTEM_PLUGINS=off -S .. + - cmake -GNinja -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Debug -S .. - ninja - name: upload-desktop-client-cache diff --git a/.woodpecker/notification.yaml b/.woodpecker/notification.yaml index 285ace6066..a2ce554143 100644 --- a/.woodpecker/notification.yaml +++ b/.woodpecker/notification.yaml @@ -15,15 +15,14 @@ when: - event: tag - event: cron cron: nightly* + - status: + - success + - failure depends_on: - build - ui-tests -runs_on: - - success - - failure - skip_clone: true steps: diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index a9eb9c6d53..d7ca5890b7 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -18,14 +18,13 @@ when: !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") - event: cron cron: nightly* + - status: + - success + - failure depends_on: - ui-tests -runs_on: - - success - - failure - skip_clone: true matrix: diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index f48654bf08..314b948308 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -113,7 +113,6 @@ steps: - name: gui-tests image: *build_image - pull: true environment: # system cache environment variables PYTHONUSERBASE: /woodpecker/desktop/test/gui/.venv From 2af6d94845656f90beb0855374cd13ae9f2034e4 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 18:05:32 +0545 Subject: [PATCH 48/75] ci: fix woodpecker warnings Signed-off-by: Saw-jan --- .woodpecker/notification.yaml | 12 +++++++++--- .woodpecker/purge-cache.yaml | 12 ++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.woodpecker/notification.yaml b/.woodpecker/notification.yaml index a2ce554143..63ccd29bfc 100644 --- a/.woodpecker/notification.yaml +++ b/.woodpecker/notification.yaml @@ -7,15 +7,21 @@ variables: from_secret: oc_ci_url when: - - event: [push, manual] + - event: + - push + - manual branch: - main - stable-* + status: + - success + - failure - event: pull_request - - event: tag + status: + - failure - event: cron cron: nightly* - - status: + status: - success - failure diff --git a/.woodpecker/purge-cache.yaml b/.woodpecker/purge-cache.yaml index d7ca5890b7..18e9fc72bb 100644 --- a/.woodpecker/purge-cache.yaml +++ b/.woodpecker/purge-cache.yaml @@ -11,14 +11,22 @@ variables: PUBLIC_BUCKET: public when: - - event: [push, manual] + - event: + - push + - manual branch: ${CI_REPO_DEFAULT_BRANCH} + status: + - success + - failure - event: pull_request evaluate: | !(CI_COMMIT_SOURCE_BRANCH matches "next-release/(main|stable-*)" && CI_COMMIT_AUTHOR == "openclouders") + status: + - success + - failure - event: cron cron: nightly* - - status: + status: - success - failure From 3eaf68458ed390a274d96d43560d258eeb3511dc Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 13:15:04 +0545 Subject: [PATCH 49/75] test: capture screenshot in CI only Signed-off-by: Saw-jan --- test/gui/environment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/gui/environment.py b/test/gui/environment.py index 1ff0f5aaa4..eb80de3886 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -44,14 +44,14 @@ def before_feature(context, feature): def after_step(context, step): - if step.status in [Status.failed, Status.error]: - - step_name = re.sub(r'[^a-zA-Z0-9_]', '_', step.name) + if step.status in [Status.failed, Status.error] and os.getenv("CI"): + scenario = context.scenario.name.lower() + scenario = re.sub(r'[^a-zA-Z0-9_]', '_', scenario) timestamp = datetime.now().strftime("%d-%b-%Y_%H-%M-%S") screenshots_dir = os.path.join(get_config("guiTestReportDir"), "screenshots") os.makedirs(screenshots_dir, exist_ok=True) - file_path = os.path.join(screenshots_dir, f"{step_name}_{timestamp}.png") + file_path = os.path.join(screenshots_dir, f"{scenario}_{timestamp}.png") pyautogui.screenshot(file_path) From 9d1a48cd27d483cc6100d428540457cd2ce9e774 Mon Sep 17 00:00:00 2001 From: Prashant Gurung <53248463+prashant-gurung899@users.noreply.github.com> Date: Mon, 18 May 2026 11:39:22 +0545 Subject: [PATCH 50/75] test: capture and store video for failed scenarios (#906) --- test/gui/environment.py | 11 ++++ test/gui/helpers/ScreenRecorder.py | 82 +++++++++++++++++++++++++ test/gui/requirements.txt | 4 +- test/gui/woodpecker/gui_test_reports.sh | 13 ++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 test/gui/helpers/ScreenRecorder.py diff --git a/test/gui/environment.py b/test/gui/environment.py index eb80de3886..cf062d3017 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -5,6 +5,7 @@ from behave.model_core import Status from datetime import datetime +from helpers import ScreenRecorder from helpers.ConfigHelper import init_config from helpers.api.provisioning import delete_created_users from helpers.SpaceHelper import delete_project_spaces @@ -43,6 +44,11 @@ def before_feature(context, feature): init_config() +def before_scenario(context, scenario): + if os.getenv("CI"): + ScreenRecorder.start_recording(scenario) + + def after_step(context, step): if step.status in [Status.failed, Status.error] and os.getenv("CI"): scenario = context.scenario.name.lower() @@ -56,6 +62,11 @@ def after_step(context, step): def after_scenario(context, scenario): + + # stop screen recording + if os.getenv("CI"): + ScreenRecorder.stop_recording(passed=scenario.status == Status.passed) + # clean up sync dir if os.path.exists(get_config("clientRootSyncPath")): for entry in os.scandir(get_config("clientRootSyncPath")): diff --git a/test/gui/helpers/ScreenRecorder.py b/test/gui/helpers/ScreenRecorder.py new file mode 100644 index 0000000000..1576986ff1 --- /dev/null +++ b/test/gui/helpers/ScreenRecorder.py @@ -0,0 +1,82 @@ +import os +import re +import threading +import time +import mss +import numpy as np +import imageio_ffmpeg +from datetime import datetime + +from helpers.ConfigHelper import get_config + + +_recording_thread = None +_stop_event = threading.Event() +_video_path = None + + +def _build_video_path(scenario): + safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", scenario.name) + timestamp = datetime.now().strftime("%d-%b-%Y_%H-%M-%S") + + recordings_dir = os.path.join(get_config("guiTestReportDir"), "recordings") + os.makedirs(recordings_dir, exist_ok=True) + + return os.path.join(recordings_dir, f"{safe_name}_{timestamp}.mp4") + + +def _record_loop(video_path): + with mss.mss() as sct: + monitor = sct.monitors[0] + width, height = monitor["width"], monitor["height"] + + writer = imageio_ffmpeg.write_frames( + video_path, + size=(width, height), + fps=24, + codec="libx264", + output_params=["-crf", "23", "-pix_fmt", "yuv420p"], + ) + writer.send(None) + + interval = 1.0 / 24 # 1/24 seconds between each frame so we get 24 frames per second + next_frame_at = time.monotonic() + + while not _stop_event.is_set(): + frame = sct.grab(monitor) + # mss gives BGRA — drop alpha, flip B and R channels to get RGB + rgb = np.flip(np.array(frame)[:, :, :3], axis=2).tobytes() + writer.send(rgb) + + next_frame_at += interval + sleep_for = next_frame_at - time.monotonic() + if sleep_for > 0: + time.sleep(sleep_for) + + writer.close() + + +def start_recording(scenario): + global _recording_thread, _video_path + + _video_path = _build_video_path(scenario) + _stop_event.clear() + + _recording_thread = threading.Thread(target=_record_loop, args=(_video_path,), daemon=True) + _recording_thread.start() + + +def stop_recording(passed): + global _recording_thread, _video_path + + if _recording_thread is None: + return + + _stop_event.set() + _recording_thread.join() + _recording_thread = None + + if passed and os.path.exists(_video_path): + os.remove(_video_path) + + _video_path = None diff --git a/test/gui/requirements.txt b/test/gui/requirements.txt index 4f11b69785..434ce7001b 100644 --- a/test/gui/requirements.txt +++ b/test/gui/requirements.txt @@ -17,4 +17,6 @@ Flask==3.0.*; python_version >= "3.10" and sys_platform == 'linux' numpy==1.26.*; python_version >= "3.10" and sys_platform == 'linux' sure==2.0.*; python_version >= "3.10" pyautogui==0.9.*; python_version >= "3.10" -behave-html-pretty-formatter==1.16.*; python_version >= "3.10" \ No newline at end of file +behave-html-pretty-formatter==1.16.*; python_version >= "3.10" +mss==9.*; python_version >= "3.10" +imageio-ffmpeg==0.6.*; python_version >= "3.10" diff --git a/test/gui/woodpecker/gui_test_reports.sh b/test/gui/woodpecker/gui_test_reports.sh index 62b51c377d..166913bba5 100644 --- a/test/gui/woodpecker/gui_test_reports.sh +++ b/test/gui/woodpecker/gui_test_reports.sh @@ -20,3 +20,16 @@ if [[ -n "$screenshots" ]]; then else echo "No screenshots found." fi + +recordings=$(mc find s3/$REPORT_PATH/recordings/ 2>/dev/null || true) +if [[ -n "$recordings" ]]; then + echo "" + echo "Recordings:" + for f in $recordings; do + # remove 's3/' prefix + f=${f/s3\//} + echo " - $MC_HOST/$f" + done +else + echo "No recordings found." +fi From d221e3d92aa486fec2974b54c5b9fba8457d2cb3 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 10:37:09 +0545 Subject: [PATCH 51/75] test: wait for files sync Signed-off-by: Saw-jan --- test/gui/features/spaces/spaces.feature | 3 ++- test/gui/features/sync-resources/syncResources.feature | 5 +++-- test/gui/helpers/SyncHelper.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/gui/features/spaces/spaces.feature b/test/gui/features/spaces/spaces.feature index c8cfa8d975..7c155b3d87 100644 --- a/test/gui/features/spaces/spaces.feature +++ b/test/gui/features/spaces/spaces.feature @@ -44,7 +44,8 @@ Feature: Project spaces test content """ And user "Alice" creates a folder "localFolder" inside the sync folder - And the user waits for the files to sync + And the user waits for file "localFile.txt" to be synced + And the user waits for folder "localFolder" to be synced Then as "Alice" the file "localFile.txt" in the space "Project101" should have content "test content" in the server And as "Alice" the space "Project101" should have folder "localFolder" in the server diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 97d0480b24..80a5273e32 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -223,7 +223,7 @@ Feature: Syncing files """ test content """ - And the user waits for the files to sync + And the user waits for folder "parent/subfolder5/test.txt" to be synced Then as "Alice" folder "parent/subfolderEmpty1" should exist in the server And as "Alice" folder "parent/subfolderEmpty2" should exist in the server And as "Alice" folder "parent/subfolderEmpty3" should exist in the server @@ -582,8 +582,9 @@ Feature: Syncing files | test-folder/sub-folder2 | And the user waits for the files to sync Then the folder "test-folder/sub-folder1" should exist on the file system - But the folder "test-folder/sub-folder2" should not exist on the file system + And the folder "test-folder/sub-folder2" should not exist on the file system When user "Alice" uploads file with content "some content" to "test-folder/sub-folder2/lorem.txt" in the server + And the user force syncs the files And the user waits for the files to sync Then the file "test-folder/sub-folder2/lorem.txt" should not exist on the file system diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index e3d52c5844..6e52f96696 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -281,6 +281,7 @@ def has_sync_pattern(patterns, resource=None): if len(actual_pattern) < pattern_len: break if pattern_len == len(actual_pattern) and pattern == actual_pattern: + print("MATCHED SYNC PATTERN:", pattern) return True # 100 milliseconds polling interval time.sleep(0.1) From 1f0c7ee465c17c9c984c1faa9b533593902d8c0a Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 13:05:24 +0545 Subject: [PATCH 52/75] test: retry folder expansion Signed-off-by: Saw-jan --- test/gui/pageObjects/SyncConnectionWizard.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 9c497dc6b1..60a3c36e14 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -214,6 +214,11 @@ def select_or_unselect_folders_to_sync(folders, select=True): if p_element.get_attribute("checked") == 'true': parent_element = p_element parent_element.native_double_click() # expand the folder + # retry once if the folder is not expanded + if parent_element.is_selected(): + # expand using space key + parent_element.native_click() + parent_element.native_send_keys(Keys.SPACE) folder_element = app().find_element(By.NAME, target_folder) is_checked = folder_element.get_attribute("checked") From 6f400169639bf92ed37916012e3d1161a1d401f4 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 13:05:39 +0545 Subject: [PATCH 53/75] test: check if account is active Signed-off-by: Saw-jan --- test/gui/pageObjects/Toolbar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 243dd75220..2e53e2ce9f 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -62,6 +62,9 @@ def open_account(username): # account_tab.click() account_tab.send_keys(Keys.TAB) account_tab.send_keys(Keys.ENTER) + # confirm account is active + if account_tab.get_attribute("checked") != "true": + raise ValueError(f"Account is not active: {username}") @staticmethod def get_displayed_account_text(displayname, host): From 8f1326302b1264dd2023a7b3d2c8bb95c0f3e832 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 14:20:15 +0545 Subject: [PATCH 54/75] test: check sync pattern with force sync for root folder Signed-off-by: Saw-jan --- test/gui/helpers/SyncHelper.py | 81 ++++++++++++++------ test/gui/pageObjects/SyncConnectionWizard.py | 1 + test/gui/steps/file_context.py | 1 - test/gui/steps/sync_context.py | 2 +- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 6e52f96696..ae51310d87 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -5,6 +5,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.common.exceptions import TimeoutException +from pageObjects.SyncConnection import SyncConnection from helpers.ConfigHelper import get_config, is_linux, is_windows from helpers.FilesHelper import sanitize_path @@ -78,30 +79,41 @@ [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']], # when syncing an account that has some files/folders [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], - ], - 'root_synced': [ + # initial root sync [ - SYNC_STATUS['SYNC'], - SYNC_STATUS['OK'], SYNC_STATUS['OK'], SYNC_STATUS['OK'], SYNC_STATUS['UPDATE'], ], + ], + 'root_synced': [ [ - SYNC_STATUS['SYNC'], - SYNC_STATUS['UPDATE'], - SYNC_STATUS['OK'], - SYNC_STATUS['OK'], - SYNC_STATUS['OK'], - SYNC_STATUS['UPDATE'], - ], - # used for local resource creation and deletion - [ - SYNC_STATUS['OKAL'], SYNC_STATUS['OK'], SYNC_STATUS['OK'], SYNC_STATUS['UPDATE'], ], + # [ + # SYNC_STATUS['SYNC'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['UPDATE'], + # ], + # [ + # SYNC_STATUS['SYNC'], + # SYNC_STATUS['UPDATE'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['UPDATE'], + # ], + # # used for local resource creation and deletion + # [ + # SYNC_STATUS['OKAL'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['OK'], + # SYNC_STATUS['UPDATE'], + # ], ], 'single_synced': [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], 'error': [SYNC_STATUS['ERROR']], @@ -226,20 +238,44 @@ def get_current_sync_status(resource, resource_type): return messages[-1] -def wait_for_resource_to_sync(resource, resource_type='FOLDER', patterns=None): +def wait_for_resource_to_sync( + resource, resource_type='FOLDER', patterns=None, force_sync=False +): listen_sync_status_for_item(resource, resource_type) + initial_timeout = 0 timeout = get_config('maxSyncTimeout') * 1000 if patterns is None: patterns = get_synced_pattern(resource) + if force_sync: + initial_timeout = 5000 + # 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') + SyncConnection.force_sync() + else: + clear_socket_messages(resource) + return + synced = wait_for( lambda: has_sync_pattern(patterns, resource), - timeout, + timeout - initial_timeout, ) + messages = read_and_update_socket_messages() + messages = filter_messages_for_item(messages, resource) clear_socket_messages(resource) - if not synced: + if synced: + return + elif not force_sync: # if the sync pattern doesn't match then check the last sync status # and pass the step if the last sync status is STATUS:OK status = get_current_sync_status(resource, resource_type) @@ -251,11 +287,11 @@ def wait_for_resource_to_sync(resource, resource_type='FOLDER', patterns=None): + '. So passing the step.' ) return - raise TimeoutError( - 'Timeout while waiting for sync to complete for ' - + str(timeout) - + ' milliseconds' - ) + raise TimeoutError( + 'Timeout while waiting for sync to complete for ' + + str(timeout) + + ' milliseconds' + ) def wait_for_initial_sync_to_complete(path): @@ -263,6 +299,7 @@ def wait_for_initial_sync_to_complete(path): path, 'FOLDER', get_initial_sync_patterns(), + True, ) diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 60a3c36e14..3ba55b9f4d 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -216,6 +216,7 @@ def select_or_unselect_folders_to_sync(folders, select=True): parent_element.native_double_click() # expand the folder # retry once if the folder is not expanded if parent_element.is_selected(): + 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) diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 4cb5605034..48d870229a 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -282,7 +282,6 @@ def step(context, user, resource, content): @When('the user deletes the {resource_type:ResourceType} "{resource_name}"') def step(context, resource_type, resource_name): wait_for_client_to_be_ready() - print(f"Deleting {resource_type} '{resource_name}'") deleteResource(resource_name, resource_type) diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 92f4b21bb7..d26998a399 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -37,7 +37,7 @@ def step(context): @When('the user waits for the files to sync') def step(context): - wait_for_resource_to_sync(get_resource_path('/')) + wait_for_resource_to_sync(get_resource_path('/'), force_sync=True) @When('the user waits for {resource_type:ResourceType} "{resource}" to be synced') From 3fc54d967f4d126b7462e1f2a4a61857a6085a70 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 13 May 2026 18:01:48 +0545 Subject: [PATCH 55/75] test: check sync using file path Signed-off-by: Saw-jan --- .../delete-files-folders/delete.feature | 4 ++-- .../sync-resources/syncResources.feature | 5 +--- test/gui/steps/file_context.py | 23 +++++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/gui/features/delete-files-folders/delete.feature b/test/gui/features/delete-files-folders/delete.feature index 5e70c2629b..8136293ce0 100644 --- a/test/gui/features/delete-files-folders/delete.feature +++ b/test/gui/features/delete-files-folders/delete.feature @@ -11,7 +11,7 @@ Feature: deleting files and folders Given user "Alice" has uploaded file with content "openCloud test text file 0" to "" in the server And user "Alice" has set up a client with default settings When the user deletes the file "" - And the user waits for the files to sync + And the user waits for file "" to be synced Then as "Alice" file "" should not exist in the server Examples: | fileName | @@ -24,7 +24,7 @@ Feature: deleting files and folders Given user "Alice" has created folder "" in the server And user "Alice" has set up a client with default settings When the user deletes the folder "" - And the user waits for the files to sync + And the user waits for folder "" to be synced Then as "Alice" file "" should not exist in the server Examples: | folderName | diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 80a5273e32..8f96fd6f1d 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -109,9 +109,6 @@ Feature: Syncing files Then the file "simple-folder/lorem.txt" should exist on the file system And the file "large-folder/lorem.txt" should not exist on the file system And as "Alice" file "simple-folder/localFile.txt" should exist in the server - When the user deletes the folder "simple-folder" - And the user waits for the files to sync - Then as "Alice" folder "simple-folder" should not exist in the server @issue-9733 @skipOnWindows Scenario: sort folders list by name and size @@ -580,7 +577,7 @@ Feature: Syncing files When the user unselects the following folders to sync in "Choose what to sync" window: | folder | | test-folder/sub-folder2 | - And the user waits for the files to sync + And the user waits for folder "test-folder/sub-folder2" to be synced Then the folder "test-folder/sub-folder1" should exist on the file system And the folder "test-folder/sub-folder2" should not exist on the file system When user "Alice" uploads file with content "some content" to "test-folder/sub-folder2/lorem.txt" in the server diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 48d870229a..e99f38130c 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -137,6 +137,8 @@ def move_resource(username, resource_type, source, destination, is_temp_folder=F def deleteResource(resource, resource_type): + wait_for_client_to_be_ready() + listen_sync_status_for_item(resource, resource_type) resource_path = sanitize_path(get_resource_path(resource)) if resource_type == 'file': os.remove(resource_path) @@ -175,7 +177,9 @@ def step(context, resource_type, resource_name, destination_dir): copy_resource(resource_type, resource_name, destination_dir, False) -@When('the user copies {resource_type:ResourceType} "{resource_name}" into the same directory') +@When( + 'the user copies {resource_type:ResourceType} "{resource_name}" into the same directory' +) def step(context, resource_type, resource_name): copy_resource(resource_type, resource_name, resource_name, False) @@ -187,17 +191,19 @@ def step(context, source, destination): rename_file_folder(source, destination) -@Then('the file "{file_path}" should exist on the file system with the following content') +@Then( + 'the file "{file_path}" should exist on the file system with the following content' +) def step(context, file_path): expected = context.text file_path = get_resource_path(file_path) with open(file_path, 'r', encoding='utf-8') as f: contents = f.read() with ensure( - '{0} expected to exist with content "{1}" but has content "{2}"', - file_path, - expected, - contents, + '{0} expected to exist with content "{1}" but has content "{2}"', + file_path, + expected, + contents, ): contents.should.equal(expected) @@ -281,17 +287,14 @@ def step(context, user, resource, content): @When('the user deletes the {resource_type:ResourceType} "{resource_name}"') def step(context, resource_type, resource_name): - wait_for_client_to_be_ready() deleteResource(resource_name, resource_type) @When('user "|any|" creates the following files inside the sync folder:') def step(context, username): - wait_for_client_to_be_ready() - for row in context.table[1:]: file = get_resource_path(row[0], username) - write_file(file, '') + wait_and_write_file(file, '') @Given('the user has created a folder "|any|" in temp folder') From 421faefd3d5744894b2822d810f12203fde181b4 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 14 May 2026 13:12:32 +0545 Subject: [PATCH 56/75] test: clear sync messages after each scenario Signed-off-by: Saw-jan --- test/gui/environment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/gui/environment.py b/test/gui/environment.py index cf062d3017..d1b936be1e 100644 --- a/test/gui/environment.py +++ b/test/gui/environment.py @@ -12,6 +12,7 @@ from helpers.ConfigHelper import get_config from helpers.FilesHelper import prefix_path_namespace, cleanup_created_paths from helpers.AppHelper import close_and_kill_app +from helpers.SyncHelper import clear_socket_messages from step_types.types import * # register all step types @@ -93,3 +94,4 @@ def after_scenario(context, scenario): append_scenario_to_app_log(scenario) store_app_log() cleanup_app_log() + clear_socket_messages() From 3ed7c1c72b6b9b9936761bc8a39478b660aa4fff Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Thu, 14 May 2026 14:11:53 +0545 Subject: [PATCH 57/75] test: raise exception if multiple elements are found Signed-off-by: Saw-jan --- test/gui/helpers/AppHelper.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/gui/helpers/AppHelper.py b/test/gui/helpers/AppHelper.py index 2bdba52136..4d26ccfe95 100644 --- a/test/gui/helpers/AppHelper.py +++ b/test/gui/helpers/AppHelper.py @@ -1,7 +1,9 @@ import pyautogui import psutil +import threading from appium.webdriver import Remote, WebElement from appium.options.common.base import AppiumOptions +from selenium.common.exceptions import WebDriverException, NoSuchElementException from helpers.ConfigHelper import get_config, get_app_env from helpers.ElementHelper import get_element_center_xy @@ -22,10 +24,36 @@ def native_send_keys(self, key): pyautogui.press(get_key(key)) +def find_element(self, by, selector): + """ + Returns a visible element. + Throws if no elements are found or if multiple visible elements are found. + """ + elements = self.find_elements(by, selector) + elements_count = len(elements) + if elements_count > 1: + visible_elements = [el for el in elements if el.is_displayed()] + if len(visible_elements) == 1: + return visible_elements.pop() + raise WebDriverException( + f'Found {elements_count} elements using "{by}={selector}"' + ) + if elements_count == 0: + raise NoSuchElementException(f'No element found for "{by}={selector}"') + return elements[0] + + +def pause(self): + threading.Event().wait() + + # bind custom element methods +Remote.find_element = find_element +Remote.pause = pause WebElement.native_click = native_click WebElement.native_double_click = native_double_click WebElement.native_send_keys = native_send_keys +WebElement.find_element = find_element app_driver = None From 37e876929d739ff37bd6c900a54c829d31f91940 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Fri, 15 May 2026 13:08:02 +0545 Subject: [PATCH 58/75] test: fix multiple elements Signed-off-by: Saw-jan --- test/gui/pageObjects/AccountConnectionWizard.py | 7 +++++-- test/gui/pageObjects/Toolbar.py | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/test/gui/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py index bdbc3dcad3..bef9c4e35e 100644 --- a/test/gui/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -63,10 +63,13 @@ def add_server(server_url): @staticmethod def accept_certificate(): - app().find_element( + buttons = app().find_elements( AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.by, AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.selector, - ).click() + ) + # click the last button + last_button = buttons.pop() + last_button.click() @staticmethod def add_user_credentials(username, password): diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 2e53e2ce9f..3770933869 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -12,7 +12,9 @@ class Toolbar: TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") - ACTIVITY_BUTTON = SimpleNamespace(by=By.NAME, selector="Activity") + ACTIVITY_BUTTON = SimpleNamespace( + by=By.CLASS_NAME, selector="[page tab | Activity]" + ) SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) QUIT_BUTTON = SimpleNamespace(by=By.NAME, selector="Quit") CONFIRM_QUIT_BUTTON = SimpleNamespace( @@ -41,9 +43,16 @@ def has_item(item_name, timeout=get_config("minSyncTimeout") * 1000): @staticmethod def open_activity(): - app().find_element( + tab = app().find_element( Toolbar.ACTIVITY_BUTTON.by, Toolbar.ACTIVITY_BUTTON.selector - ).click() + ) + # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 + # Cannot select navigation tab by click event + # Select the navigation tab using keyboard events as a workaround + # TODO: Remove the workaround and uncomment 'click' action + # account_tab.click() + tab.send_keys(Keys.TAB) + tab.send_keys(Keys.ENTER) @staticmethod def open_new_account_setup(): From 4089b4436efb349dab296ff9501281115cd9713b Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Fri, 15 May 2026 14:02:11 +0545 Subject: [PATCH 59/75] test: fix sync activity checks Signed-off-by: Saw-jan --- test/gui/features/activity/activity.feature | 4 ++ test/gui/pageObjects/Activity.py | 58 +++++++++++++++++---- test/gui/steps/sync_context.py | 36 +++++++++++-- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index 8b32544265..742a920dac 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -8,6 +8,7 @@ Feature: filter activity for user Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server + And user "Brian" has created folder "brian-folder" in the server And the user has set up the following accounts with default settings: | users | | Alice | @@ -18,6 +19,9 @@ Feature: filter activity for user Then the following activities should be displayed in synced table | resource | action | account | | simple-folder | Downloaded | Alice Hansen@%local_server_hostname% | + But the following activities should not be displayed in synced table + | resource | action | account | + | brian-folder | Downloaded | Alice Hansen@%local_server_hostname% | @skipOnWindows Scenario: filter not synced activities (Linux only) diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 54bfb832fa..0cdb944b74 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -1,6 +1,7 @@ # from objectmaphelper import RegularExpression from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By +from selenium.common.exceptions import NoSuchElementException from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config @@ -9,11 +10,14 @@ class Activity: TAB_CONTAINER = SimpleNamespace(by=None, selector=None) - SUBTAB_CONTAINER = SimpleNamespace(by=By.XPATH, selector="//*[@name='{tab_name}']") + SUBTAB_CONTAINER = SimpleNamespace( + by=By.CLASS_NAME, selector="[page tab | {tab_name}]" + ) NOT_SYNCED_TABLE = SimpleNamespace(by=None, selector=None) LOCAL_ACTIVITY_FILTER_BUTTON = SimpleNamespace(by=By.NAME, selector="Filter") - SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR = SimpleNamespace(by=By.NAME, selector=None) - SYNCED_ACTIVITY_TABLE = SimpleNamespace(by=None, selector=None) + LOCAL_ACTIVITY_FILTER_OPTION_SELECTOR = SimpleNamespace(by=By.NAME, selector=None) + LOCAL_ACTIVITY_TABLE = SimpleNamespace(by=By.NAME, selector="Local activity table") + FILTER_BUTTON_SELECTED_STATE = SimpleNamespace(by=By.NAME, selector="1 Filter") NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=None, selector=None) NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) @@ -91,15 +95,25 @@ def select_synced_filter(sync_filter): Activity.LOCAL_ACTIVITY_FILTER_BUTTON.by, Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector, ).click() - app().find_element( - Activity.SYNCED_ACTIVITY_FILTER_OPTION_SELECTOR.by, sync_filter + container = app().find_element( + Activity.LOCAL_ACTIVITY_TABLE.by, Activity.LOCAL_ACTIVITY_TABLE.selector ) + # NOTE: clicking filter options does not work + container.find_element( + Activity.LOCAL_ACTIVITY_FILTER_OPTION_SELECTOR.by, sync_filter + ).click() + # FIXME: enable the check below once the filter options are clickable + # confirm filter is applied + # app().find_element( + # Activity.FILTER_BUTTON_SELECTED_STATE.by, + # Activity.FILTER_BUTTON_SELECTED_STATE.selector, + # ) @staticmethod def get_synced_file_selector(resource): return { "column": Activity.get_synced_table_column_number_by_name("File"), - "container": Activity.SYNCED_ACTIVITY_TABLE, + "container": Activity.LOCAL_ACTIVITY_TABLE, "text": resource, "type": "QModelIndex", } @@ -116,10 +130,34 @@ def get_synced_table_column_number_by_name(column_name): )["section"] @staticmethod - def check_synced_table(resource, action, account): - app().find_element(By.NAME, resource) - app().find_element(By.NAME, action) - app().find_element(By.NAME, account) + def has_activity(resource, action, account): + try: + row = app().find_element(By.NAME, resource) + row_y = row.rect['y'] + # check other properties using current row position + action_cells = app().find_elements(By.NAME, action) + found_action_cell = False + for action_el in action_cells: + if action_el.rect['y'] == row_y: + found_action_cell = True + break + if not found_action_cell: + raise NoSuchElementException( + f'Activity for "{resource}" does not have "{action}" action' + ) + account_cells = app().find_elements(By.NAME, account) + found_account_cell = False + for account_el in account_cells: + if account_el.rect['y'] == row_y: + found_account_cell = True + break + if not found_account_cell: + raise NoSuchElementException( + f'Activity for "{resource}" does not have "{account}" account label' + ) + return True + except: + return False @staticmethod def select_not_synced_filter(filter_option): diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index d26998a399..6d8939100c 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -18,6 +18,7 @@ get_resource_path, ) from helpers.FilesHelper import convert_path_separators_for_os +from helpers.TableParser import table_hashes @Given('the user has paused the file sync') @@ -264,11 +265,36 @@ def step(context, account): @Then('the following activities should be displayed in synced table') def step(context): - for row in context.table: - resource = row[0] - action = row[1] - account = substitute_inline_codes(row[2]) - Activity.check_synced_table(resource, action, account) + activities = table_hashes(context.table) + for activity in activities: + activity["account"] = substitute_inline_codes(activity["account"]) + has_activity = Activity.has_activity( + activity["resource"], activity["action"], activity["account"] + ) + with ensure( + 'Activity should exist: {0} | {1} | {2}', + activity["resource"], + activity["action"], + activity["account"], + ): + has_activity.should.be.true + + +@Then('the following activities should not be displayed in synced table') +def step(context): + activities = table_hashes(context.table) + for activity in activities: + activity["account"] = substitute_inline_codes(activity["account"]) + has_activity = Activity.has_activity( + activity["resource"], activity["action"], activity["account"] + ) + with ensure( + 'Activity should not exist: {0} | {1} | {2}', + activity["resource"], + activity["action"], + activity["account"], + ): + has_activity.should.be.false @Then( From 8fb0395601198a84de2fa50369e0dca8d20c16cf Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Fri, 15 May 2026 14:23:23 +0545 Subject: [PATCH 60/75] test: fix multiple elements Signed-off-by: Saw-jan --- test/gui/features/activity/activity.feature | 4 +-- test/gui/helpers/SyncHelper.py | 9 ++++--- test/gui/pageObjects/AccountSetting.py | 13 ++++----- test/gui/pageObjects/Activity.py | 2 +- test/gui/pageObjects/SyncConnection.py | 4 +-- test/gui/pageObjects/SyncConnectionWizard.py | 7 ++++- test/gui/pageObjects/Toolbar.py | 28 +++++++++++++------- 7 files changed, 40 insertions(+), 27 deletions(-) diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index 742a920dac..7f9db44c5b 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -3,7 +3,7 @@ Feature: filter activity for user I want to filter activity So that I can view activity of specific user - @smoke + @smoke @skip Scenario: filter synced activities Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes @@ -21,7 +21,7 @@ Feature: filter activity for user | simple-folder | Downloaded | Alice Hansen@%local_server_hostname% | But the following activities should not be displayed in synced table | resource | action | account | - | brian-folder | Downloaded | Alice Hansen@%local_server_hostname% | + | brian-folder | Downloaded | Brian Murphy@%local_server_hostname% | @skipOnWindows Scenario: filter not synced activities (Linux only) diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index ae51310d87..0aad8937fb 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -75,10 +75,10 @@ SYNC_STATUS['UPDATE'], ], # when syncing empty account (hidden files are ignored) - [SYNC_STATUS['UPDATE'], SYNC_STATUS['OK']], - [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']], + # [SYNC_STATUS['UPDATE'], SYNC_STATUS['OK']], + # [SYNC_STATUS['UPDATE'], SYNC_STATUS['OKAL']], # when syncing an account that has some files/folders - [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], + # [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], # initial root sync [ SYNC_STATUS['OK'], @@ -270,6 +270,9 @@ def wait_for_resource_to_sync( lambda: has_sync_pattern(patterns, resource), timeout - initial_timeout, ) + # import time + + # time.sleep(30) messages = read_and_update_socket_messages() messages = filter_messages_for_item(messages, resource) clear_socket_messages(resource) diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 50fca8a719..7733019a3a 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -66,15 +66,12 @@ def login(): @staticmethod def get_account_connection_label(): - label = ( - app() - .find_element( - AccountSetting.ACCOUNT_CONNECTION_LABEL.by, - AccountSetting.ACCOUNT_CONNECTION_LABEL.selector, - ) - .text + labels = app().find_elements( + AccountSetting.ACCOUNT_CONNECTION_LABEL.by, + AccountSetting.ACCOUNT_CONNECTION_LABEL.selector, ) - return label + # first label is the sync status label + return labels[0].text @staticmethod def is_connecting(): diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 0cdb944b74..e4f2032bb8 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -86,7 +86,7 @@ def has_sync_status(filename, status): try: app().find_element(Activity.SYNCED_ACTIVITY_STATUS.by, status) return True - except: + except NoSuchElementException: return False @staticmethod diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index c4bf004b9c..1693c3bd2e 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.webdriver.common.keys import Keys +from selenium.common.exceptions import NoSuchElementException from helpers.ConfigHelper import get_config from helpers.AppHelper import app @@ -93,7 +93,7 @@ def has_sync_connection(sync_folder): ), ) return True - except: + except NoSuchElementException: return False @staticmethod diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 3ba55b9f4d..80c53dd984 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -71,7 +71,10 @@ def select_remote_destination_folder(folder): @staticmethod def deselect_all_remote_folders(): - element = app().find_element(By.NAME, "Add Space") + element = app().find_element( + SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.by, + SyncConnectionWizard.ADD_SYNC_CONNECTION_BUTTON.selector, + ) element.send_keys(Keys.ARROW_DOWN) element.native_send_keys(Keys.SPACE) # uncheck the root folder @@ -220,6 +223,8 @@ def select_or_unselect_folders_to_sync(folders, select=True): # expand using space key parent_element.native_click() parent_element.native_send_keys(Keys.SPACE) + if parent_element.is_selected(): + raise AssertionError(f'Failed to expand folder: {parent}') folder_element = app().find_element(By.NAME, target_folder) is_checked = folder_element.get_attribute("checked") diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 3770933869..177793c675 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -2,6 +2,7 @@ from urllib.parse import urlparse from appium.webdriver.common.appiumby import AppiumBy as By from selenium.webdriver.common.keys import Keys +from selenium.common.exceptions import NoSuchElementException from helpers.AppHelper import app from helpers.ConfigHelper import get_config @@ -10,16 +11,18 @@ class Toolbar: TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) - ACCOUNT_BUTTON = SimpleNamespace(by=None, selector=None) - ADD_ACCOUNT_BUTTON = SimpleNamespace(by=By.NAME, selector="Add Account") + ACCOUNT_BUTTON = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | {text}]") + ADD_ACCOUNT_BUTTON = SimpleNamespace( + by=By.CLASS_NAME, selector="[push button | Add Account]" + ) ACTIVITY_BUTTON = SimpleNamespace( by=By.CLASS_NAME, selector="[page tab | Activity]" ) SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) - QUIT_BUTTON = SimpleNamespace(by=By.NAME, selector="Quit") + QUIT_BUTTON = SimpleNamespace(by=By.CLASS_NAME, selector="[push button | Quit]") CONFIRM_QUIT_BUTTON = SimpleNamespace( - by=By.ACCESSIBILITY_ID, - selector="QApplication.QMessageBox.qt_msgbox_buttonbox.QPushButton", + by=By.NAME, + selector="Yes", ) TOOLBAR_ITEMS = ["Add Account", "Activity", "Settings", "Quit"] @@ -50,9 +53,11 @@ def open_activity(): # Cannot select navigation tab by click event # Select the navigation tab using keyboard events as a workaround # TODO: Remove the workaround and uncomment 'click' action - # account_tab.click() + tab.click() tab.send_keys(Keys.TAB) tab.send_keys(Keys.ENTER) + if tab.get_attribute("checked") != "true": + raise AssertionError("Activity tab is not active") @staticmethod def open_new_account_setup(): @@ -68,12 +73,12 @@ def open_account(username): # Cannot activate account tab by click event # Select the account tab using keyboard events as a workaround # TODO: Remove the workaround and uncomment 'click' action - # account_tab.click() + account_tab.click() account_tab.send_keys(Keys.TAB) account_tab.send_keys(Keys.ENTER) # confirm account is active if account_tab.get_attribute("checked") != "true": - raise ValueError(f"Account is not active: {username}") + raise AssertionError(f"Account is not active: {username}") @staticmethod def get_displayed_account_text(displayname, host): @@ -125,8 +130,11 @@ def get_account(username): account_label = f"{display_name}@{server_host}" account = None try: - account = app().find_element(By.NAME, account_label) - except: + account = app().find_element( + Toolbar.ACCOUNT_BUTTON.by, + Toolbar.ACCOUNT_BUTTON.selector.format(text=account_label), + ) + except NoSuchElementException: pass return account From 50e999acd98aeee24e550af5c22668c906d444a2 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Fri, 15 May 2026 17:40:35 +0545 Subject: [PATCH 61/75] test: wait for toolbar to be enabled Signed-off-by: Saw-jan --- test/gui/helpers/SyncHelper.py | 2 -- test/gui/pageObjects/Toolbar.py | 17 +++++++++++++++++ test/gui/steps/account_context.py | 4 ++++ test/gui/steps/spaces_context.py | 2 ++ test/gui/steps/sync_context.py | 1 + 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 0aad8937fb..e78c1279ea 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -270,9 +270,7 @@ def wait_for_resource_to_sync( lambda: has_sync_pattern(patterns, resource), timeout - initial_timeout, ) - # import time - # time.sleep(30) messages = read_and_update_socket_messages() messages = filter_messages_for_item(messages, resource) clear_socket_messages(resource) diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 177793c675..690aa982c6 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -7,10 +7,14 @@ from helpers.AppHelper import app from helpers.ConfigHelper import get_config from helpers.UserHelper import get_displayname_for_user +from helpers.SyncHelper import wait_for class Toolbar: TOOLBAR_ROW = SimpleNamespace(by=None, selector=None) + NAVIGATION_BAR = SimpleNamespace( + by=By.XPATH, selector="//*[@name='Navigation bar']/.." + ) ACCOUNT_BUTTON = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | {text}]") ADD_ACCOUNT_BUTTON = SimpleNamespace( by=By.CLASS_NAME, selector="[push button | Add Account]" @@ -27,6 +31,19 @@ class Toolbar: TOOLBAR_ITEMS = ["Add Account", "Activity", "Settings", "Quit"] + @staticmethod + def wait_toolbar_enabled(): + toolbar = app().find_element( + Toolbar.NAVIGATION_BAR.by, Toolbar.NAVIGATION_BAR.selector + ) + timeout = get_config('maxSyncTimeout') * 1000 + enabled = wait_for( + lambda: toolbar.is_enabled(), + timeout, + ) + if not enabled: + raise AssertionError(f"Toolbar is not enabled within {timeout} ms") + @staticmethod def get_item_selector(item_name): return { diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index a3c338da58..8b4d27a14f 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -63,6 +63,7 @@ def step(context, username): # wait for files to sync wait_for_initial_sync_to_complete(get_resource_path('/', username)) + Toolbar.wait_toolbar_enabled() @Given('the user has set up the following accounts with default settings:') @@ -86,6 +87,7 @@ def step(context): enter_password.login_after_setup(username, password) # wait for files to sync wait_for_initial_sync_to_complete(sync_paths[username]) + Toolbar.wait_toolbar_enabled() @When('the user starts the client') @@ -105,6 +107,7 @@ def step(context): AccountConnectionWizard.add_account(account_details) # # wait for files to sync wait_for_initial_sync_to_complete(get_resource_path('/', account_details['user'])) + Toolbar.wait_toolbar_enabled() @Given('the user has entered the following account information:') @@ -143,6 +146,7 @@ def step(context, username): # wait for files to sync wait_for_initial_sync_to_complete(get_resource_path('/', username)) + Toolbar.wait_toolbar_enabled() @When('user "|any|" opens login dialog') diff --git a/test/gui/steps/spaces_context.py b/test/gui/steps/spaces_context.py index d5d8c2e980..5d7861b814 100644 --- a/test/gui/steps/spaces_context.py +++ b/test/gui/steps/spaces_context.py @@ -1,6 +1,7 @@ from sure import ensure from pageObjects.EnterPassword import EnterPassword +from pageObjects.Toolbar import Toolbar from helpers.UserHelper import get_password_for_user from helpers.SetupClientHelper import setup_client, get_resource_path from helpers.SyncHelper import wait_for_initial_sync_to_complete @@ -49,6 +50,7 @@ def step(context, user, space_name): enter_password.login_after_setup(user, password) # wait for files to sync wait_for_initial_sync_to_complete(get_resource_path('/', user, space_name)) + Toolbar.wait_toolbar_enabled() @Then( diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 6d8939100c..87c9f6f3c2 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -45,6 +45,7 @@ def step(context): def step(context, resource_type, resource): resource = get_resource_path(resource) wait_for_resource_to_sync(convert_path_separators_for_os(resource), resource_type) + Toolbar.wait_toolbar_enabled() @When(r'the user waits for (file|folder) "([^"]*)" to have sync error', regexp=True) From dcfad712a487e34dee93f464f8aeef1d3ff4425a Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Mon, 18 May 2026 14:16:35 +0545 Subject: [PATCH 62/75] test: add sync pattern for resource deletion Signed-off-by: Saw-jan --- test/gui/helpers/SyncHelper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index e78c1279ea..09796aea90 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -115,7 +115,11 @@ # SYNC_STATUS['UPDATE'], # ], ], - 'single_synced': [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], + 'single_synced': [ + [SYNC_STATUS['SYNC'], SYNC_STATUS['OK']], + # file/folder deletion + [SYNC_STATUS['SYNC'], SYNC_STATUS['NOP']], + ], 'error': [SYNC_STATUS['ERROR']], } From 154efe0fc71b586f36093ea57d5c93fb7dbddfe9 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Mon, 18 May 2026 14:16:55 +0545 Subject: [PATCH 63/75] test: fix element coordinates Signed-off-by: Saw-jan --- test/gui/helpers/AppHelper.py | 15 +++++++++++++++ test/gui/pageObjects/Toolbar.py | 12 +++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/test/gui/helpers/AppHelper.py b/test/gui/helpers/AppHelper.py index 4d26ccfe95..d6474890c0 100644 --- a/test/gui/helpers/AppHelper.py +++ b/test/gui/helpers/AppHelper.py @@ -3,6 +3,7 @@ import threading from appium.webdriver import Remote, WebElement from appium.options.common.base import AppiumOptions +from appium.webdriver.common.appiumby import AppiumBy as By from selenium.common.exceptions import WebDriverException, NoSuchElementException from helpers.ConfigHelper import get_config, get_app_env @@ -12,6 +13,11 @@ def native_click(self, **kwargs): x, y = get_element_center_xy(self) + win_x, win_y = get_window_location() + if x < win_x: + x = x + win_x + if y < win_y: + y = y + win_y pyautogui.click(x, y, **kwargs) @@ -97,3 +103,12 @@ def close_and_kill_app(): # Reset driver for reuse app_driver = None + + +def get_window_location(): + window = ( + app() + .find_element(By.XPATH, "//*[contains(@name,'OpenCloud Desktop')]") + .location + ) + return window['x'], window['y'] diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 690aa982c6..24967134c7 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -4,7 +4,7 @@ from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import NoSuchElementException -from helpers.AppHelper import app +from helpers.AppHelper import app, get_window_location from helpers.ConfigHelper import get_config from helpers.UserHelper import get_displayname_for_user from helpers.SyncHelper import wait_for @@ -70,9 +70,8 @@ def open_activity(): # Cannot select navigation tab by click event # Select the navigation tab using keyboard events as a workaround # TODO: Remove the workaround and uncomment 'click' action - tab.click() - tab.send_keys(Keys.TAB) - tab.send_keys(Keys.ENTER) + # tab.click() + tab.native_click() if tab.get_attribute("checked") != "true": raise AssertionError("Activity tab is not active") @@ -90,9 +89,8 @@ def open_account(username): # Cannot activate account tab by click event # Select the account tab using keyboard events as a workaround # TODO: Remove the workaround and uncomment 'click' action - account_tab.click() - account_tab.send_keys(Keys.TAB) - account_tab.send_keys(Keys.ENTER) + # account_tab.click() + account_tab.native_click() # confirm account is active if account_tab.get_attribute("checked") != "true": raise AssertionError(f"Account is not active: {username}") From 578354e72e1ffa48fe94240bfaa7f36d8ab657dd Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 20 May 2026 10:47:53 +0545 Subject: [PATCH 64/75] test(gui): fix and enable activity filter scenario (#910) --- test/gui/features/activity/activity.feature | 2 +- .../sync-resources/syncResources.feature | 2 +- test/gui/pageObjects/Activity.py | 31 ++++++++++--------- test/gui/pageObjects/Toolbar.py | 3 +- test/gui/steps/account_context.py | 3 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index 7f9db44c5b..f8c550584b 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -3,7 +3,7 @@ Feature: filter activity for user I want to filter activity So that I can view activity of specific user - @smoke @skip + @smoke Scenario: filter synced activities Given user "Alice" has been created in the server with default attributes And user "Brian" has been created in the server with default attributes diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 8f96fd6f1d..bd969dffcd 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -220,7 +220,7 @@ Feature: Syncing files """ test content """ - And the user waits for folder "parent/subfolder5/test.txt" to be synced + And the user waits for file "parent/subfolder5/test.txt" to be synced Then as "Alice" folder "parent/subfolderEmpty1" should exist in the server And as "Alice" folder "parent/subfolderEmpty2" should exist in the server And as "Alice" folder "parent/subfolderEmpty3" should exist in the server diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index e4f2032bb8..7abcc64f89 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -1,6 +1,7 @@ # from objectmaphelper import RegularExpression 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 helpers.FilesHelper import build_conflicted_regex @@ -17,7 +18,9 @@ class Activity: LOCAL_ACTIVITY_FILTER_BUTTON = SimpleNamespace(by=By.NAME, selector="Filter") LOCAL_ACTIVITY_FILTER_OPTION_SELECTOR = SimpleNamespace(by=By.NAME, selector=None) LOCAL_ACTIVITY_TABLE = SimpleNamespace(by=By.NAME, selector="Local activity table") - FILTER_BUTTON_SELECTED_STATE = SimpleNamespace(by=By.NAME, selector="1 Filter") + FILTER_BUTTON_SELECTED_STATE = SimpleNamespace( + by=By.XPATH, selector="//*[contains(@name, '1 Filter')]" + ) NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=None, selector=None) NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) @@ -91,23 +94,23 @@ def has_sync_status(filename, status): @staticmethod def select_synced_filter(sync_filter): - app().find_element( + menu = app().find_element( Activity.LOCAL_ACTIVITY_FILTER_BUTTON.by, Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector, - ).click() - container = app().find_element( - Activity.LOCAL_ACTIVITY_TABLE.by, Activity.LOCAL_ACTIVITY_TABLE.selector ) - # NOTE: clicking filter options does not work - container.find_element( - Activity.LOCAL_ACTIVITY_FILTER_OPTION_SELECTOR.by, sync_filter - ).click() - # FIXME: enable the check below once the filter options are clickable + menu.click() + + # NOTE: Filter options are not visible in the accessibility tree. + # As a workaround, select the second filter option (which is an account filter). + # This means we cannot select a specific account filter for now. + menu.send_keys(Keys.ARROW_DOWN) + menu.send_keys(Keys.ARROW_DOWN) + menu.send_keys(Keys.ENTER) # confirm filter is applied - # app().find_element( - # Activity.FILTER_BUTTON_SELECTED_STATE.by, - # Activity.FILTER_BUTTON_SELECTED_STATE.selector, - # ) + app().find_element( + Activity.FILTER_BUTTON_SELECTED_STATE.by, + Activity.FILTER_BUTTON_SELECTED_STATE.selector, + ) @staticmethod def get_synced_file_selector(resource): diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 24967134c7..23ba33bd02 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -1,10 +1,9 @@ from types import SimpleNamespace from urllib.parse import urlparse from appium.webdriver.common.appiumby import AppiumBy as By -from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import NoSuchElementException -from helpers.AppHelper import app, get_window_location +from helpers.AppHelper import app from helpers.ConfigHelper import get_config from helpers.UserHelper import get_displayname_for_user from helpers.SyncHelper import wait_for diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 8b4d27a14f..80b4155bec 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -8,7 +8,6 @@ from pageObjects.AccountSetting import AccountSetting from pageObjects.Toolbar import Toolbar from pageObjects.EnterPassword import EnterPassword -from pageObjects.AccountSetting import AccountSetting from helpers.SetupClientHelper import ( start_client, setup_client, @@ -24,6 +23,7 @@ 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 +from helpers.AppHelper import close_and_kill_app @Given('the user has started the client') @@ -244,6 +244,7 @@ def step(context): @When('the user quits the client') def step(context): Toolbar.quit_opencloud() + close_and_kill_app() @Then('"{username}" account should be opened') From 213b9b168d8e6ac0e30fba0b52f6e3b0197365dc Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 20 May 2026 13:38:04 +0545 Subject: [PATCH 65/75] test(gui): fix and enable settings test scenarios (#911) * test: fix settings tab scenarios Signed-off-by: Saw-jan * test: fix settings tab scenarios Signed-off-by: Saw-jan * test: rename feature files Signed-off-by: Saw-jan * test: check about dialog Signed-off-by: Saw-jan * ci: fix browser install on browser cache miss Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- .woodpecker/cache-python.yaml | 2 + test/gui/features/activity/activity.feature | 6 +- test/gui/features/spaces/spaces.feature | 2 +- .../sync-resources/syncResources.feature | 10 +-- .../{test.feature => tabsSettings.feature} | 7 +- .../vfs/{test.feature => vfs.feature} | 0 test/gui/pageObjects/Settings.py | 85 ++++++++++++++----- test/gui/pageObjects/Toolbar.py | 46 ++++++---- test/gui/steps/sync_context.py | 47 +++++++--- 9 files changed, 142 insertions(+), 63 deletions(-) rename test/gui/features/tabs-settings/{test.feature => tabsSettings.feature} (93%) rename test/gui/features/vfs/{test.feature => vfs.feature} (100%) diff --git a/.woodpecker/cache-python.yaml b/.woodpecker/cache-python.yaml index d1d90924d2..9073e0b5aa 100644 --- a/.woodpecker/cache-python.yaml +++ b/.woodpecker/cache-python.yaml @@ -75,7 +75,9 @@ steps: - . ./.woodpecker.env - if $BROWSER_CACHE_FOUND; then exit 0; fi - cd test/gui/ + - python3 -m venv .venv --system-site-packages - . .venv/bin/activate + - grep "^playwright" requirements.txt | pip install -r /dev/stdin - make install-chromium - tar -czf playwright-browsers.tar.gz .playwright diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index f8c550584b..2b86e6ed7d 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -13,7 +13,7 @@ Feature: filter activity for user | users | | Alice | | Brian | - When the user clicks on the activity tab + When the user opens the activity tab And the user selects "Local Activity" tab in the activity And the user checks the activities of account "Alice Hansen@%local_server_hostname%" Then the following activities should be displayed in synced table @@ -32,7 +32,7 @@ Feature: filter activity for user | files | | /.htaccess | | /Folder1/a\\a.txt | - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Not Synced" tab in the activity Then the file "Folder1/a\\a.txt" should be blacklisted And the file ".htaccess" should be excluded @@ -48,7 +48,7 @@ Feature: filter activity for user When user "Alice" creates the following files inside the sync folder: | files | | /.htaccess | - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Not Synced" tab in the activity Then the file ".htaccess" should be excluded When the user unchecks the "Excluded" filter diff --git a/test/gui/features/spaces/spaces.feature b/test/gui/features/spaces/spaces.feature index 7c155b3d87..4f2bb3267e 100644 --- a/test/gui/features/spaces/spaces.feature +++ b/test/gui/features/spaces/spaces.feature @@ -77,7 +77,7 @@ Feature: Project spaces """ simple-folder: Not allowed because you don't have permission to add subfolders to that folder """ - When the user clicks on the activity tab + 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 | resource | status | account | diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index bd969dffcd..9390ce0efe 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -14,7 +14,7 @@ Feature: Syncing files test content """ And the user waits for file "lorem-for-upload.txt" to be synced - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Local Activity" tab in the activity Then the file "lorem-for-upload.txt" should have status "Uploaded" in the activity tab And as "Alice" the file "lorem-for-upload.txt" should have the content "test content" in the server @@ -45,7 +45,7 @@ Feature: Syncing files And user "Alice" has uploaded file with content "changed server content" to "/conflict.txt" in the server And the user has waited for "5" seconds When the user resumes the file sync on the client - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Not Synced" tab in the activity Then the table of conflict warnings should include file "conflict.txt" And the file "conflict.txt" should exist on the file system with the following content @@ -181,7 +181,7 @@ Feature: Syncing files And user "Alice" has set up a client with default settings When user "Alice" creates a folder "folder with space at end " inside the sync folder And the user force syncs the files - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Not Synced" tab in the activity Then the file "trailing-space.txt " should be ignored And the file "folder with space at end " should be ignored @@ -274,7 +274,7 @@ Feature: Syncing files """ test content """ - And the user clicks on the activity tab + And the user opens the activity tab And the user selects "Not Synced" tab in the activity Then the file "Folder1/a\\a.txt" should exist on the file system And the file "Folder1/a\\a.txt" should be blacklisted @@ -527,7 +527,7 @@ Feature: Syncing files """ And as "Brian" folder "simple-folder/sub-folder" should not exist in the server And as "Brian" file "simple-folder/simple.pdf" should not exist in the server - When the user clicks on the activity tab + 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 | resource | status | account | diff --git a/test/gui/features/tabs-settings/test.feature b/test/gui/features/tabs-settings/tabsSettings.feature similarity index 93% rename from test/gui/features/tabs-settings/test.feature rename to test/gui/features/tabs-settings/tabsSettings.feature index e5c6e5f6a7..378ec8947c 100644 --- a/test/gui/features/tabs-settings/test.feature +++ b/test/gui/features/tabs-settings/tabsSettings.feature @@ -3,7 +3,7 @@ Feature: Visually check all tabs I want to visually check all tabs in client So that I can perform all the actions related to client - + @smoke Scenario: Tabs in toolbar looks correct Given user "Alice" has been created in the server with default attributes And user "Alice" has set up a client with default settings @@ -13,11 +13,11 @@ Feature: Visually check all tabs | Settings | | Quit | - + @smoke Scenario: Verify various setting options in Settings tab Given user "Alice" has been created in the server with default attributes And user "Alice" has set up a client with default settings - When the user clicks on the settings tab + When the user opens the settings tab Then the settings tab should have the following options in the general section: | Start on Login | And the settings tab should have the following options in the advanced section: @@ -25,7 +25,6 @@ Feature: Visually check all tabs | Edit ignored files | | Log settings | And the settings tab should have the following options in the network section: - | Proxy Settings | | Download Bandwidth | | Upload Bandwidth | When the user opens the about dialog diff --git a/test/gui/features/vfs/test.feature b/test/gui/features/vfs/vfs.feature similarity index 100% rename from test/gui/features/vfs/test.feature rename to test/gui/features/vfs/vfs.feature diff --git a/test/gui/pageObjects/Settings.py b/test/gui/pageObjects/Settings.py index 5a1fb79c3f..10141648f3 100644 --- a/test/gui/pageObjects/Settings.py +++ b/test/gui/pageObjects/Settings.py @@ -1,16 +1,36 @@ from types import SimpleNamespace from appium.webdriver.common.appiumby import AppiumBy as By +from helpers.AppHelper import app + class Settings: CHECKBOX_OPTION_ITEM = SimpleNamespace(by=None, selector=None) NETWORK_OPTION_ITEM = SimpleNamespace(by=None, selector=None) - ABOUT_BUTTON = SimpleNamespace(by=None, selector=None) - ABOUT_DIALOG = SimpleNamespace(by=None, selector=None) - ABOUT_DIALOG_OK_BUTTON = SimpleNamespace(by=None, selector=None) - GENERAL_OPTIONS_MAP = SimpleNamespace(by=None, selector=None) - ADVANCED_OPTION_MAP = SimpleNamespace(by=None, selector=None) - NETWORK_OPTION_MAP = SimpleNamespace(by=None, selector=None) + ABOUT_BUTTON = SimpleNamespace(by=By.NAME, selector="About") + ABOUT_DIALOG = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | About]") + ABOUT_DIALOG_OK_BUTTON = SimpleNamespace(by=By.NAME, selector="OK") + GENERAL_SETTING_START_ON_LOGIN = SimpleNamespace( + by=By.XPATH, selector="//panel/*[@name='Start on Login']" + ) + GENERAL_SETTING_LANGUAGE = SimpleNamespace( + by=By.XPATH, selector="//panel/label[@name='Language']" + ) + ADVANCED_SETTING_SYNC_HIDDEN_FILES = SimpleNamespace( + by=By.XPATH, selector="//panel/*[@name='Sync hidden files']" + ) + ADVANCED_SETTING_EDIT_IGNORED_FILES = SimpleNamespace( + by=By.XPATH, selector="//panel/*[@name='Edit Ignored Files']" + ) + ADVANCED_SETTING_LOG_SETTINGS = SimpleNamespace( + by=By.XPATH, selector="//panel/*[@name='Log Settings']" + ) + NETWORK_SETTING_DOWNLOAD_BANDWIDTH = SimpleNamespace( + by=By.XPATH, selector="//panel[@name='Download Bandwidth']" + ) + NETWORK_SETTING_UPLOAD_BANDWIDTH = SimpleNamespace( + by=By.XPATH, selector="//panel[@name='Upload Bandwidth']" + ) @staticmethod def get_checkbox_option_selector(name): @@ -29,28 +49,53 @@ def get_network_option_selector(name): return selector @staticmethod - def check_general_option(option): - selector = Settings.GENERAL_OPTIONS_MAP[option] - squish.waitForObjectExists(Settings.get_checkbox_option_selector(selector)) + def has_general_setting(setting): + if setting.lower() == "start on login": + locator = Settings.GENERAL_SETTING_START_ON_LOGIN + elif setting.lower() == "language": + locator = Settings.GENERAL_SETTING_LANGUAGE + else: + raise ValueError(f"Unknown general setting: {setting}") + return app().find_element(locator.by, locator.selector).is_displayed() @staticmethod - def check_advanced_option(option): - selector = Settings.ADVANCED_OPTION_MAP[option] - squish.waitForObjectExists(Settings.get_checkbox_option_selector(selector)) + def has_advanced_setting(setting): + if setting.lower() == "sync hidden files": + locator = Settings.ADVANCED_SETTING_SYNC_HIDDEN_FILES + elif setting.lower() == "edit ignored files": + locator = Settings.ADVANCED_SETTING_EDIT_IGNORED_FILES + elif setting.lower() == "log settings": + locator = Settings.ADVANCED_SETTING_LOG_SETTINGS + else: + raise ValueError(f"Unknown advanced setting: {setting}") + return app().find_element(locator.by, locator.selector).is_displayed() @staticmethod - def check_network_option(option): - selector = Settings.NETWORK_OPTION_MAP[option] - squish.waitForObjectExists(Settings.get_network_option_selector(selector)) + def has_network_setting(setting): + if setting.lower() == "download bandwidth": + locator = Settings.NETWORK_SETTING_DOWNLOAD_BANDWIDTH + elif setting.lower() == "upload bandwidth": + locator = Settings.NETWORK_SETTING_UPLOAD_BANDWIDTH + else: + raise ValueError(f"Unknown network setting: {setting}") + return app().find_element(locator.by, locator.selector).is_displayed() @staticmethod - def open_about_button(): - squish.clickButton(squish.waitForObject(Settings.ABOUT_BUTTON)) + def open_about_dialog(): + app().find_element( + Settings.ABOUT_BUTTON.by, Settings.ABOUT_BUTTON.selector + ).click() @staticmethod - def wait_for_about_dialog_to_be_visible(): - squish.waitForObjectExists(Settings.ABOUT_DIALOG) + def has_about_dialog(): + return ( + app() + .find_element(Settings.ABOUT_DIALOG.by, Settings.ABOUT_DIALOG.selector) + .is_displayed() + ) @staticmethod def close_about_dialog(): - squish.clickButton(squish.waitForObjectExists(Settings.ABOUT_DIALOG_OK_BUTTON)) + app().find_element( + Settings.ABOUT_DIALOG_OK_BUTTON.by, Settings.ABOUT_DIALOG_OK_BUTTON.selector + ).click() diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 23ba33bd02..515dfae8ac 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -14,14 +14,12 @@ class Toolbar: NAVIGATION_BAR = SimpleNamespace( by=By.XPATH, selector="//*[@name='Navigation bar']/.." ) - ACCOUNT_BUTTON = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | {text}]") + ACCOUNT_TAB = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | {text}]") ADD_ACCOUNT_BUTTON = SimpleNamespace( by=By.CLASS_NAME, selector="[push button | Add Account]" ) - ACTIVITY_BUTTON = SimpleNamespace( - by=By.CLASS_NAME, selector="[page tab | Activity]" - ) - SETTINGS_BUTTON = SimpleNamespace(by=None, selector=None) + ACTIVITY_TAB = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | Activity]") + SETTINGS_TAB = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | Settings]") QUIT_BUTTON = SimpleNamespace(by=By.CLASS_NAME, selector="[push button | Quit]") CONFIRM_QUIT_BUTTON = SimpleNamespace( by=By.NAME, @@ -53,18 +51,22 @@ def get_item_selector(item_name): } @staticmethod - def has_item(item_name, timeout=get_config("minSyncTimeout") * 1000): - try: - squish.waitForObject(Toolbar.get_item_selector(item_name), timeout) - return True - except: - return False + def has_tab(tab_name): + if tab_name.lower() == "add account": + tab = Toolbar.ADD_ACCOUNT_BUTTON + elif tab_name.lower() == "activity": + tab = Toolbar.ACTIVITY_TAB + elif tab_name.lower() == "settings": + tab = Toolbar.SETTINGS_TAB + elif tab_name.lower() == "quit": + tab = Toolbar.QUIT_BUTTON + else: + raise ValueError(f"Unknown tab: {tab_name}") + return app().find_element(tab.by, tab.selector).is_displayed() @staticmethod def open_activity(): - tab = app().find_element( - Toolbar.ACTIVITY_BUTTON.by, Toolbar.ACTIVITY_BUTTON.selector - ) + tab = app().find_element(Toolbar.ACTIVITY_TAB.by, Toolbar.ACTIVITY_TAB.selector) # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 # Cannot select navigation tab by click event # Select the navigation tab using keyboard events as a workaround @@ -104,7 +106,15 @@ def get_displayed_account_text(displayname, host): @staticmethod def open_settings_tab(): - squish.mouseClick(squish.waitForObject(Toolbar.SETTINGS_BUTTON)) + tab = app().find_element(Toolbar.SETTINGS_TAB.by, Toolbar.SETTINGS_TAB.selector) + # ISSUE: https://github.com/opencloud-eu/desktop/pull/879 + # Cannot select navigation tab by click event + # Select the navigation tab using keyboard events as a workaround + # TODO: Remove the workaround and uncomment 'click' action + # tab.click() + tab.native_click() + if tab.get_attribute("checked") != "true": + raise AssertionError("Settings tab is not active") @staticmethod def quit_opencloud(): @@ -127,7 +137,7 @@ def get_accounts(): "initials": str(obj.accountState.account.initials), "current": obj.checked, } - account_locator = Toolbar.ACCOUNT_BUTTON.copy() + account_locator = Toolbar.ACCOUNT_TAB.copy() if account_idx > 1: account_locator.update({"occurrence": account_idx}) account_locator.update({"text": account_info["hostname"]}) @@ -145,8 +155,8 @@ def get_account(username): account = None try: account = app().find_element( - Toolbar.ACCOUNT_BUTTON.by, - Toolbar.ACCOUNT_BUTTON.selector.format(text=account_label), + Toolbar.ACCOUNT_TAB.by, + Toolbar.ACCOUNT_TAB.selector.format(text=account_label), ) except NoSuchElementException: pass diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 87c9f6f3c2..9c4b5ebc81 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -18,7 +18,7 @@ get_resource_path, ) from helpers.FilesHelper import convert_path_separators_for_os -from helpers.TableParser import table_hashes +from helpers.TableParser import table_hashes, table_raw @Given('the user has paused the file sync') @@ -78,11 +78,16 @@ def step(context, item): ) -@When('the user clicks on the activity tab') +@When('the user opens the activity tab') def step(context): Toolbar.open_activity() +@When('the user opens the settings tab') +def step(context): + Toolbar.open_settings_tab() + + @Then('the table of conflict warnings should include file "|any|"') def step(context, filename): Activity.check_file_exist(filename) @@ -112,8 +117,11 @@ def step(context, tab_name): @Then('the toolbar should have the following tabs:') def step(context): - for tab_name in context.table: - Toolbar.has_item(tab_name[0]) + tabs = table_raw(context.table) + for tab_name in tabs: + tab_name = tab_name[0] + with ensure('Tab not found: {0}', tab_name): + Toolbar.has_tab(tab_name).should.be.true @When('the user selects the following folders to sync:') @@ -182,30 +190,45 @@ def step(context, space_name): @Then('the settings tab should have the following options in the general section:') def step(context): - for item in context.table: - Settings.check_general_option(item[0]) + settings = table_raw(context.table) + for setting in settings: + setting = setting[0] + with ensure('General setting not found: {0}', setting): + Settings.has_general_setting(setting).should.be.true @Then('the settings tab should have the following options in the advanced section:') def step(context): - for item in context.table: - Settings.check_advanced_option(item[0]) + settings = table_raw(context.table) + for setting in settings: + setting = setting[0] + with ensure('Advanced setting not found: {0}', setting): + Settings.has_advanced_setting(setting).should.be.true @Then('the settings tab should have the following options in the network section:') def step(context): - for item in context.table: - Settings.check_network_option(item[0]) + settings = table_raw(context.table) + for setting in settings: + setting = setting[0] + with ensure('Network setting not found: {0}', setting): + Settings.has_network_setting(setting).should.be.true @When('the user opens the about dialog') def step(context): - Settings.open_about_button() + Settings.open_about_dialog() @Then('the about dialog should be opened') def step(context): - Settings.wait_for_about_dialog_to_be_visible() + with ensure('About dialog is not opened.'): + Settings.has_about_dialog().should.be.true + + +@When('the user closes the about dialog') +def step(context): + Settings.close_about_dialog() @When('the user adds the folder sync connection') From 7bbdd0eb8a3b683698018788adee5ee9dea964c8 Mon Sep 17 00:00:00 2001 From: Sawjan Gurung Date: Wed, 20 May 2026 16:56:05 +0545 Subject: [PATCH 66/75] test(gui): select folders based on the parent position (#912) * test: fix double click at element position Signed-off-by: Saw-jan * test: select folders based on the parent position Signed-off-by: Saw-jan --------- Signed-off-by: Saw-jan --- test/gui/helpers/AppHelper.py | 5 ++++ test/gui/pageObjects/SyncConnectionWizard.py | 26 ++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/test/gui/helpers/AppHelper.py b/test/gui/helpers/AppHelper.py index d6474890c0..821ad4062e 100644 --- a/test/gui/helpers/AppHelper.py +++ b/test/gui/helpers/AppHelper.py @@ -23,6 +23,11 @@ def native_click(self, **kwargs): def native_double_click(self, **kwargs): x, y = get_element_center_xy(self) + win_x, win_y = get_window_location() + if x < win_x: + x = x + win_x + if y < win_y: + y = y + win_y pyautogui.doubleClick(x, y, **kwargs) diff --git a/test/gui/pageObjects/SyncConnectionWizard.py b/test/gui/pageObjects/SyncConnectionWizard.py index 80c53dd984..e58917a7bf 100644 --- a/test/gui/pageObjects/SyncConnectionWizard.py +++ b/test/gui/pageObjects/SyncConnectionWizard.py @@ -208,14 +208,18 @@ def select_or_unselect_folders_to_sync(folders, select=True): target_folder = parents.pop() parent_element = None + parent_position = 0 for parent in parents: - if parent_element: - p_elements = parent_element.find_elements(By.NAME, parent) - else: - p_elements = app().find_elements(By.NAME, parent) + p_elements = app().find_elements(By.NAME, parent) + # select nested folders based on the position of the parent folder for p_element in p_elements: - if p_element.get_attribute("checked") == 'true': + if ( + p_element.get_attribute("checked") == 'true' + and p_element.rect["x"] > parent_position + ): parent_element = p_element + parent_position = p_element.rect["x"] + break parent_element.native_double_click() # expand the folder # retry once if the folder is not expanded if parent_element.is_selected(): @@ -226,14 +230,22 @@ def select_or_unselect_folders_to_sync(folders, select=True): if parent_element.is_selected(): raise AssertionError(f'Failed to expand folder: {parent}') - folder_element = app().find_element(By.NAME, target_folder) + 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") # return early if the folder is already in the expected state. if is_checked == expected_state: return folder_element.native_click() - folder_element.native_send_keys(Keys.SPACE) # select the folder + if not folder_element.is_selected(): + raise AssertionError(f"Failed to focus folder: {target_folder}") + folder_element.native_send_keys(Keys.SPACE) # toggle the folder selection is_checked = folder_element.get_attribute("checked") if is_checked != expected_state: From e8e77d7c2960c1e5a39f67f6ea15ebdb991a3ea7 Mon Sep 17 00:00:00 2001 From: Asmita Paudel <94975243+Asmitapaudel@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:52:59 +0545 Subject: [PATCH 67/75] test(ci): add @skip tag to exclude scenario from CI execution (#926) --- .woodpecker/ui-tests.yaml | 2 +- test/gui/features/activity/activity.feature | 4 +- test/gui/features/add-account/account.feature | 6 +-- .../delete-files-folders/delete.feature | 4 +- test/gui/features/edit-files/edit.feature | 2 +- .../moveFilesFolders.feature | 8 ++-- .../removeAccountConnection.feature | 2 +- test/gui/features/spaces/spaces.feature | 4 +- .../sync-resources/syncResources.feature | 38 +++++++++---------- test/gui/features/vfs/vfs.feature | 10 ++--- 10 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.woodpecker/ui-tests.yaml b/.woodpecker/ui-tests.yaml index 314b948308..84fa9ea6f4 100644 --- a/.woodpecker/ui-tests.yaml +++ b/.woodpecker/ui-tests.yaml @@ -127,7 +127,7 @@ steps: GUI_TEST_REPORT_DIR: /woodpecker/desktop/test/gui/reports BEHAVE_TEST_DIR: /woodpecker/desktop/test/gui # Cannot handle this tags format inside a container: --tags='@smoke and not @skip' - BEHAVE_PARAMETERS: '--tags=@smoke --tags=~@skip ${SUITES}' + BEHAVE_PARAMETERS: '--tags=~@skip ${SUITES}' # - name: crash-log # image: *alpine_image diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index 2b86e6ed7d..a046571aec 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -23,7 +23,7 @@ Feature: filter activity for user | resource | action | account | | brian-folder | Downloaded | Brian Murphy@%local_server_hostname% | - @skipOnWindows + @skipOnWindows @skip Scenario: filter not synced activities (Linux only) Given user "Alice" has been created in the server with default attributes And user "Alice" has set up a client with default settings @@ -41,7 +41,7 @@ Feature: filter activity for user | resource | status | account | | Folder1/a\\a.txt | Blacklisted | Alice Hansen@%local_server_hostname% | - @skipOnLinux + @skipOnLinux @skip Scenario: filter not synced activities (Windows only) Given user "Alice" has been created in the server with default attributes And user "Alice" has set up a client with default settings diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 202ed6eeef..7e7fe263e6 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -6,7 +6,7 @@ Feature: adding accounts Background: Given user "Alice" has been created in the server with default attributes - + @skip Scenario: Check default options in advanced configuration Given the user has started the client And the user has entered the following account information: @@ -38,7 +38,7 @@ Feature: adding accounts Then "Brian" account should be opened And "Alice" account should be added - + @skip Scenario: Adding account with self signed certificate for the first time Given the user has started the client When the user adds the server "%local_server%" @@ -61,7 +61,7 @@ Feature: adding accounts And the user syncs the "Personal" space Then the folder "simple-folder" should exist on the file system - + @skip Scenario: Check for suffix when sync path exists Given the user has created folder "OpenCloud" in the default home path And the user has started the client diff --git a/test/gui/features/delete-files-folders/delete.feature b/test/gui/features/delete-files-folders/delete.feature index 8136293ce0..f2f3dbffcf 100644 --- a/test/gui/features/delete-files-folders/delete.feature +++ b/test/gui/features/delete-files-folders/delete.feature @@ -46,7 +46,7 @@ Feature: deleting files and folders And as "Alice" file "textfile2.txt" should exist in the server And as "Alice" folder "test-folder2" should exist in the server - + @skip Scenario: Delete multiple files Given user "Alice" has uploaded the following files to the server | file | content | @@ -65,7 +65,7 @@ Feature: deleting files and folders | textfile1.txt | And as "Alice" file "textfile2.txt" should exist in the server - + @skip Scenario Outline: Create and delete a file with special characters Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder diff --git a/test/gui/features/edit-files/edit.feature b/test/gui/features/edit-files/edit.feature index 11215dba43..1de413b6fa 100644 --- a/test/gui/features/edit-files/edit.feature +++ b/test/gui/features/edit-files/edit.feature @@ -26,7 +26,7 @@ Feature: edit files And the user waits for file "testfile.txt" to be synced Then as "Alice" the file "testfile.txt" should have the content "overwrite openCloud test text file" in the server - + @skip Scenario Outline: Replace and modify the content of a file multiple times Given user "Alice" has set up a client with default settings And the user has copied file "" from outside the sync folder to "/" in the sync folder diff --git a/test/gui/features/move-files-folders/moveFilesFolders.feature b/test/gui/features/move-files-folders/moveFilesFolders.feature index e1f92b2136..5ace485c31 100644 --- a/test/gui/features/move-files-folders/moveFilesFolders.feature +++ b/test/gui/features/move-files-folders/moveFilesFolders.feature @@ -11,7 +11,7 @@ Feature: move file and folder And user "Alice" has created folder "folder1/folder2/folder3/folder4" in the server And user "Alice" has created folder "folder1/folder2/folder3/folder4/folder5" in the server - + @skip Scenario: Move folder and file from level 5 sub-folder to sync root Given user "Alice" has created folder "folder1/folder2/folder3/folder4/folder5/test-folder" in the server And user "Alice" has uploaded file with content "openCloud" to "folder1/folder2/folder3/folder4/folder5/lorem.txt" in the server @@ -24,7 +24,7 @@ Feature: move file and folder And as "Alice" file "folder1/folder2/folder3/folder4/folder5/lorem.txt" should not exist in the server And as "Alice" folder "folder1/folder2/folder3/folder4/folder5/test-folder" should not exist in the server - + @skip Scenario: Move two folders and a file down to the level 5 sub-folder And user "Alice" has created folder "test-folder1" in the server And user "Alice" has created folder "test-folder2" in the server @@ -67,7 +67,7 @@ Feature: move file and folder And as "Alice" file "folder1/file2.txt" should not exist in the server - + @skip Scenario: Move resources from different sub-levels to sync root Given user "Alice" has created folder "folder1/folder2/folder3/folder4/test-folder" in the server And user "Alice" has uploaded file with content "openCloud" to "folder1/folder2/lorem.txt" in the server @@ -80,7 +80,7 @@ Feature: move file and folder And as "Alice" file "folder1/folder2/lorem.txt" should not exist in the server And as "Alice" folder "folder1/folder2/folder3/folder4/test-folder" should not exist in the server - + @skip Scenario: Syncing a 50MB file moved into the local sync folder Given user "Alice" has set up a client with default settings And user "Alice" has created a folder "NewFolder" inside the sync folder diff --git a/test/gui/features/remove-account-connection/removeAccountConnection.feature b/test/gui/features/remove-account-connection/removeAccountConnection.feature index 3003bf68f3..39dd3d89e6 100644 --- a/test/gui/features/remove-account-connection/removeAccountConnection.feature +++ b/test/gui/features/remove-account-connection/removeAccountConnection.feature @@ -15,7 +15,7 @@ Feature: remove account connection Then "Brian" account should not be displayed But "Alice" account should be added - + @skip Scenario: remove the only account connection Given user "Alice" has been created in the server with default attributes And user "Alice" has created folder "large-folder" in the server diff --git a/test/gui/features/spaces/spaces.feature b/test/gui/features/spaces/spaces.feature index 4f2bb3267e..b28c4e7782 100644 --- a/test/gui/features/spaces/spaces.feature +++ b/test/gui/features/spaces/spaces.feature @@ -16,7 +16,7 @@ Feature: Project spaces Then user "Alice" should be able to open the file "testfile.txt" on the file system And as "Alice" the file "testfile.txt" should have content "some content" on the file system - + @skip Scenario: User with Viewer role cannot edit the file Given the administrator has created a folder "planning" in space "Project101" And the administrator has uploaded a file "testfile.txt" with content "some content" inside space "Project101" @@ -68,7 +68,7 @@ Feature: Project spaces Then for user "Alice" sync folder "Project101" should not be displayed But the file "testfile.txt" should exist on the file system - + @skip Scenario: User with Viewer role cannot create resource Given the administrator has added user "Alice" to space "Project101" with role "viewer" And user "Alice" has set up a client with space "Project101" diff --git a/test/gui/features/sync-resources/syncResources.feature b/test/gui/features/sync-resources/syncResources.feature index 9390ce0efe..8316037850 100644 --- a/test/gui/features/sync-resources/syncResources.feature +++ b/test/gui/features/sync-resources/syncResources.feature @@ -33,7 +33,7 @@ Feature: Syncing files And the folder "simple-folder" should exist on the file system And the folder "large-folder" should exist on the file system - @issue-9733 + @issue-9733 @skip Scenario: Syncing a file from the server and creating a conflict Given user "Alice" has uploaded file with content "server content" to "/conflict.txt" in the server And user "Alice" has set up a client with default settings @@ -57,7 +57,7 @@ Feature: Syncing files client content """ - @skipOnWindows + @skipOnWindows @skip Scenario: Sync all is selected by default Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has created folder "large-folder" in the server @@ -110,7 +110,7 @@ Feature: Syncing files And the file "large-folder/lorem.txt" should not exist on the file system And as "Alice" file "simple-folder/localFile.txt" should exist in the server - @issue-9733 @skipOnWindows + @issue-9733 @skipOnWindows @skip Scenario: sort folders list by name and size Given user "Alice" has created folder "123Folder" in the server And user "Alice" has uploaded file with content "small" to "123Folder/lorem.txt" in the server @@ -165,7 +165,7 @@ Feature: Syncing files | "myFolder" | | "really long folder name with some spaces and special char such as $%ñ&" | - @skipOnWindows + @skipOnWindows @skip Scenario Outline: Syncing a folder having space at the end (Linux only) Given user "Alice" has set up a client with default settings When user "Alice" creates a folder inside the sync folder @@ -175,7 +175,7 @@ Feature: Syncing files | foldername | | "folder with space at end " | - @skipOnLinux + @skipOnLinux @skip Scenario: Try to sync files having space at the end (Windows only) Given user "Alice" has uploaded file with content "lorem epsum" to "trailing-space.txt " in the server And user "Alice" has set up a client with default settings @@ -266,7 +266,7 @@ Feature: Syncing files And as "Alice" folder "Folder1/subFolder1" should exist in the server And as "Alice" folder "Folder1/subFolder1/subFolder2" should exist in the server - @skipOnWindows + @skipOnWindows @skip Scenario: Filenames that are rejected by the server are reported (Linux only) Given user "Alice" has created folder "Folder1" in the server And user "Alice" has set up a client with default settings @@ -279,7 +279,7 @@ Feature: Syncing files Then the file "Folder1/a\\a.txt" should exist on the file system And the file "Folder1/a\\a.txt" should be blacklisted - + @skip Scenario Outline: Sync long nested folder Given user "Alice" has created folder "" in the server And user "Alice" has set up a client with default settings @@ -312,7 +312,7 @@ Feature: Syncing files And as "Alice" file "/PRN" should exist in the server And as "Alice" file "/foo%" should exist in the server - @skipOnLinux + @skipOnLinux @skip Scenario: Sync invalid system names (Windows only) Given user "Alice" has created folder "CON" in the server And user "Alice" has created folder "test%" in the server @@ -348,7 +348,7 @@ Feature: Syncing files And the file "simple-folder/simple.pptx" should exist on the file system And the file "simple-folder/simple.xlsx" should exist on the file system - + @skip Scenario: various types of files can be synced from client to server Given user "Alice" has set up a client with default settings When user "Alice" creates the following files inside the sync folder: @@ -393,7 +393,7 @@ Feature: Syncing files And the user waits for file "newfile.txt" to be synced Then as "Alice" file "newfile.txt" should exist in the server - + @skip Scenario: File with spaces in the name can sync Given user "Alice" has set up a client with default settings When user "Alice" creates a file "file with space.txt" with the following content inside the sync folder @@ -403,7 +403,7 @@ Feature: Syncing files And the user waits for file "file with space.txt" to be synced Then as "Alice" file "file with space.txt" should exist in the server - + @skip Scenario: Syncing folders each having large number of files Given the user has created a folder "folder1" in temp folder And the user has created "500" files each of size "1048576" bytes inside folder "folder1" in temp folder @@ -438,7 +438,7 @@ Feature: Syncing files And for user "Alice" sync folder "Personal" should not be displayed And for user "Alice" sync folder "Shares" should not be displayed - + @skip Scenario: extract a zip file in the sync folder Given the user has created a zip file "archive.zip" with the following resources in the temp folder | resource | type | content | @@ -456,7 +456,7 @@ Feature: Syncing files And as "Alice" the file "file2.txt" should have the content "Test file2" in the server - @skipOnWindows + @skipOnWindows @skip Scenario: sync remote folder to a local sync folder having special characters Given user "Alice" has created folder "~`!@#$^&()-_=+{[}];',)" in the server And user "Alice" has created folder "simple-folder" in the server @@ -487,14 +487,14 @@ Feature: Syncing files And the folder "test-folder/sub-folder2" should exist on the file system And the folder "test-folder/sub-folder1" should not exist on the file system - + @skip Scenario: Syncing a local folder having special characters to the server Given user "Alice" has set up a client with default settings When user "Alice" creates a folder "~`!@#$^&()-_=+{[}];',)💥🫨❤️‍🔥" inside the sync folder And the user waits for folder "~`!@#$^&()-_=+{[}];',)💥🫨❤️‍🔥" to be synced Then as "Alice" folder "~`!@#$^&()-_=+{[}];',)💥🫨❤️‍🔥" should exist in the server - @issue-11814 + @issue-11814 @skip Scenario: Remove folder sync connection (Personal Space) Given user "Alice" has created folder "simple-folder" in the server And user "Alice" has set up a client with default settings @@ -503,7 +503,7 @@ Feature: Syncing files And the folder "simple-folder" should exist on the file system And as "Alice" folder "simple-folder" should exist in the server - + @skip Scenario: Sync a received shared folder with Viewer permission role Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server @@ -534,7 +534,7 @@ Feature: Syncing files | simple-folder/sub-folder | Blacklisted | Brian Murphy@%local_server_hostname% | | simple-folder/simple.pdf | Blacklisted | Brian Murphy@%local_server_hostname% | - + @skip Scenario Outline: File with long multi-byte characters name can be synced (76 characters, 255 bytes including extension) Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder @@ -547,7 +547,7 @@ Feature: Syncing files | filename | | 𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺abôǣฎพฒฆ๘ตกกผพฒณญไใๅำ๊๒๔๗๘รศฬอฮ.txt | - + @skip Scenario: Sync a received shared folder with Editor permission role Given user "Brian" has been created in the server with default attributes And user "Alice" has created folder "simple-folder" in the server @@ -585,7 +585,7 @@ Feature: Syncing files And the user waits for the files to sync Then the file "test-folder/sub-folder2/lorem.txt" should not exist on the file system - @skipOnWindows + @skipOnWindows @skip Scenario: Only root level files sync when all folders are unselected Given user "Alice" has created folder "test-folder" in the server And user "Alice" has created folder "test-folder/sub-folder1" in the server diff --git a/test/gui/features/vfs/vfs.feature b/test/gui/features/vfs/vfs.feature index b8cd3e173b..a7c078383a 100644 --- a/test/gui/features/vfs/vfs.feature +++ b/test/gui/features/vfs/vfs.feature @@ -4,7 +4,7 @@ Feature: VFS support I want to sync files with vfs So that I can decide which files to download - + @skip Scenario: Default VFS sync Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "openCloud" to "testFile.txt" in the server @@ -17,7 +17,7 @@ Feature: VFS support Then the file "parent/lorem.txt" should be downloaded And the placeholder file "testFile.txt" should exist on the file system - + @skip Scenario: Copy placeholder file Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "sample file" to "sampleFile.txt" in the server @@ -43,7 +43,7 @@ Feature: VFS support And as "Alice" file "testFile.txt" should exist in the server And as "Alice" file "testFile (Copy).txt" should exist in the server - + @skip Scenario: Move placeholder file Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "lorem file" to "lorem.txt" in the server @@ -58,7 +58,7 @@ Feature: VFS support And as "Alice" file "lorem.txt" should not exist in the server And as "Alice" file "sampleFile.txt" should not exist in the server - + @skip Scenario: Hydration and dehydration of files via file explorer Given user "Alice" has been created in the server with default attributes And user "Alice" has uploaded file with content "test content" to "testFile.txt" in the server @@ -101,7 +101,7 @@ Feature: VFS support And the user waits for file "simple.txt" to be synced Then the placeholder file "simple.txt" should exist on the file system - + @skip Scenario: Hydration and dehydration of folders via file explorer Given user "Alice" has been created in the server with default attributes And user "Alice" has created folder "testFol" in the server From 5c15c57512e873600c0b65798d4ffb4ca1564cd0 Mon Sep 17 00:00:00 2001 From: Asmita Paudel <94975243+Asmitapaudel@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:10:50 +0545 Subject: [PATCH 68/75] test: port account test to appium (#921) Signed-off-by: Asmitapaudel --- test/gui/features/add-account/account.feature | 2 +- test/gui/pageObjects/AccountConnectionWizard.py | 12 +++++------- test/gui/steps/account_context.py | 16 +++++++--------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/test/gui/features/add-account/account.feature b/test/gui/features/add-account/account.feature index 7e7fe263e6..5294cfab5b 100644 --- a/test/gui/features/add-account/account.feature +++ b/test/gui/features/add-account/account.feature @@ -38,7 +38,7 @@ Feature: adding accounts Then "Brian" account should be opened And "Alice" account should be added - @skip + Scenario: Adding account with self signed certificate for the first time Given the user has started the client When the user adds the server "%local_server%" diff --git a/test/gui/pageObjects/AccountConnectionWizard.py b/test/gui/pageObjects/AccountConnectionWizard.py index bef9c4e35e..f4fb1e5640 100644 --- a/test/gui/pageObjects/AccountConnectionWizard.py +++ b/test/gui/pageObjects/AccountConnectionWizard.py @@ -32,7 +32,7 @@ class AccountConnectionWizard: selector="QApplication.Settings.centralwidget.dialogStack.SetupWizardWidget.contentWidget.AccountConfiguredWizardPage.advancedConfigGroupBox.advancedConfigGroupBoxContentWidget.localDirectoryGroupBox.chooseLocalDirectoryButton", ) CHOOSE_FOLDER_BUTTON = SimpleNamespace(by=By.NAME, selector="Choose") - OAUTH_CREDENTIAL_PAGE = SimpleNamespace(by=None, selector=None) + LOGIN_DIALOG = SimpleNamespace(by=By.NAME, selector="Log in with your web browser") COPY_URL_TO_CLIPBOARD_BUTTON = SimpleNamespace( by=By.NAME, selector="Copy URL", @@ -192,12 +192,10 @@ def is_new_connection_window_visible(): @staticmethod def is_credential_window_visible(): - visible = False - try: - squish.waitForObject(AccountConnectionWizard.OAUTH_CREDENTIAL_PAGE) - visible = True - except: - pass + visible = app().find_element( + AccountConnectionWizard.LOGIN_DIALOG.by, + AccountConnectionWizard.LOGIN_DIALOG.selector + ).is_displayed() return visible @staticmethod diff --git a/test/gui/steps/account_context.py b/test/gui/steps/account_context.py index 80b4155bec..18ebb662e6 100644 --- a/test/gui/steps/account_context.py +++ b/test/gui/steps/account_context.py @@ -179,9 +179,9 @@ def step(context): AccountConnectionWizard.accept_certificate() -@When('the user adds the server "|any|"') -def step(context, server): - server_url = substitute_inline_codes(server) +@When('the user adds the server "{server_url}"') +def step(context, server_url): + server_url = substitute_inline_codes(server_url) AccountConnectionWizard.add_server(server_url) @@ -193,12 +193,10 @@ def step(context): @Then('credentials wizard should be visible') def step(context): - test.compare( - AccountConnectionWizard.is_credential_window_visible(), - True, - 'Credentials wizard is 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): From 22d9a327feae6c564662fff3ca431c37922e98ff Mon Sep 17 00:00:00 2001 From: Asmita Paudel <94975243+Asmitapaudel@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:30:24 +0545 Subject: [PATCH 69/75] test(gui): enable move files and folders test (#931) --- .../move-files-folders/moveFilesFolders.feature | 10 +++++----- test/gui/steps/file_context.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/gui/features/move-files-folders/moveFilesFolders.feature b/test/gui/features/move-files-folders/moveFilesFolders.feature index 5ace485c31..4d24d3a651 100644 --- a/test/gui/features/move-files-folders/moveFilesFolders.feature +++ b/test/gui/features/move-files-folders/moveFilesFolders.feature @@ -11,7 +11,7 @@ Feature: move file and folder And user "Alice" has created folder "folder1/folder2/folder3/folder4" in the server And user "Alice" has created folder "folder1/folder2/folder3/folder4/folder5" in the server - @skip + Scenario: Move folder and file from level 5 sub-folder to sync root Given user "Alice" has created folder "folder1/folder2/folder3/folder4/folder5/test-folder" in the server And user "Alice" has uploaded file with content "openCloud" to "folder1/folder2/folder3/folder4/folder5/lorem.txt" in the server @@ -24,7 +24,7 @@ Feature: move file and folder And as "Alice" file "folder1/folder2/folder3/folder4/folder5/lorem.txt" should not exist in the server And as "Alice" folder "folder1/folder2/folder3/folder4/folder5/test-folder" should not exist in the server - @skip + Scenario: Move two folders and a file down to the level 5 sub-folder And user "Alice" has created folder "test-folder1" in the server And user "Alice" has created folder "test-folder2" in the server @@ -67,7 +67,7 @@ Feature: move file and folder And as "Alice" file "folder1/file2.txt" should not exist in the server - @skip + Scenario: Move resources from different sub-levels to sync root Given user "Alice" has created folder "folder1/folder2/folder3/folder4/test-folder" in the server And user "Alice" has uploaded file with content "openCloud" to "folder1/folder2/lorem.txt" in the server @@ -80,11 +80,11 @@ Feature: move file and folder And as "Alice" file "folder1/folder2/lorem.txt" should not exist in the server And as "Alice" folder "folder1/folder2/folder3/folder4/test-folder" should not exist in the server - @skip + Scenario: Syncing a 50MB file moved into the local sync folder Given user "Alice" has set up a client with default settings And user "Alice" has created a folder "NewFolder" inside the sync folder - And user "Alice" has created a file "newfile.txt" with size "50MB" in the sync folder + And the user has created a file "newfile.txt" with size "50MB" in the sync folder When user "Alice" moves file "newfile.txt" to "NewFolder" in the sync folder And the user waits for file "NewFolder/newfile.txt" to be synced Then as "Alice" file "NewFolder/newfile.txt" should exist in the server diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index e99f38130c..dc983f1b5b 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -4,7 +4,7 @@ import shutil import zipfile from os.path import isfile, join, isdir, exists -from behave import when as When, then as Then +from behave import when as When, then as Then, given as Given from sure import ensure from helpers.SetupClientHelper import get_resource_path, get_temp_resource_path @@ -453,6 +453,6 @@ def step(context): deleteResource(filename, 'file') -@Given('user "|any|" has created a file "|any|" with size "|any|" in the sync folder') -def step(context, _, filename, filesize): +@Given('the user has created a file "{filename}" with size "{filesize}" in the sync folder') +def step(context, filename, filesize): create_file_with_size(filename, filesize) From 71d0291890b4f559d8efb276737d7f527f82b074 Mon Sep 17 00:00:00 2001 From: Asmitapaudel Date: Thu, 28 May 2026 16:05:13 +0545 Subject: [PATCH 70/75] test(activity): filter not synced activities (Linux only) --- test/gui/features/activity/activity.feature | 2 +- test/gui/pageObjects/Activity.py | 25 +++++---- test/gui/steps/file_context.py | 4 +- test/gui/steps/sync_context.py | 56 +++++++++++++-------- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index a046571aec..1e3bf71813 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -23,7 +23,7 @@ Feature: filter activity for user | resource | action | account | | brian-folder | Downloaded | Brian Murphy@%local_server_hostname% | - @skipOnWindows @skip + @skipOnWindows Scenario: filter not synced activities (Linux only) Given user "Alice" has been created in the server with default attributes And user "Alice" has set up a client with default settings diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 7abcc64f89..d8cdd7aa37 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -7,6 +7,7 @@ from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config from helpers.AppHelper import app +from helpers.SyncHelper import wait_for class Activity: @@ -21,7 +22,9 @@ class Activity: FILTER_BUTTON_SELECTED_STATE = SimpleNamespace( by=By.XPATH, selector="//*[contains(@name, '1 Filter')]" ) - NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=None, selector=None) + NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=By.ACCESSIBILITY_ID, + selector="QApplication.Settings.centralwidget.dialogStack.page.stack.OCC::ActivitySettings.QTabWidget.qt_tabwidget_stackedwidget.OCC__IssuesWidget._filterButton" + ) NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) @@ -62,7 +65,7 @@ def check_file_exist(filename): @staticmethod def is_resource_blacklisted(filename): - result = squish.waitFor( + result = wait_for( lambda: Activity.has_sync_status(filename, "Blacklisted"), get_config("maxSyncTimeout") * 1000, ) @@ -78,7 +81,7 @@ def is_resource_ignored(filename): @staticmethod def is_resource_excluded(filename): - result = squish.waitFor( + result = wait_for( lambda: Activity.has_sync_status(filename, "Excluded"), get_config("maxSyncTimeout") * 1000, ) @@ -164,12 +167,16 @@ def has_activity(resource, action, account): @staticmethod def select_not_synced_filter(filter_option): - squish.clickButton(squish.waitForObject(Activity.NOT_SYNCED_FILTER_BUTTON)) - squish.activateItem( - squish.waitForObjectItem( - Activity.NOT_SYNCED_FILTER_OPTION_SELECTOR, filter_option - ) - ) + menu = app().find_element( + Activity.NOT_SYNCED_FILTER_BUTTON.by, + Activity.NOT_SYNCED_FILTER_BUTTON.selector) + menu.click() + # NOTE: Filter options are not visible in the accessibility tree. + # As a workaround, select the 6th filter option (which is an Excluded filter). + # This means we cannot select a specific Excluded filter for now. + for _ in range(6): + menu.send_keys(Keys.ARROW_DOWN) + menu.send_keys(Keys.ENTER) @staticmethod def get_not_synced_table_column_number_by_name(column_name): diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index dc983f1b5b..cb1ff6b45a 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -290,9 +290,9 @@ def step(context, resource_type, resource_name): deleteResource(resource_name, resource_type) -@When('user "|any|" creates the following files inside the sync folder:') +@When('user "{username}" creates the following files inside the sync folder:') def step(context, username): - for row in context.table[1:]: + for row in context.table: file = get_resource_path(row[0], username) wait_and_write_file(file, '') diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 9c4b5ebc81..069a45ba2e 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -21,6 +21,23 @@ from helpers.TableParser import table_hashes, table_raw +def _check_activities(context, not_synced=False): + field = "status" if not_synced else "action" + activities = table_hashes(context.table) + for activity in activities: + activity["account"] = substitute_inline_codes(activity["account"]) + has_activity = Activity.has_activity( + activity["resource"], activity[field], activity["account"] + ) + with ensure( + 'Activity should exist: {0} | {1} | {2}', + activity["resource"], + activity[field], + activity["account"], + ): + has_activity.should.be.true + + @Given('the user has paused the file sync') def step(context): SyncConnection.pause_sync() @@ -93,11 +110,12 @@ def step(context, filename): Activity.check_file_exist(filename) -@Then('the file "|any|" should be blacklisted') +@Then('the file "{filename}" should be blacklisted') def step(context, filename): - test.compare( - True, Activity.is_resource_blacklisted(filename), 'File is Blacklisted' - ) + with ensure( + 'File is Blacklisted' + ): + Activity.is_resource_blacklisted(filename).should.be.true @Then('the file "|any|" should be ignored') @@ -105,9 +123,12 @@ def step(context, filename): test.compare(True, Activity.is_resource_ignored(filename), 'File is Ignored') -@Then('the file "|any|" should be excluded') +@Then('the file "{filename}" should be excluded') def step(context, filename): - test.compare(True, Activity.is_resource_excluded(filename), 'File is Excluded') + with ensure( + 'File is Excluded' + ): + Activity.is_resource_excluded(filename).should.be.true @When('the user selects "{tab_name}" tab in the activity') @@ -289,19 +310,12 @@ def step(context, account): @Then('the following activities should be displayed in synced table') def step(context): - activities = table_hashes(context.table) - for activity in activities: - activity["account"] = substitute_inline_codes(activity["account"]) - has_activity = Activity.has_activity( - activity["resource"], activity["action"], activity["account"] - ) - with ensure( - 'Activity should exist: {0} | {1} | {2}', - activity["resource"], - activity["action"], - activity["account"], - ): - has_activity.should.be.true + _check_activities(context) + + +@Then('the following activities should be displayed in not synced table') +def step(context): + _check_activities(context, not_synced=True) @Then('the following activities should not be displayed in synced table') @@ -338,7 +352,7 @@ def step(context, should_or_should_not): ) -@When('the user unchecks the "|any|" filter') +@When('the user unchecks the "{filter_option}" filter') def step(context, filter_option): Activity.select_not_synced_filter(filter_option) @@ -374,4 +388,4 @@ def step(context): folders.append(row[0]) SyncConnectionWizard.unselect_folders_to_sync( folders, new_sync_connection_wizard=False - ) + ) \ No newline at end of file From 38c8434f487645cb24cee8956f080f24dead7ee0 Mon Sep 17 00:00:00 2001 From: Asmitapaudel Date: Wed, 3 Jun 2026 14:39:03 +0545 Subject: [PATCH 71/75] test(activity): Add should_exist handling in _check_activities for synced and not synced tables --- test/gui/features/activity/activity.feature | 1 + test/gui/steps/sync_context.py | 42 ++++++--------------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/test/gui/features/activity/activity.feature b/test/gui/features/activity/activity.feature index 1e3bf71813..233797de12 100644 --- a/test/gui/features/activity/activity.feature +++ b/test/gui/features/activity/activity.feature @@ -41,6 +41,7 @@ Feature: filter activity for user | resource | status | account | | Folder1/a\\a.txt | Blacklisted | Alice Hansen@%local_server_hostname% | + @skipOnLinux @skip Scenario: filter not synced activities (Windows only) Given user "Alice" has been created in the server with default attributes diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 069a45ba2e..28168811f0 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -21,7 +21,7 @@ from helpers.TableParser import table_hashes, table_raw -def _check_activities(context, not_synced=False): +def _check_activities(context, not_synced=False, should_exist=True): field = "status" if not_synced else "action" activities = table_hashes(context.table) for activity in activities: @@ -35,7 +35,10 @@ def _check_activities(context, not_synced=False): activity[field], activity["account"], ): - has_activity.should.be.true + if should_exist: + has_activity.should.be.true + else: + has_activity.should.be.false @Given('the user has paused the file sync') @@ -316,40 +319,17 @@ def step(context): @Then('the following activities should be displayed in not synced table') def step(context): _check_activities(context, not_synced=True) - + @Then('the following activities should not be displayed in synced table') def step(context): - activities = table_hashes(context.table) - for activity in activities: - activity["account"] = substitute_inline_codes(activity["account"]) - has_activity = Activity.has_activity( - activity["resource"], activity["action"], activity["account"] - ) - with ensure( - 'Activity should not exist: {0} | {1} | {2}', - activity["resource"], - activity["action"], - activity["account"], - ): - has_activity.should.be.false + _check_activities(context, should_exist=False) -@Then( - r'the following activities (should|should not) be displayed in not synced table', - regexp=True, -) -def step(context, should_or_should_not): - expected = should_or_should_not == "should" - for row in context.table[1:]: - resource = row[0] - status = row[1] - account = substitute_inline_codes(row[2]) - test.compare( - Activity.check_not_synced_table(resource, status, account), - expected, - 'Resource should be displayed in the not synced table', - ) +@Then('the following activities should not be displayed in not synced table') +def step(context): + _check_activities(context, not_synced=True, should_exist=False) + @When('the user unchecks the "{filter_option}" filter') From 33117a0449232857d16267568bad28a72ff356a6 Mon Sep 17 00:00:00 2001 From: Asmitapaudel Date: Tue, 2 Jun 2026 14:35:50 +0545 Subject: [PATCH 72/75] test(delete): Delete multiple files --- .../delete-files-folders/delete.feature | 6 ++--- test/gui/steps/file_context.py | 3 +-- test/gui/steps/server_context.py | 22 +++++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/test/gui/features/delete-files-folders/delete.feature b/test/gui/features/delete-files-folders/delete.feature index f2f3dbffcf..79cb1e3aa7 100644 --- a/test/gui/features/delete-files-folders/delete.feature +++ b/test/gui/features/delete-files-folders/delete.feature @@ -17,7 +17,7 @@ Feature: deleting files and folders | fileName | | textfile0.txt | | textfile0-with-name-more-than-20-characters | - | ~`!@#$^&()-_=+{[}];',textfile.txt | + | ~`!@#$^&()-_=+{[}];',textfile.txt | @issue-9439 @smoke Scenario Outline: Delete a folder @@ -46,7 +46,7 @@ Feature: deleting files and folders And as "Alice" file "textfile2.txt" should exist in the server And as "Alice" folder "test-folder2" should exist in the server - @skip + Scenario: Delete multiple files Given user "Alice" has uploaded the following files to the server | file | content | @@ -65,7 +65,7 @@ Feature: deleting files and folders | textfile1.txt | And as "Alice" file "textfile2.txt" should exist in the server - @skip + Scenario Outline: Create and delete a file with special characters Given user "Alice" has set up a client with default settings When user "Alice" creates a file "" with the following content inside the sync folder diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index cb1ff6b45a..c026e7d95c 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -447,8 +447,7 @@ def step(context, resource_name, destination): @When('the user deletes the following files') def step(context): wait_for_client_to_be_ready() - - for row in context.table[1:]: + for row in context.table: filename = row[0] deleteResource(filename, 'file') diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index ef21698a61..1b22d5d053 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -115,23 +115,21 @@ def step(context, user_name, server_file_name, local_file_name): ) -@Then( - r'as "([^"].*)" following files should not exist in the server', - regexp=True, -) +@Then('as "{user_name}" following files should not exist in the server',) def step(context, user_name): - for row in context.table[1:]: + for row in context.table: resource_name = row[0] - test.compare( - webdav.resource_exists(user_name, resource_name), - False, - f"Resource '{resource_name}' should not exist, but does", - ) + resource_exists = webdav.resource_exists(user_name, resource_name) + with ensure( + f"Resource '{resource_name}' should not exist, but it does", + ): + resource_exists.should.be.false + -@Given('user "|any|" has uploaded the following files to the server') +@Given('user "{user}" has uploaded the following files to the server') def step(context, user): - for row in context.table[1:]: + for row in context.table: file_name = row[0] file_content = row[1] webdav.create_file(user, file_name, file_content) From e2dfd98c0174836c93519ede1b6b572ad7af27c0 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 10 Jun 2026 12:50:18 +0545 Subject: [PATCH 73/75] test: move wait_for into utils helper Signed-off-by: Saw-jan --- test/gui/helpers/SyncHelper.py | 24 +++++------------------- test/gui/helpers/Utils.py | 11 +++++++++++ test/gui/pageObjects/AccountSetting.py | 18 +++++++++--------- test/gui/pageObjects/Activity.py | 18 ++++++++++-------- test/gui/pageObjects/SyncConnection.py | 2 +- test/gui/pageObjects/Toolbar.py | 6 +++--- test/gui/steps/file_context.py | 12 +++++++----- test/gui/steps/sync_context.py | 15 +++++---------- 8 files changed, 51 insertions(+), 55 deletions(-) create mode 100644 test/gui/helpers/Utils.py diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index 09796aea90..e03543782a 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -2,12 +2,11 @@ import re import time import urllib.request -from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import TimeoutException from pageObjects.SyncConnection import SyncConnection from helpers.ConfigHelper import get_config, is_linux, is_windows from helpers.FilesHelper import sanitize_path +from helpers.Utils import wait_for if is_windows(): from helpers.WinPipeHelper import WinPipeConnect as SocketConnect @@ -248,13 +247,13 @@ def wait_for_resource_to_sync( listen_sync_status_for_item(resource, resource_type) initial_timeout = 0 - timeout = get_config('maxSyncTimeout') * 1000 + timeout = get_config('maxSyncTimeout') if patterns is None: patterns = get_synced_pattern(resource) if force_sync: - initial_timeout = 5000 + initial_timeout = 5 # first try with 5 seconds timeout synced = wait_for( lambda: has_sync_pattern(patterns, resource), @@ -293,9 +292,7 @@ def wait_for_resource_to_sync( ) return raise TimeoutError( - 'Timeout while waiting for sync to complete for ' - + str(timeout) - + ' milliseconds' + 'Timeout while waiting for sync to complete for ' + str(timeout) + ' seconds' ) @@ -352,7 +349,7 @@ def wait_for_resource_to_have_sync_status( listen_sync_status_for_item(resource, resource_type) if not timeout: - timeout = get_config('maxSyncTimeout') * 1000 + timeout = get_config('maxSyncTimeout') result = wait_for( lambda: has_sync_status(resource, status), @@ -409,14 +406,3 @@ def make_available_locally(resource_path): socket_connect = get_socket_connection() resource_path = resource_path.rstrip('\\').rstrip('/') socket_connect.sendCommand(f'MAKE_AVAILABLE_LOCALLY:{resource_path}\n') - - -def wait_for(condition, timeout, interval=0.5): - from helpers.AppHelper import app - - wait = WebDriverWait(app(), timeout / 1000, poll_frequency=interval) - try: - wait.until(lambda _: condition()) - return True - except TimeoutException: - return False diff --git a/test/gui/helpers/Utils.py b/test/gui/helpers/Utils.py new file mode 100644 index 0000000000..442f919d8d --- /dev/null +++ b/test/gui/helpers/Utils.py @@ -0,0 +1,11 @@ +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import TimeoutException + + +def wait_for(condition, timeout=10, interval=0.5): + wait = WebDriverWait(None, timeout, poll_frequency=interval) + try: + wait.until(lambda _: condition()) + return True + except TimeoutException: + return False diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index 7733019a3a..ec6b36945f 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -6,7 +6,7 @@ from helpers.SetupClientHelper import substitute_inline_codes from helpers.UserHelper import get_displayname_for_user from helpers.AppHelper import app -from helpers.SyncHelper import wait_for +from helpers.Utils import wait_for class AccountSetting: @@ -86,7 +86,7 @@ def is_user_signed_in(): return "Connected" in AccountSetting.get_account_connection_label() @staticmethod - def wait_until_connection_is_configured(timeout=5000): + def wait_until_connection_is_configured(timeout=5): result = squish.waitFor( AccountSetting.is_connecting, timeout, @@ -96,11 +96,11 @@ def wait_until_connection_is_configured(timeout=5000): raise TimeoutError( "Timeout waiting for connection to be configured for " + str(timeout) - + " milliseconds" + + " seconds" ) @staticmethod - def wait_until_account_is_connected(timeout=5000): + def wait_until_account_is_connected(timeout=5): result = wait_for( AccountSetting.is_user_signed_in, timeout, @@ -110,12 +110,12 @@ def wait_until_account_is_connected(timeout=5000): raise TimeoutError( "Timeout waiting for the account to be connected for " + str(timeout) - + " milliseconds" + + " seconds" ) return result @staticmethod - def wait_until_sync_folder_is_configured(timeout=5000): + def wait_until_sync_folder_is_configured(timeout=5): result = squish.waitFor( lambda: not squish.waitForObjectExists( AccountSetting.ACCOUNT_LOADING @@ -127,7 +127,7 @@ def wait_until_sync_folder_is_configured(timeout=5000): raise TimeoutError( "Timeout waiting for sync folder to be connected for " + str(timeout) - + " milliseconds" + + " seconds" ) return result @@ -154,7 +154,7 @@ def remove_connection_for_user(username): AccountSetting.remove_account_connection() @staticmethod - def wait_until_account_is_removed(username, timeout=10000): + def wait_until_account_is_removed(username, timeout=10): displayname = get_displayname_for_user(username) displayname = substitute_inline_codes(displayname) @@ -168,5 +168,5 @@ def account_removed(): raise TimeoutError( "Timeout waiting for account to be removed for " + str(timeout) - + " milliseconds" + + " seconds" ) diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index d8cdd7aa37..30ab783979 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -7,7 +7,7 @@ from helpers.FilesHelper import build_conflicted_regex from helpers.ConfigHelper import get_config from helpers.AppHelper import app -from helpers.SyncHelper import wait_for +from helpers.Utils import wait_for class Activity: @@ -22,8 +22,9 @@ class Activity: FILTER_BUTTON_SELECTED_STATE = SimpleNamespace( by=By.XPATH, selector="//*[contains(@name, '1 Filter')]" ) - NOT_SYNCED_FILTER_BUTTON = SimpleNamespace(by=By.ACCESSIBILITY_ID, - selector="QApplication.Settings.centralwidget.dialogStack.page.stack.OCC::ActivitySettings.QTabWidget.qt_tabwidget_stackedwidget.OCC__IssuesWidget._filterButton" + NOT_SYNCED_FILTER_BUTTON = SimpleNamespace( + by=By.ACCESSIBILITY_ID, + selector="QApplication.Settings.centralwidget.dialogStack.page.stack.OCC::ActivitySettings.QTabWidget.qt_tabwidget_stackedwidget.OCC__IssuesWidget._filterButton", ) NOT_SYNCED_FILTER_OPTION_SELECTOR = SimpleNamespace(by=None, selector=None) SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR = SimpleNamespace(by=None, selector=None) @@ -67,7 +68,7 @@ def check_file_exist(filename): def is_resource_blacklisted(filename): result = wait_for( lambda: Activity.has_sync_status(filename, "Blacklisted"), - get_config("maxSyncTimeout") * 1000, + get_config("maxSyncTimeout"), ) return result @@ -75,7 +76,7 @@ def is_resource_blacklisted(filename): def is_resource_ignored(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "File Ignored"), - get_config("maxSyncTimeout") * 1000, + get_config("maxSyncTimeout"), ) return result @@ -83,7 +84,7 @@ def is_resource_ignored(filename): def is_resource_excluded(filename): result = wait_for( lambda: Activity.has_sync_status(filename, "Excluded"), - get_config("maxSyncTimeout") * 1000, + get_config("maxSyncTimeout"), ) return result @@ -169,7 +170,8 @@ def has_activity(resource, action, account): def select_not_synced_filter(filter_option): menu = app().find_element( Activity.NOT_SYNCED_FILTER_BUTTON.by, - Activity.NOT_SYNCED_FILTER_BUTTON.selector) + Activity.NOT_SYNCED_FILTER_BUTTON.selector, + ) menu.click() # NOTE: Filter options are not visible in the accessibility tree. # As a workaround, select the 6th filter option (which is an Excluded filter). @@ -194,7 +196,7 @@ def check_not_synced_table(resource, status, account): try: file_row = squish.waitForObject( Activity.get_not_synced_file_selector(resource), - get_config("lowestSyncTimeout") * 1000, + get_config("lowestSyncTimeout"), )["row"] squish.waitForObjectExists( { diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index 1693c3bd2e..ab8dd7c3f5 100644 --- a/test/gui/pageObjects/SyncConnection.py +++ b/test/gui/pageObjects/SyncConnection.py @@ -118,7 +118,7 @@ def wait_for_error_label(to_exist=True): """Wait for permission error label to appear or disappear""" status = squish.waitFor( lambda: object.exists(SyncConnection.PERMISSION_ERROR_LABEL) == to_exist, - get_config("maxSyncTimeout") * 1000, + get_config("maxSyncTimeout"), ) if not status: action = "appear" if to_exist else "disappear" diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index 515dfae8ac..a5f5a3d8fa 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -6,7 +6,7 @@ from helpers.AppHelper import app from helpers.ConfigHelper import get_config from helpers.UserHelper import get_displayname_for_user -from helpers.SyncHelper import wait_for +from helpers.Utils import wait_for class Toolbar: @@ -33,9 +33,9 @@ def wait_toolbar_enabled(): toolbar = app().find_element( Toolbar.NAVIGATION_BAR.by, Toolbar.NAVIGATION_BAR.selector ) - timeout = get_config('maxSyncTimeout') * 1000 + timeout = get_config('maxSyncTimeout') enabled = wait_for( - lambda: toolbar.is_enabled(), + toolbar.is_enabled, timeout, ) if not enabled: diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index c026e7d95c..e12a3d7405 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -11,8 +11,8 @@ from helpers.SyncHelper import ( wait_for_client_to_be_ready, listen_sync_status_for_item, - wait_for, ) +from helpers.Utils import wait_for from helpers.ConfigHelper import get_config, is_windows from helpers.FilesHelper import ( build_conflicted_regex, @@ -28,14 +28,14 @@ ) -def folder_exists(folder_path, timeout=1000): +def folder_exists(folder_path, timeout=1): return wait_for( lambda: isdir(sanitize_path(folder_path)), timeout, ) -def file_exists(file_path, timeout=1000): +def file_exists(file_path, timeout=1): return wait_for( lambda: isfile(sanitize_path(file_path)), timeout, @@ -212,7 +212,7 @@ def step(context, file_path): def step(context, resource_type, resource): resource_path = get_resource_path(resource) resource_exists = False - timeout = get_config('maxSyncTimeout') * 1000 + timeout = get_config('maxSyncTimeout') if resource_type == 'file': resource_exists = file_exists(resource_path, timeout) else: @@ -452,6 +452,8 @@ def step(context): deleteResource(filename, 'file') -@Given('the user has created a file "{filename}" with size "{filesize}" in the sync folder') +@Given( + 'the user has created a file "{filename}" with size "{filesize}" in the sync folder' +) def step(context, filename, filesize): create_file_with_size(filename, filesize) diff --git a/test/gui/steps/sync_context.py b/test/gui/steps/sync_context.py index 28168811f0..82f50d94f0 100644 --- a/test/gui/steps/sync_context.py +++ b/test/gui/steps/sync_context.py @@ -115,10 +115,8 @@ def step(context, 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 + with ensure('File is Blacklisted'): + Activity.is_resource_blacklisted(filename).should.be.true @Then('the file "|any|" should be ignored') @@ -128,9 +126,7 @@ def step(context, filename): @Then('the file "{filename}" should be excluded') def step(context, filename): - with ensure( - 'File is Excluded' - ): + with ensure('File is Excluded'): Activity.is_resource_excluded(filename).should.be.true @@ -319,7 +315,7 @@ def step(context): @Then('the following activities should be displayed in not synced table') def step(context): _check_activities(context, not_synced=True) - + @Then('the following activities should not be displayed in synced table') def step(context): @@ -331,7 +327,6 @@ def step(context): _check_activities(context, not_synced=True, should_exist=False) - @When('the user unchecks the "{filter_option}" filter') def step(context, filter_option): Activity.select_not_synced_filter(filter_option) @@ -368,4 +363,4 @@ def step(context): folders.append(row[0]) SyncConnectionWizard.unselect_folders_to_sync( folders, new_sync_connection_wizard=False - ) \ No newline at end of file + ) From d53b0708d91227df86293319ccf9aed25fa5887a Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Wed, 10 Jun 2026 13:08:33 +0545 Subject: [PATCH 74/75] test: rename different timeouts Signed-off-by: Saw-jan --- test/gui/config.sample.ini | 4 +--- test/gui/helpers/ConfigHelper.py | 13 ++++++------- test/gui/helpers/SyncHelper.py | 8 ++++---- test/gui/helpers/api/http_helper.py | 2 +- test/gui/pageObjects/AccountSetting.py | 9 +++++---- test/gui/pageObjects/Activity.py | 8 ++++---- test/gui/pageObjects/SyncConnection.py | 2 +- test/gui/pageObjects/Toolbar.py | 2 +- test/gui/steps/file_context.py | 8 ++++---- 9 files changed, 27 insertions(+), 29 deletions(-) diff --git a/test/gui/config.sample.ini b/test/gui/config.sample.ini index 549027e75d..504e88001d 100644 --- a/test/gui/config.sample.ini +++ b/test/gui/config.sample.ini @@ -2,9 +2,7 @@ APP_PATH= BACKEND_HOST= CLIENT_ROOT_SYNC_PATH= -MAX_SYNC_TIMEOUT= -MIN_SYNC_TIMEOUT= -LOWEST_SYNC_TIMEOUT= +SYNC_TIMEOUT= TEMP_FOLDER_PATH= GUI_TEST_REPORT_DIR= RECORD_VIDEO_ON_FAILURE=false diff --git a/test/gui/helpers/ConfigHelper.py b/test/gui/helpers/ConfigHelper.py index e83717cf9a..995ab93389 100644 --- a/test/gui/helpers/ConfigHelper.py +++ b/test/gui/helpers/ConfigHelper.py @@ -63,9 +63,7 @@ def get_app_env(): CONFIG_ENV_MAP = { 'app_path': 'APP_PATH', 'localBackendUrl': 'BACKEND_HOST', - 'maxSyncTimeout': 'MAX_SYNC_TIMEOUT', - 'minSyncTimeout': 'MIN_SYNC_TIMEOUT', - 'lowestSyncTimeout': 'LOWEST_SYNC_TIMEOUT', + 'sync_timeout': 'SYNC_TIMEOUT', 'clientRootSyncPath': 'CLIENT_ROOT_SYNC_PATH', 'tempFolderPath': 'TEMP_FOLDER_PATH', 'guiTestReportDir': 'GUI_TEST_REPORT_DIR', @@ -85,9 +83,10 @@ def get_app_env(): # default config values CONFIG = { 'localBackendUrl': 'https://localhost:9200/', - 'maxSyncTimeout': 60, - 'minSyncTimeout': 5, - 'lowestSyncTimeout': 1, + 'sync_timeout': 60, + 'max_timeout': 60, + 'min_timeout': 5, + 'lowest_timeout': 1, 'clientRootSyncPath': get_client_root_path(), 'clientConfigFile': os.path.join(get_config_home(), "OpenCloud", APP_CONFIG_FILE), 'guiTestReportDir': os.path.join(CURRENT_DIR.parent, 'reports'), @@ -138,7 +137,7 @@ def init_config(): # Set the default values if empty for key, value in CONFIG.items(): - if key in ('maxSyncTimeout', 'minSyncTimeout'): + if key in ('sync_timeout', 'max_timeout', 'min_timeout', 'lowest_timeout'): CONFIG[key] = builtins.int(value) elif key == 'localBackendUrl': # make sure there is always one trailing slash diff --git a/test/gui/helpers/SyncHelper.py b/test/gui/helpers/SyncHelper.py index e03543782a..750aa14ff1 100644 --- a/test/gui/helpers/SyncHelper.py +++ b/test/gui/helpers/SyncHelper.py @@ -247,13 +247,13 @@ def wait_for_resource_to_sync( listen_sync_status_for_item(resource, resource_type) initial_timeout = 0 - timeout = get_config('maxSyncTimeout') + timeout = get_config('sync_timeout') if patterns is None: patterns = get_synced_pattern(resource) if force_sync: - initial_timeout = 5 + initial_timeout = get_config('min_timeout') # first try with 5 seconds timeout synced = wait_for( lambda: has_sync_pattern(patterns, resource), @@ -349,7 +349,7 @@ def wait_for_resource_to_have_sync_status( listen_sync_status_for_item(resource, resource_type) if not timeout: - timeout = get_config('maxSyncTimeout') + timeout = get_config('sync_timeout') result = wait_for( lambda: has_sync_status(resource, status), @@ -378,7 +378,7 @@ def wait_for_resource_to_have_sync_error(resource, resource_type): def wait_for_client_to_be_ready(): global WAITED_AFTER_SYNC if not WAITED_AFTER_SYNC: - time.sleep(get_config('minSyncTimeout')) + time.sleep(get_config('min_timeout')) WAITED_AFTER_SYNC = True diff --git a/test/gui/helpers/api/http_helper.py b/test/gui/helpers/api/http_helper.py index 515bf9c805..348b2bf315 100644 --- a/test/gui/helpers/api/http_helper.py +++ b/test/gui/helpers/api/http_helper.py @@ -20,7 +20,7 @@ def send_request(url, method, body=None, headers=None, user=None, password=None) verify=False, # in seconds # e.g.: 60 - timeout=get_config("maxSyncTimeout"), + timeout=get_config("max_timeout"), ) diff --git a/test/gui/pageObjects/AccountSetting.py b/test/gui/pageObjects/AccountSetting.py index ec6b36945f..7809e2e451 100644 --- a/test/gui/pageObjects/AccountSetting.py +++ b/test/gui/pageObjects/AccountSetting.py @@ -7,6 +7,7 @@ from helpers.UserHelper import get_displayname_for_user from helpers.AppHelper import app from helpers.Utils import wait_for +from helpers.ConfigHelper import get_config class AccountSetting: @@ -86,7 +87,7 @@ def is_user_signed_in(): return "Connected" in AccountSetting.get_account_connection_label() @staticmethod - def wait_until_connection_is_configured(timeout=5): + def wait_until_connection_is_configured(timeout=get_config('min_timeout')): result = squish.waitFor( AccountSetting.is_connecting, timeout, @@ -100,7 +101,7 @@ def wait_until_connection_is_configured(timeout=5): ) @staticmethod - def wait_until_account_is_connected(timeout=5): + def wait_until_account_is_connected(timeout=get_config('min_timeout')): result = wait_for( AccountSetting.is_user_signed_in, timeout, @@ -115,7 +116,7 @@ def wait_until_account_is_connected(timeout=5): return result @staticmethod - def wait_until_sync_folder_is_configured(timeout=5): + def wait_until_sync_folder_is_configured(timeout=get_config('min_timeout')): result = squish.waitFor( lambda: not squish.waitForObjectExists( AccountSetting.ACCOUNT_LOADING @@ -154,7 +155,7 @@ def remove_connection_for_user(username): AccountSetting.remove_account_connection() @staticmethod - def wait_until_account_is_removed(username, timeout=10): + def wait_until_account_is_removed(username, timeout=get_config('min_timeout')): displayname = get_displayname_for_user(username) displayname = substitute_inline_codes(displayname) diff --git a/test/gui/pageObjects/Activity.py b/test/gui/pageObjects/Activity.py index 30ab783979..76e8b8fde9 100644 --- a/test/gui/pageObjects/Activity.py +++ b/test/gui/pageObjects/Activity.py @@ -68,7 +68,7 @@ def check_file_exist(filename): def is_resource_blacklisted(filename): result = wait_for( lambda: Activity.has_sync_status(filename, "Blacklisted"), - get_config("maxSyncTimeout"), + get_config("sync_timeout"), ) return result @@ -76,7 +76,7 @@ def is_resource_blacklisted(filename): def is_resource_ignored(filename): result = squish.waitFor( lambda: Activity.has_sync_status(filename, "File Ignored"), - get_config("maxSyncTimeout"), + get_config("sync_timeout"), ) return result @@ -84,7 +84,7 @@ def is_resource_ignored(filename): def is_resource_excluded(filename): result = wait_for( lambda: Activity.has_sync_status(filename, "Excluded"), - get_config("maxSyncTimeout"), + get_config("sync_timeout"), ) return result @@ -196,7 +196,7 @@ def check_not_synced_table(resource, status, account): try: file_row = squish.waitForObject( Activity.get_not_synced_file_selector(resource), - get_config("lowestSyncTimeout"), + get_config("lowest_timeout"), )["row"] squish.waitForObjectExists( { diff --git a/test/gui/pageObjects/SyncConnection.py b/test/gui/pageObjects/SyncConnection.py index ab8dd7c3f5..330d312520 100644 --- a/test/gui/pageObjects/SyncConnection.py +++ b/test/gui/pageObjects/SyncConnection.py @@ -118,7 +118,7 @@ def wait_for_error_label(to_exist=True): """Wait for permission error label to appear or disappear""" status = squish.waitFor( lambda: object.exists(SyncConnection.PERMISSION_ERROR_LABEL) == to_exist, - get_config("maxSyncTimeout"), + get_config("max_timeout"), ) if not status: action = "appear" if to_exist else "disappear" diff --git a/test/gui/pageObjects/Toolbar.py b/test/gui/pageObjects/Toolbar.py index a5f5a3d8fa..6d1d689866 100644 --- a/test/gui/pageObjects/Toolbar.py +++ b/test/gui/pageObjects/Toolbar.py @@ -33,7 +33,7 @@ def wait_toolbar_enabled(): toolbar = app().find_element( Toolbar.NAVIGATION_BAR.by, Toolbar.NAVIGATION_BAR.selector ) - timeout = get_config('maxSyncTimeout') + timeout = get_config('max_timeout') enabled = wait_for( toolbar.is_enabled, timeout, diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index e12a3d7405..8291d81364 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -13,7 +13,7 @@ listen_sync_status_for_item, ) from helpers.Utils import wait_for -from helpers.ConfigHelper import get_config, is_windows +from helpers.ConfigHelper import get_config from helpers.FilesHelper import ( build_conflicted_regex, sanitize_path, @@ -28,14 +28,14 @@ ) -def folder_exists(folder_path, timeout=1): +def folder_exists(folder_path, timeout=get_config('min_timeout')): return wait_for( lambda: isdir(sanitize_path(folder_path)), timeout, ) -def file_exists(file_path, timeout=1): +def file_exists(file_path, timeout=get_config('min_timeout')): return wait_for( lambda: isfile(sanitize_path(file_path)), timeout, @@ -212,7 +212,7 @@ def step(context, file_path): def step(context, resource_type, resource): resource_path = get_resource_path(resource) resource_exists = False - timeout = get_config('maxSyncTimeout') + timeout = get_config('max_timeout') if resource_type == 'file': resource_exists = file_exists(resource_path, timeout) else: From 6a30d1217ad64c056323cf9cd8d8b1d1bc7f33c9 Mon Sep 17 00:00:00 2001 From: prashant-gurung899 Date: Wed, 3 Jun 2026 11:50:39 +0545 Subject: [PATCH 75/75] enable edit and remove acc connection tests Signed-off-by: prashant-gurung899 --- test/gui/features/edit-files/edit.feature | 4 +--- .../removeAccountConnection.feature | 2 +- test/gui/steps/file_context.py | 4 ++-- test/gui/steps/server_context.py | 16 +++++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/gui/features/edit-files/edit.feature b/test/gui/features/edit-files/edit.feature index 1de413b6fa..cba497ac23 100644 --- a/test/gui/features/edit-files/edit.feature +++ b/test/gui/features/edit-files/edit.feature @@ -26,14 +26,12 @@ Feature: edit files And the user waits for file "testfile.txt" to be synced Then as "Alice" the file "testfile.txt" should have the content "overwrite openCloud test text file" in the server - @skip + Scenario Outline: Replace and modify the content of a file multiple times Given user "Alice" has set up a client with default settings And the user has copied file "" from outside the sync folder to "/" in the sync folder When the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for the files to sync And the user copies file "" from outside the sync folder to "/" in the sync folder - And the user waits for the files to sync And the user copies file "" from outside the sync folder to "/" in the sync folder And the user waits for the files to sync Then as "Alice" the content of file "" in the server should match the content of local file "" diff --git a/test/gui/features/remove-account-connection/removeAccountConnection.feature b/test/gui/features/remove-account-connection/removeAccountConnection.feature index 39dd3d89e6..3003bf68f3 100644 --- a/test/gui/features/remove-account-connection/removeAccountConnection.feature +++ b/test/gui/features/remove-account-connection/removeAccountConnection.feature @@ -15,7 +15,7 @@ Feature: remove account connection Then "Brian" account should not be displayed But "Alice" account should be added - @skip + Scenario: remove the only account connection Given user "Alice" has been created in the server with default attributes And user "Alice" has created folder "large-folder" in the server diff --git a/test/gui/steps/file_context.py b/test/gui/steps/file_context.py index 8291d81364..d02d09ec61 100644 --- a/test/gui/steps/file_context.py +++ b/test/gui/steps/file_context.py @@ -429,7 +429,7 @@ def step(context, folder_name): @Given( - r'the user has copied file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', + 'the user has copied file "{resource_name}" from outside the sync folder to "{destination}" in the sync folder', regexp=True, ) def step(context, resource_name, destination): @@ -437,7 +437,7 @@ def step(context, resource_name, destination): @When( - r'the user copies file "([^"]*)" from outside the sync folder to "([^"]*)" in the sync folder', + 'the user copies file "{resource_name}" from outside the sync folder to "{destination}" in the sync folder', regexp=True, ) def step(context, resource_name, destination): diff --git a/test/gui/steps/server_context.py b/test/gui/steps/server_context.py index 1b22d5d053..28513dc467 100644 --- a/test/gui/steps/server_context.py +++ b/test/gui/steps/server_context.py @@ -1,8 +1,11 @@ +import tempfile +from pathlib import Path from behave import given as Given, then as Then from sure import ensure from helpers.api import provisioning, webdav_helper as webdav from helpers.TableParser import table_rows_hash +from helpers.FilesHelper import get_file_for_upload, get_document_content @Given('user "{user}" has been created in the server with default attributes') @@ -96,7 +99,7 @@ def step(context, user, file_name, destination): @Then( - 'as "|any|" the content of file "|any|" in the server should match the content of local file "|any|"' + 'as "{user_name}" the content of file "{server_file_name}" in the server should match the content of local file "{local_file_name}"' ) def step(context, user_name, server_file_name, local_file_name): raw_server_content = webdav.get_file_content(user_name, server_file_name) @@ -108,11 +111,10 @@ def step(context, user_name, server_file_name, local_file_name): 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}'", - ) + with ensure( + f"Server file '{server_file_name}' differs from local file '{local_file_name}'", + ): + server_content.should.equal(local_content) @Then('as "{user_name}" following files should not exist in the server',) @@ -124,7 +126,7 @@ def step(context, user_name): f"Resource '{resource_name}' should not exist, but it does", ): resource_exists.should.be.false - + @Given('user "{user}" has uploaded the following files to the server')