Initial QSfera import
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
custom_lib/
|
||||
reports
|
||||
venv/
|
||||
__webdriver/
|
||||
reports/
|
||||
@@ -0,0 +1,87 @@
|
||||
[MASTER]
|
||||
init-hook='import sys; sys.path.append("./shared/scripts")'
|
||||
|
||||
[MAIN]
|
||||
enable=all
|
||||
disable=
|
||||
missing-docstring,
|
||||
global-statement,
|
||||
locally-disabled,
|
||||
suppressed-message,
|
||||
unused-variable,
|
||||
too-many-public-methods,
|
||||
bare-except,
|
||||
fixme,
|
||||
fail-under=10
|
||||
ignore-paths=^tst_.*/test.py$,
|
||||
shared/scripts/names.py,
|
||||
shared/scripts/custom_lib
|
||||
ignored-modules=
|
||||
squish,
|
||||
squishinfo,
|
||||
object,
|
||||
objectmaphelper,
|
||||
test,
|
||||
requests,
|
||||
psutil,
|
||||
urllib3,
|
||||
custom_lib.syncstate,
|
||||
load-plugins=
|
||||
pylint.extensions.check_elif,
|
||||
pylint.extensions.bad_builtin,
|
||||
pylint.extensions.code_style,
|
||||
pylint.extensions.overlapping_exceptions,
|
||||
pylint.extensions.typing,
|
||||
pylint.extensions.redefined_variable_type,
|
||||
py-version=3.10
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
[BASIC]
|
||||
attr-naming-style=snake_case
|
||||
; TODO: enable it later
|
||||
; class-attribute-naming-style=snake_case
|
||||
class-const-naming-style=UPPER_CASE
|
||||
class-naming-style=PascalCase
|
||||
const-naming-style=UPPER_CASE
|
||||
function-naming-style=snake_case
|
||||
inlinevar-naming-style=snake_case
|
||||
method-naming-style=snake_case
|
||||
module-naming-style=any
|
||||
variable-naming-style=snake_case
|
||||
|
||||
[DESIGN]
|
||||
max-args=8
|
||||
|
||||
[VARIABLES]
|
||||
additional-builtins=
|
||||
squish,
|
||||
squishinfo,
|
||||
test,
|
||||
testSettings,
|
||||
OnFeatureStart,
|
||||
OnFeatureEnd,
|
||||
OnScenarioStart,
|
||||
OnScenarioEnd,
|
||||
QApplication,
|
||||
Given,
|
||||
When,
|
||||
Then,
|
||||
Step,
|
||||
allow-global-unused-variables=no
|
||||
dummy-variables-rgx=^hook$|^_*$|^step$
|
||||
ignored-argument-names=^context$|^_*$
|
||||
|
||||
[STRING]
|
||||
check-quote-consistency=yes
|
||||
|
||||
[FORMAT]
|
||||
indent-string=' '
|
||||
max-line-length=120
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=object,test
|
||||
|
||||
[SIMILARITIES]
|
||||
ignore-comments=yes
|
||||
ignore-docstrings=yes
|
||||
ignore-imports=yes
|
||||
@@ -0,0 +1 @@
|
||||
ATSPI_WEBDRIVER_VERSION="6682aeb734730c50949f0fda69827b6c5b50dbc7"
|
||||
@@ -0,0 +1,30 @@
|
||||
PYTHON_LINT_PATHS:="./**/*.py"
|
||||
|
||||
.PHONY: install
|
||||
install: pip-install install-chromium
|
||||
|
||||
.PHONY: install-chromium
|
||||
install-chromium:
|
||||
playwright install chromium
|
||||
|
||||
.PHONY: pip-install
|
||||
pip-install:
|
||||
python3 -m pip install -r requirements.txt
|
||||
|
||||
.PHONY: python-lint
|
||||
python-lint:
|
||||
black --check --diff .
|
||||
python3 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS)
|
||||
|
||||
.PHONY: python-lint-fix
|
||||
python-lint-fix:
|
||||
black .
|
||||
python3 -m pylint --rcfile ./.pylintrc $(PYTHON_LINT_PATHS)
|
||||
|
||||
.PHONY: gherkin-lint
|
||||
gherkin-lint:
|
||||
gherlint -c ./config/.gherlintrc.json .
|
||||
|
||||
.PHONY: gherkin-lint-fix
|
||||
gherkin-lint-fix:
|
||||
gherlint --fix -c ./config/.gherlintrc.json .
|
||||
@@ -0,0 +1,21 @@
|
||||
[behave]
|
||||
paths=features
|
||||
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 = 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
|
||||
@@ -0,0 +1,10 @@
|
||||
[DEFAULT]
|
||||
APP_PATH=
|
||||
BACKEND_HOST=
|
||||
CLIENT_ROOT_SYNC_PATH=
|
||||
MAX_SYNC_TIMEOUT=
|
||||
MIN_SYNC_TIMEOUT=
|
||||
LOWEST_SYNC_TIMEOUT=
|
||||
TEMP_FOLDER_PATH=
|
||||
GUI_TEST_REPORT_DIR=
|
||||
RECORD_VIDEO_ON_FAILURE=false
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rules": {
|
||||
"indentation": ["error", 4],
|
||||
"no_repetitive_step_keyword": "error",
|
||||
"require_step": "error",
|
||||
"no_trailing_whitespace": "error",
|
||||
"require_scenario": "error",
|
||||
"no_then_as_first_step": "error",
|
||||
"only_given_step_in_background": "error",
|
||||
"newline_before_scenario": ["error", 2, true],
|
||||
"lowercase_title": "off",
|
||||
"no_but_in_given_when": "error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import shutil
|
||||
import os
|
||||
import re
|
||||
import pyautogui
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
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 cleanup_app_log():
|
||||
if os.path.exists(get_config('currentAppLogFile')):
|
||||
os.remove(get_config('currentAppLogFile'))
|
||||
|
||||
|
||||
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()
|
||||
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"{scenario}_{timestamp}.png")
|
||||
pyautogui.screenshot(file_path)
|
||||
|
||||
|
||||
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")):
|
||||
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_project_spaces()
|
||||
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()
|
||||
clear_socket_messages()
|
||||
@@ -0,0 +1,57 @@
|
||||
Feature: filter activity for user
|
||||
As a user
|
||||
I want to filter activity
|
||||
So that I can view activity of specific user
|
||||
|
||||
@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 user "Brian" has created folder "brian-folder" in the server
|
||||
And the user has set up the following accounts with default settings:
|
||||
| users |
|
||||
| Alice |
|
||||
| Brian |
|
||||
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
|
||||
| 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 | Brian Murphy@%local_server_hostname% |
|
||||
|
||||
@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
|
||||
And user "Alice" has created a folder "Folder1" inside the sync folder
|
||||
When user "Alice" creates the following files inside the sync folder:
|
||||
| files |
|
||||
| /.htaccess |
|
||||
| /Folder1/a\\a.txt |
|
||||
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
|
||||
When the user unchecks the "Excluded" filter
|
||||
Then the following activities should be displayed in not synced table
|
||||
| resource | status | account |
|
||||
| Folder1/a\\a.txt | Blacklisted | Alice Hansen@%local_server_hostname% |
|
||||
|
||||
@skipOnLinux
|
||||
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
|
||||
When user "Alice" creates the following files inside the sync folder:
|
||||
| files |
|
||||
| /.htaccess |
|
||||
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
|
||||
Then the following activities should not be displayed in not synced table
|
||||
| resource | status | account |
|
||||
| .htaccess | Excluded | Alice Hansen@%local_server_hostname% |
|
||||
@@ -0,0 +1,91 @@
|
||||
Feature: adding accounts
|
||||
As a user
|
||||
I want to be able join multiple QSfera servers to the client
|
||||
So that I can sync data with various organisations
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
|
||||
|
||||
Scenario: Check default options in advanced configuration
|
||||
Given 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 opens the advanced configuration
|
||||
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:
|
||||
| server | %local_server% |
|
||||
| user | Alice |
|
||||
| password | 1234 |
|
||||
Then "Alice" account should be added
|
||||
|
||||
@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
|
||||
When the user opens the add-account dialog
|
||||
And the user adds the following account:
|
||||
| server | %local_server% |
|
||||
| user | Brian |
|
||||
| password | AaBb2Cc3Dd4 |
|
||||
Then "Brian" account should be opened
|
||||
And "Alice" account should be added
|
||||
|
||||
|
||||
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%"
|
||||
And the user accepts the certificate
|
||||
Then credentials wizard should be visible
|
||||
When the user adds the following account:
|
||||
| user | Alice |
|
||||
| password | 1234 |
|
||||
Then "Alice" account should be opened
|
||||
|
||||
@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
|
||||
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 syncs the "Personal" space
|
||||
Then the folder "simple-folder" should exist on the file system
|
||||
|
||||
|
||||
Scenario: Check for suffix when sync path exists
|
||||
Given the user has created folder "QSfera" in the default home path
|
||||
And the user has started the client
|
||||
And the user has entered the following account information:
|
||||
| server | %local_server% |
|
||||
When the user adds the following user credentials:
|
||||
| user | Alice |
|
||||
| password | 1234 |
|
||||
And the user opens the advanced configuration
|
||||
Then the default local sync path should contain "%home%/QSfera (2)" in the configuration wizard
|
||||
When the user selects download everything option in advanced section
|
||||
Then the button to open sync connection wizard should be disabled
|
||||
|
||||
@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
|
||||
And user "Alice" has set up a client with default settings
|
||||
And the user has removed the connection for user "Alice"
|
||||
When the user opens the add-account dialog
|
||||
And the user adds the following account:
|
||||
| server | %local_server% |
|
||||
| user | Alice |
|
||||
| password | 1234 |
|
||||
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
|
||||
@@ -0,0 +1,80 @@
|
||||
Feature: deleting files and folders
|
||||
As a user
|
||||
I want to delete files and folders
|
||||
So that I can keep my filing system clean and tidy
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
|
||||
@issue-9439 @smoke
|
||||
Scenario Outline: Delete a file
|
||||
Given user "Alice" has uploaded file with content "openCloud test text file 0" to "<fileName>" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When the user deletes the file "<fileName>"
|
||||
And the user waits for file "<fileName>" to be synced
|
||||
Then as "Alice" file "<fileName>" should not exist in the server
|
||||
Examples:
|
||||
| fileName |
|
||||
| textfile0.txt |
|
||||
| textfile0-with-name-more-than-20-characters |
|
||||
| ~`!@#$^&()-_=+{[}];',textfile.txt |
|
||||
|
||||
@issue-9439 @smoke
|
||||
Scenario Outline: Delete a folder
|
||||
Given user "Alice" has created folder "<folderName>" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When the user deletes the folder "<folderName>"
|
||||
And the user waits for folder "<folderName>" to be synced
|
||||
Then as "Alice" file "<folderName>" should not exist in the server
|
||||
Examples:
|
||||
| folderName |
|
||||
| simple-empty-folder |
|
||||
| simple-folder-with-name-more-than-20-characters |
|
||||
|
||||
@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
|
||||
And user "Alice" has created folder "test-folder1" in the server
|
||||
And user "Alice" has created folder "test-folder2" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When the user deletes the file "textfile1.txt"
|
||||
And the user deletes the folder "test-folder1"
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" file "textfile1.txt" should not exist in the server
|
||||
And as "Alice" folder "test-folder1" should not exist in the server
|
||||
And as "Alice" file "textfile2.txt" should exist in the server
|
||||
And as "Alice" folder "test-folder2" should exist in the server
|
||||
|
||||
|
||||
Scenario: Delete multiple files
|
||||
Given user "Alice" has uploaded the following files to the server
|
||||
| file | content |
|
||||
| textfile0.txt | openCloud test text file 0 |
|
||||
| textfile1.txt | openCloud test text file 1 |
|
||||
| textfile2.txt | openCloud test text file 2 |
|
||||
And user "Alice" has set up a client with default settings
|
||||
When the user deletes the following files
|
||||
| file |
|
||||
| textfile0.txt |
|
||||
| textfile1.txt |
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" following files should not exist in the server
|
||||
| file |
|
||||
| textfile0.txt |
|
||||
| textfile1.txt |
|
||||
And as "Alice" file "textfile2.txt" should exist in the server
|
||||
|
||||
|
||||
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 "<fileName>" with the following content inside the sync folder
|
||||
"""
|
||||
special characters
|
||||
"""
|
||||
And the user deletes the file "<fileName>"
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" file "<fileName>" should not exist in the server
|
||||
Examples:
|
||||
| fileName |
|
||||
| ~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️🔥.txt |
|
||||
@@ -0,0 +1,44 @@
|
||||
Feature: edit files
|
||||
As a user
|
||||
I want to be able to edit the file content
|
||||
So that I can modify and change file data
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
|
||||
@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
|
||||
When the user overwrites the file "S@mpleFile!With,$pecial&Characters.txt" with content "overwrite openCloud test text file"
|
||||
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
|
||||
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
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the user waits for file "testfile.txt" to be synced
|
||||
And the user overwrites the file "testfile.txt" with content "overwrite openCloud test text file"
|
||||
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
|
||||
|
||||
|
||||
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 "<initialFile>" from outside the sync folder to "/" in the sync folder
|
||||
When the user copies file "<updateFile1>" from outside the sync folder to "/<initialFile>" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
And the user copies file "<updateFile2>" from outside the sync folder to "/<initialFile>" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
And the user copies file "<updateFile3>" from outside the sync folder to "/<initialFile>" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the content of file "<initialFile>" in the server should match the content of local file "<updateFile3>"
|
||||
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 |
|
||||
@@ -0,0 +1,23 @@
|
||||
Feature: Logout users
|
||||
As a user
|
||||
I want to be able to login and logout of my account
|
||||
So that I can protect my work and identity and be assured of privacy
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
|
||||
@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
|
||||
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
|
||||
When user "Alice" logs in using the client-UI
|
||||
Then user "Alice" should be connected to the server
|
||||
When the user quits the client
|
||||
And the user starts the client
|
||||
Then user "Alice" should be connected to the server
|
||||
@@ -0,0 +1,90 @@
|
||||
Feature: move file and folder
|
||||
As a user
|
||||
I want to be able to move a folder and a file
|
||||
So that I can organize my files and folders
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
And user "Alice" has created folder "folder1" in the server
|
||||
And user "Alice" has created folder "folder1/folder2" in the server
|
||||
And user "Alice" has created folder "folder1/folder2/folder3" in the server
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves file "folder1/folder2/folder3/folder4/folder5/lorem.txt" to "/" in the sync folder
|
||||
And user "Alice" moves folder "folder1/folder2/folder3/folder4/folder5/test-folder" to "/" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the file "lorem.txt" should have the content "openCloud" in the server
|
||||
And as "Alice" folder "test-folder" should exist in the server
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has uploaded file with content "openCloud test" to "testFile.txt" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves folder "test-folder1" to "folder1/folder2/folder3/folder4/folder5" in the sync folder
|
||||
And user "Alice" moves folder "test-folder2" to "folder1/folder2/folder3/folder4/folder5" in the sync folder
|
||||
And user "Alice" moves file "testFile.txt" to "folder1/folder2/folder3/folder4/folder5" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the file "folder1/folder2/folder3/folder4/folder5/testFile.txt" should have the content "openCloud test" in the server
|
||||
And as "Alice" folder "folder1/folder2/folder3/folder4/folder5/test-folder1" should exist in the server
|
||||
And as "Alice" folder "folder1/folder2/folder3/folder4/folder5/test-folder2" should exist in the server
|
||||
And as "Alice" file "testFile.txt" should not exist in the server
|
||||
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
|
||||
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
|
||||
When the user renames a file "textfile.txt" to "lorem.txt"
|
||||
And the user renames a folder "folder1" to "FOLDER"
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the file "lorem.txt" should have the content "test file 1" in the server
|
||||
And as "Alice" folder "FOLDER" should exist in the server
|
||||
But as "Alice" file "textfile.txt" should not exist in the server
|
||||
And as "Alice" folder "folder1" should not exist in the server
|
||||
|
||||
@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
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves file "folder1/file1.txt" to "folder1/folder2" in the sync folder
|
||||
And user "Alice" moves file "folder1/file2.txt" to "folder1/folder2" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the file "folder1/folder2/file1.txt" should have the content "test file 1" in the server
|
||||
And as "Alice" the file "folder1/folder2/file2.txt" should have the content "test file 2" in the server
|
||||
And as "Alice" file "folder1/file1.txt" should not exist in the server
|
||||
And as "Alice" file "folder1/file2.txt" should not exist in the server
|
||||
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves file "folder1/folder2/lorem.txt" to "/" in the sync folder
|
||||
And user "Alice" moves folder "folder1/folder2/folder3/folder4/test-folder" to "/" in the sync folder
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" the file "lorem.txt" should have the content "openCloud" in the server
|
||||
And as "Alice" folder "test-folder" should exist in the server
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
@@ -0,0 +1,28 @@
|
||||
Feature: remove account connection
|
||||
As a user
|
||||
I want to remove my account
|
||||
So that I won't be using any client-UI services
|
||||
|
||||
@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 "Brian" account should not be displayed
|
||||
But "Alice" account should be added
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has uploaded file with content "test content" to "testFile.txt" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When the user removes the connection for user "Alice"
|
||||
Then the settings tab should have the following options in the general section:
|
||||
| Start on Login |
|
||||
And the folder "large-folder" should exist on the file system
|
||||
And the file "testFile.txt" should exist on the file system
|
||||
@@ -0,0 +1,98 @@
|
||||
Feature: Project spaces
|
||||
As a user
|
||||
I want to sync project space
|
||||
So that I can do view and manage the space
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
And the administrator has created a space "Project101"
|
||||
|
||||
@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"
|
||||
And the administrator has added user "Alice" to space "Project101" with role "viewer"
|
||||
And user "Alice" has set up a client with space "Project101"
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
And the administrator has added user "Alice" to space "Project101" with role "viewer"
|
||||
And user "Alice" has set up a client with space "Project101"
|
||||
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
|
||||
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"
|
||||
And the administrator has added user "Alice" to space "Project101" with role "editor"
|
||||
And user "Alice" has set up a client with space "Project101"
|
||||
When the user overwrites the file "testfile.txt" with content "some content edited"
|
||||
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
|
||||
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"
|
||||
When user "Alice" creates a file "localFile.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And user "Alice" creates a folder "localFolder" inside the sync folder
|
||||
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
|
||||
|
||||
@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"
|
||||
And user "Alice" has set up a client with space "Project101"
|
||||
When the user renames a file "testfile.txt" to "renamedFile.txt"
|
||||
And the user waits for file "renamedFile.txt" to be synced
|
||||
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
|
||||
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 for user "Alice" sync folder "Project101" should not be displayed
|
||||
But the file "testfile.txt" should exist on the file system
|
||||
|
||||
|
||||
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"
|
||||
When user "Alice" creates a folder "simple-folder" inside the sync folder
|
||||
Then the following error message should appear in the client
|
||||
"""
|
||||
simple-folder: Not allowed because you don't have permission to add subfolders to that folder
|
||||
"""
|
||||
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 |
|
||||
| simple-folder | Blacklisted | Alice Hansen@%local_server_hostname% |
|
||||
|
||||
@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
|
||||
And user "Alice" has uploaded file with content "test content" to "simple-folder/uploaded-lorem.txt" in the server
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | simple-folder |
|
||||
| sharee | Brian |
|
||||
| permissionsRole | Editor |
|
||||
And user "Brian" has set up a client with space "Shares"
|
||||
When user "Brian" deletes the folder "Shares/simple-folder" in the server
|
||||
And the user waits for the files to sync
|
||||
Then the folder "simple-folder" should not exist on the file system
|
||||
@@ -0,0 +1,610 @@
|
||||
Feature: Syncing files
|
||||
As a user
|
||||
I want to be able to sync my local folders to to my QSfera server
|
||||
so that I dont have to upload and download files manually
|
||||
|
||||
Background:
|
||||
Given user "Alice" has been created in the server with default attributes
|
||||
|
||||
@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
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the user waits for file "lorem-for-upload.txt" to be synced
|
||||
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
|
||||
|
||||
@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
|
||||
And user "Alice" has uploaded file with content "test content" to "uploaded-lorem.txt" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
Then the file "uploaded-lorem.txt" should exist on the file system
|
||||
And the file "uploaded-lorem.txt" should exist on the file system with the following content
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the folder "simple-folder" should exist on the file system
|
||||
And the folder "large-folder" should exist on the file system
|
||||
|
||||
@issue-9733
|
||||
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
|
||||
And the user has paused the file sync
|
||||
And the user has changed the content of local file "conflict.txt" to:
|
||||
"""
|
||||
client content
|
||||
"""
|
||||
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 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
|
||||
"""
|
||||
changed server content
|
||||
"""
|
||||
And a conflict file for "conflict.txt" should exist on the file system with the following content
|
||||
"""
|
||||
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
|
||||
And user "Alice" has uploaded file with content "test content" to "testFile.txt" in the server
|
||||
And user "Alice" has uploaded file with content "lorem content" to "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 navigates back in the sync connection wizard
|
||||
And the user sets the temp folder "localSyncFolder" as local sync path in sync connection wizard
|
||||
And the user selects "Personal" space in sync connection wizard
|
||||
Then the sync all checkbox should be checked
|
||||
When 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 file "testFile.txt" should exist on the file system
|
||||
And the file "lorem.txt" should exist on the file system
|
||||
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
|
||||
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
|
||||
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 the user selects the following folders to sync:
|
||||
| folder |
|
||||
| simple-folder |
|
||||
Then the folder "simple-folder" should exist on the file system
|
||||
But the folder "large-folder" should not exist on the file system
|
||||
When user "Alice" uploads file with content "some content" to "simple-folder/lorem.txt" in the server
|
||||
And user "Alice" uploads file with content "openCloud" to "large-folder/lorem.txt" in the server
|
||||
And user "Alice" creates a file "simple-folder/localFile.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the user waits for the files to sync
|
||||
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
|
||||
|
||||
@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
|
||||
And user "Alice" has created folder "aFolder" in the server
|
||||
And user "Alice" has uploaded file with content "more contents" to "aFolder/lorem.txt" in the server
|
||||
And user "Alice" has created folder "bFolder" 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
|
||||
# folders are sorted by name in ascending order by default
|
||||
Then the folders should be in the following order:
|
||||
| folder |
|
||||
| 123Folder |
|
||||
| aFolder |
|
||||
| bFolder |
|
||||
# sort folder by name in descending order
|
||||
When the user sorts the folder list by "Name"
|
||||
Then the folders should be in the following order:
|
||||
| folder |
|
||||
| bFolder |
|
||||
| aFolder |
|
||||
| 123Folder |
|
||||
# sort folder by size in ascending order
|
||||
When the user sorts the folder list by "Size"
|
||||
Then the folders should be in the following order:
|
||||
| folder |
|
||||
| bFolder |
|
||||
| 123Folder |
|
||||
| aFolder |
|
||||
# sort folder by size in descending order
|
||||
When the user sorts the folder list by "Size"
|
||||
Then the folders should be in the following order:
|
||||
| folder |
|
||||
| aFolder |
|
||||
| 123Folder |
|
||||
| bFolder |
|
||||
And the user cancels the sync connection wizard
|
||||
|
||||
@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 <foldername> inside the sync folder
|
||||
And the user waits for folder <foldername> to be synced
|
||||
Then as "Alice" folder <foldername> should exist in the server
|
||||
Examples:
|
||||
| foldername |
|
||||
| "myFolder" |
|
||||
| "really long folder name with some spaces and special char such as $%ñ&" |
|
||||
|
||||
@skipOnWindows
|
||||
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 <foldername> inside the sync folder
|
||||
And the user waits for folder <foldername> to be synced
|
||||
Then as "Alice" folder <foldername> should exist in the server
|
||||
Examples:
|
||||
| foldername |
|
||||
| "folder with space at end " |
|
||||
|
||||
@skipOnLinux
|
||||
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
|
||||
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 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
|
||||
|
||||
@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
|
||||
When user "Alice" creates a folder "parent/subfolderEmpty1" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolderEmpty2" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolderEmpty3" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolderEmpty4" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolderEmpty5" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolder1" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolder2" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolder3" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolder4" inside the sync folder
|
||||
And user "Alice" creates a folder "parent/subfolder5" inside the sync folder
|
||||
And user "Alice" creates a file "parent/subfolder1/test.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And user "Alice" creates a file "parent/subfolder2/test.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And user "Alice" creates a file "parent/subfolder3/test.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And user "Alice" creates a file "parent/subfolder4/test.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And user "Alice" creates a file "parent/subfolder5/test.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
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
|
||||
And as "Alice" folder "parent/subfolderEmpty4" should exist in the server
|
||||
And as "Alice" folder "parent/subfolderEmpty5" should exist in the server
|
||||
And as "Alice" folder "parent/subfolder1" should exist in the server
|
||||
And as "Alice" folder "parent/subfolder2" should exist in the server
|
||||
And as "Alice" folder "parent/subfolder3" should exist in the server
|
||||
And as "Alice" folder "parent/subfolder4" should exist in the server
|
||||
And as "Alice" folder "parent/subfolder5" should exist in the server
|
||||
|
||||
@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
|
||||
And user "Alice" creates a file "original/localFile.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
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 "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
|
||||
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
|
||||
When user "Alice" creates a folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" inside the sync folder
|
||||
And the user waits for folder "Folder1/thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks-thisIsAVeryLongFolderNameToCheckThatItWorks" to be synced
|
||||
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
|
||||
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
|
||||
And user "Alice" has created a folder "Folder1/subFolder1/subFolder2" inside the sync folder
|
||||
And user "Alice" has set up a client with default settings
|
||||
Then as "Alice" folder "Folder1" should exist in the server
|
||||
And as "Alice" folder "Folder1/subFolder1" should exist in the server
|
||||
And as "Alice" folder "Folder1/subFolder1/subFolder2" should exist in the server
|
||||
|
||||
@skipOnWindows
|
||||
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
|
||||
When user "Alice" creates a file "Folder1/a\\a.txt" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
Scenario Outline: Sync long nested folder
|
||||
Given user "Alice" has created folder "<foldername>" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" creates a folder "<foldername>/<foldername>" inside the sync folder
|
||||
And user "Alice" creates a folder "<foldername>/<foldername>/<foldername>" inside the sync folder
|
||||
And user "Alice" creates a folder "<foldername>/<foldername>/<foldername>/<foldername>" inside the sync folder
|
||||
And user "Alice" creates a folder "<foldername>/<foldername>/<foldername>/<foldername>/<foldername>" inside the sync folder
|
||||
And the user waits for folder "<foldername>/<foldername>/<foldername>/<foldername>/<foldername>" to be synced
|
||||
Then as "Alice" folder "<foldername>/<foldername>" should exist in the server
|
||||
And as "Alice" folder "<foldername>/<foldername>/<foldername>" should exist in the server
|
||||
And as "Alice" folder "<foldername>/<foldername>/<foldername>/<foldername>" should exist in the server
|
||||
And as "Alice" folder "<foldername>/<foldername>/<foldername>/<foldername>/<foldername>" should exist in the server
|
||||
Examples:
|
||||
| foldername |
|
||||
| An empty folder which name is obviously more than 59 characters |
|
||||
|
||||
@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
|
||||
And user "Alice" has uploaded file with content "server content" to "/PRN" in the server
|
||||
And user "Alice" has uploaded file with content "server content" to "/foo%" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
Then the folder "CON" should exist on the file system
|
||||
And the folder "test%" should exist on the file system
|
||||
And the file "PRN" should exist on the file system
|
||||
And the file "foo%" should exist on the file system
|
||||
And as "Alice" folder "CON" should exist in the server
|
||||
And as "Alice" folder "test%" should exist in the server
|
||||
And as "Alice" file "/PRN" should exist in the server
|
||||
And as "Alice" file "/foo%" should exist in the server
|
||||
|
||||
@skipOnLinux
|
||||
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
|
||||
And user "Alice" has uploaded file with content "server content" to "/PRN" in the server
|
||||
And user "Alice" has uploaded file with content "server content" to "/foo%" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
Then the folder "test%" should exist on the file system
|
||||
And the file "foo%" should exist on the file system
|
||||
But the folder "CON" should not exist on the file system
|
||||
And the file "PRN" should not exist on the file system
|
||||
|
||||
@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
|
||||
And user "Alice" has uploaded file "testavatar.jpg" to "simple-folder/testavatar.jpg" in the server
|
||||
And user "Alice" has uploaded file "testavatar.jpeg" to "simple-folder/testavatar.jpeg" in the server
|
||||
And user "Alice" has uploaded file "testimage.mp3" to "simple-folder/testimage.mp3" in the server
|
||||
And user "Alice" has uploaded file "test_video.mp4" to "simple-folder/test_video.mp4" in the server
|
||||
And user "Alice" has uploaded file "simple.pdf" to "simple-folder/simple.pdf" in the server
|
||||
And user "Alice" has uploaded file "simple.docx" to "simple-folder/simple.docx" in the server
|
||||
And user "Alice" has uploaded file "simple.pptx" to "simple-folder/simple.pptx" in the server
|
||||
And user "Alice" has uploaded file "simple.xlsx" to "simple-folder/simple.xlsx" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
Then the folder "simple-folder" should exist on the file system
|
||||
And the file "simple-folder/testavatar.png" should exist on the file system
|
||||
And the file "simple-folder/testavatar.jpg" should exist on the file system
|
||||
And the file "simple-folder/testavatar.jpeg" should exist on the file system
|
||||
And the file "simple-folder/testimage.mp3" should exist on the file system
|
||||
And the file "simple-folder/test_video.mp4" should exist on the file system
|
||||
And the file "simple-folder/simple.pdf" should exist on the file system
|
||||
And the file "simple-folder/simple.docx" should exist on the file system
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
| files |
|
||||
| /testavatar.png |
|
||||
| /testavatar.jpg |
|
||||
| /testavatar.jpeg |
|
||||
| /testaudio.mp3 |
|
||||
| /test_video.mp4 |
|
||||
| /simple.txt |
|
||||
| /simple.docx |
|
||||
| /simple.pptx |
|
||||
| /simple.xlsx |
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" file "testavatar.png" should exist in the server
|
||||
And as "Alice" file "testavatar.jpg" should exist in the server
|
||||
And as "Alice" file "testavatar.jpeg" should exist in the server
|
||||
And as "Alice" file "testaudio.mp3" should exist in the server
|
||||
And as "Alice" file "test_video.mp4" should exist in the server
|
||||
And as "Alice" file "simple.txt" should exist in the server
|
||||
And as "Alice" file "simple.docx" should exist in the server
|
||||
And as "Alice" file "simple.pptx" should exist in the server
|
||||
And as "Alice" file "simple.xlsx" should exist in the server
|
||||
|
||||
@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 "<filename>" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the user waits for file "<filename>" to be synced
|
||||
Then as "Alice" file "<filename>" should exist in the server
|
||||
Examples:
|
||||
| filename |
|
||||
| thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIs.txt |
|
||||
|
||||
@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
|
||||
And the user waits for file "newfile.txt" to be synced
|
||||
Then as "Alice" file "newfile.txt" should exist in the server
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
test contents
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
And the user has created a folder "folder2" in temp folder
|
||||
And the user has created "500" files each of size "1048576" bytes inside folder "folder2" in temp folder
|
||||
And the user has created a folder "folder3" in temp folder
|
||||
And the user has created "1000" files each of size "1048576" bytes inside folder "folder3" in temp folder
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves folder "folder1" from the temp folder into the sync folder
|
||||
And user "Alice" moves folder "folder2" from the temp folder into the sync folder
|
||||
And user "Alice" moves folder "folder3" from the temp folder into the sync folder
|
||||
And the user waits for folder "folder1" to be synced
|
||||
And the user waits for folder "folder2" to be synced
|
||||
And the user waits for folder "folder3" to be synced
|
||||
Then as "Alice" folder "folder1" should exist in the server
|
||||
And as user "Alice" folder "folder1" should contain "500" items in the server
|
||||
And as "Alice" folder "folder2" should exist in the server
|
||||
And as user "Alice" folder "folder2" should contain "500" items in the server
|
||||
And as "Alice" folder "folder3" should exist in the server
|
||||
And as user "Alice" folder "folder3" should contain "1000" items in the server
|
||||
|
||||
@smoke
|
||||
Scenario: Skip sync folder configuration
|
||||
Given 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 cancels the sync connection wizard
|
||||
Then "Alice" account should be added
|
||||
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
|
||||
Given the user has created a zip file "archive.zip" with the following resources in the temp folder
|
||||
| resource | type | content |
|
||||
| folder1 | folder | |
|
||||
| folder2 | folder | |
|
||||
| file1.txt | file | Test file1 |
|
||||
| file2.txt | file | Test file2 |
|
||||
And user "Alice" has set up a client with default settings
|
||||
When user "Alice" moves file "archive.zip" from the temp folder into the sync folder
|
||||
And user "Alice" unzips the zip file "archive.zip" inside the sync root
|
||||
And the user waits for the files to sync
|
||||
Then as "Alice" folder "folder1" should exist in the server
|
||||
And as "Alice" folder "folder2" should exist in the server
|
||||
And as "Alice" the file "file1.txt" should have the content "Test file1" in the server
|
||||
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
|
||||
And 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 created folder "~test%" in the server
|
||||
And the user has created a folder "~`!@#$^&()-_=+{[}];',)PRN%" in temp folder
|
||||
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 temp folder "~`!@#$^&()-_=+{[}];',)PRN%" as local sync path in sync connection wizard
|
||||
And the user selects "Personal" space in sync connection wizard
|
||||
And the user selects the following folders to sync:
|
||||
| folder |
|
||||
| ~`!@#$^&()-_=+{[}];',) |
|
||||
| simple-folder |
|
||||
| test-folder/sub-folder2 |
|
||||
Then the folder "~`!@#$^&()-_=+{[}];',)" should exist on the file system
|
||||
And the folder "simple-folder" should exist on the file system
|
||||
But the folder "~test%" should not exist on the file system
|
||||
When user "Alice" deletes the folder "simple-folder" in the server
|
||||
And the user waits for the files to sync
|
||||
Then the folder "simple-folder" should not exist on the file system
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
When the user removes the folder sync connection
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has uploaded file with content "test content" to "simple-folder/uploaded-lorem.txt" in the server
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | simple-folder |
|
||||
| sharee | Brian |
|
||||
| permissionsRole | Viewer |
|
||||
And user "Brian" has set up a client with space "Shares"
|
||||
When user "Brian" creates a folder "simple-folder/sub-folder" inside the sync folder
|
||||
Then the folder "simple-folder/sub-folder" should exist on the file system
|
||||
But the following error message should appear in the client
|
||||
"""
|
||||
simple-folder/sub-folder: Not allowed because you don't have permission to add subfolders to that folder
|
||||
"""
|
||||
When the user copies file "simple.pdf" from outside the sync folder to "simple-folder/simple.pdf" in the sync folder
|
||||
Then the file "simple-folder/simple.pdf" should exist on the file system
|
||||
But the following error message should appear in the client
|
||||
"""
|
||||
simple-folder/simple.pdf: Not allowed because you don't have permission to add files in that folder
|
||||
"""
|
||||
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 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 |
|
||||
| simple-folder/sub-folder | Blacklisted | Brian Murphy@%local_server_hostname% |
|
||||
| simple-folder/simple.pdf | Blacklisted | Brian Murphy@%local_server_hostname% |
|
||||
|
||||
|
||||
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 "<filename>" with the following content inside the sync folder
|
||||
"""
|
||||
test content
|
||||
"""
|
||||
And the user waits for file "<filename>" to be synced
|
||||
Then as "Alice" file "<filename>" should exist in the server
|
||||
Examples:
|
||||
| filename |
|
||||
| 𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺𒁻𒁼𒁾𒁿𒁰𒁱𒁲𒁳𒁴𒁵𒁶𒁷𒁸𒁹𒁺abôǣฎพฒฆ๘ตกกผพฒณญไใๅำ๊๒๔๗๘รศฬอฮ.txt |
|
||||
|
||||
|
||||
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
|
||||
And user "Alice" has uploaded file with content "test content" to "simple-folder/uploaded-lorem.txt" in the server
|
||||
And user "Alice" has sent the following resource share invitation:
|
||||
| resource | simple-folder |
|
||||
| sharee | Brian |
|
||||
| permissionsRole | Editor |
|
||||
And user "Brian" has set up a client with space "Shares"
|
||||
When user "Brian" creates a folder "simple-folder/sub-folder" inside the sync folder
|
||||
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
|
||||
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
|
||||
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
|
||||
And user "Alice" has created folder "test-folder/sub-folder2" in the server
|
||||
And user "Alice" has set up a client with default settings
|
||||
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 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
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
Feature: Visually check all tabs
|
||||
As a user
|
||||
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
|
||||
Then the toolbar should have the following tabs:
|
||||
| Add Account |
|
||||
| Activity |
|
||||
| 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 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:
|
||||
| Sync hidden files |
|
||||
| Edit ignored files |
|
||||
| Log settings |
|
||||
And the settings tab should have the following options in the network section:
|
||||
| Download Bandwidth |
|
||||
| Upload Bandwidth |
|
||||
When the user opens the about dialog
|
||||
Then the about dialog should be opened
|
||||
@@ -0,0 +1,174 @@
|
||||
@skipOnLinux
|
||||
Feature: VFS support
|
||||
As a user
|
||||
I want to sync files with vfs
|
||||
So that I can decide which files to download
|
||||
|
||||
|
||||
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 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 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 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 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
|
||||
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 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
|
||||
|
||||
|
||||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
Binary file not shown.
@@ -0,0 +1,119 @@
|
||||
import pyautogui
|
||||
import psutil
|
||||
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
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_window_location():
|
||||
window = (
|
||||
app()
|
||||
.find_element(By.XPATH, "//*[contains(@name,'КуСфера')]")
|
||||
.location
|
||||
)
|
||||
return window['x'], window['y']
|
||||
@@ -0,0 +1,185 @@
|
||||
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 = "qsfera.cfg"
|
||||
CUMULATIVE_APP_LOG_FILE = "qsfera.log"
|
||||
CURRENT_APP_LOG_FILE = "app.log"
|
||||
|
||||
|
||||
def is_windows():
|
||||
return platform.system() == 'Windows'
|
||||
|
||||
|
||||
def is_linux():
|
||||
return platform.system() == 'Linux'
|
||||
|
||||
|
||||
def get_win_user_home():
|
||||
return os.environ.get('USERPROFILE', '')
|
||||
|
||||
|
||||
def get_client_root_path():
|
||||
if is_windows():
|
||||
return os.path.join(get_win_user_home(), 'qsferatest')
|
||||
return os.path.join(gettempdir(), 'qsferatest')
|
||||
|
||||
|
||||
def get_config_home_linux():
|
||||
return os.path.join(tempfile.gettempdir(), 'qsferatest', '.config')
|
||||
|
||||
|
||||
def get_config_home_win():
|
||||
return os.path.join(
|
||||
get_win_user_home(), 'AppData', 'Local', 'Temp', 'qsferatest', '.config'
|
||||
)
|
||||
|
||||
|
||||
def get_config_home():
|
||||
if is_windows():
|
||||
return get_config_home_win()
|
||||
return get_config_home_linux()
|
||||
|
||||
|
||||
def get_default_home_dir():
|
||||
if is_windows():
|
||||
return get_win_user_home()
|
||||
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',
|
||||
'localBackendUrl': 'BACKEND_HOST',
|
||||
'maxSyncTimeout': 'MAX_SYNC_TIMEOUT',
|
||||
'minSyncTimeout': 'MIN_SYNC_TIMEOUT',
|
||||
'lowestSyncTimeout': 'LOWEST_SYNC_TIMEOUT',
|
||||
'clientRootSyncPath': 'CLIENT_ROOT_SYNC_PATH',
|
||||
'tempFolderPath': 'TEMP_FOLDER_PATH',
|
||||
'guiTestReportDir': 'GUI_TEST_REPORT_DIR',
|
||||
'record_video_on_failure': 'RECORD_VIDEO_ON_FAILURE',
|
||||
}
|
||||
|
||||
DEFAULT_PATH_CONFIG = {
|
||||
'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
|
||||
CONFIG = {
|
||||
'localBackendUrl': 'https://localhost:9200/',
|
||||
'maxSyncTimeout': 60,
|
||||
'minSyncTimeout': 5,
|
||||
'lowestSyncTimeout': 1,
|
||||
'clientRootSyncPath': get_client_root_path(),
|
||||
'clientConfigFile': os.path.join(get_config_home(), "QSfera", APP_CONFIG_FILE),
|
||||
'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',
|
||||
}
|
||||
|
||||
# Permission roles mapping
|
||||
PERMISSION_ROLES = {
|
||||
'Viewer': 'b1e2218d-eef8-4d4c-b82d-0f1a1b48f3b5',
|
||||
'Editor': 'fb6c3e19-e378-47e5-b277-9732f9de6e21',
|
||||
}
|
||||
|
||||
CONFIG.update(DEFAULT_PATH_CONFIG)
|
||||
|
||||
READONLY_CONFIG = list(CONFIG_ENV_MAP.keys()) + list(DEFAULT_PATH_CONFIG.keys())
|
||||
|
||||
|
||||
def read_cfg_file(cfg_path):
|
||||
cfg = ConfigParser()
|
||||
if cfg.read(cfg_path):
|
||||
for key, _ in CONFIG.items():
|
||||
if key in CONFIG_ENV_MAP:
|
||||
if value := cfg.get('DEFAULT', CONFIG_ENV_MAP[key]):
|
||||
if key == 'record_video_on_failure':
|
||||
CONFIG[key] = value == 'true'
|
||||
else:
|
||||
CONFIG[key] = value
|
||||
|
||||
|
||||
def init_config():
|
||||
# try reading configs from config.ini
|
||||
try:
|
||||
cfg_path = os.path.abspath(os.path.join(CURRENT_DIR.parent, 'config.ini'))
|
||||
read_cfg_file(cfg_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# read and override configs from environment variables
|
||||
for key, value in CONFIG_ENV_MAP.items():
|
||||
if os.environ.get(value):
|
||||
if key == 'record_video_on_failure':
|
||||
CONFIG[key] = os.environ.get(value) == 'true'
|
||||
else:
|
||||
CONFIG[key] = os.environ.get(value)
|
||||
|
||||
# Set the default values if empty
|
||||
for key, value in CONFIG.items():
|
||||
if key in ('maxSyncTimeout', 'minSyncTimeout'):
|
||||
CONFIG[key] = builtins.int(value)
|
||||
elif key == 'localBackendUrl':
|
||||
# make sure there is always one trailing slash
|
||||
CONFIG[key] = value.rstrip('/') + '/'
|
||||
elif key in (
|
||||
'clientRootSyncPath',
|
||||
'tempFolderPath',
|
||||
'guiTestReportDir',
|
||||
):
|
||||
# make sure there is always one trailing slash
|
||||
if is_windows():
|
||||
value = value.replace('/', '\\')
|
||||
CONFIG[key] = value.rstrip('\\') + '\\'
|
||||
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')
|
||||
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):
|
||||
return CONFIG[key]
|
||||
|
||||
|
||||
def set_config(key, value):
|
||||
if key in READONLY_CONFIG:
|
||||
raise KeyError(f'Cannot set read-only config: {key}')
|
||||
CONFIG[key] = value
|
||||
@@ -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
|
||||
@@ -0,0 +1,188 @@
|
||||
import os
|
||||
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
|
||||
|
||||
|
||||
def build_conflicted_regex(filename):
|
||||
if "." in filename:
|
||||
# TODO: improve this for complex filenames
|
||||
namepart = filename.split(".")[0]
|
||||
extpart = filename.split(".")[1]
|
||||
# pylint: disable=anomalous-backslash-in-string
|
||||
return rf"{namepart} \(conflicted copy \d{{4}}-\d{{2}}-\d{{2}} \d{{6}}\)\.{extpart}"
|
||||
# pylint: disable=anomalous-backslash-in-string
|
||||
return rf"{filename} \(conflicted copy \d{{4}}-\d{{2}}-\d{{2}} \d{{6}}\)"
|
||||
|
||||
|
||||
def sanitize_path(path):
|
||||
return path.replace("//", "/")
|
||||
|
||||
|
||||
def prefix_path_namespace(path):
|
||||
if is_windows():
|
||||
# https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#win32-file-namespaces
|
||||
# disable string parsing
|
||||
# - long path
|
||||
# - trailing whitespaces
|
||||
return f"\\\\?\\{path}"
|
||||
return path
|
||||
|
||||
|
||||
def can_read(resource):
|
||||
read = False
|
||||
try:
|
||||
with open(resource, encoding="utf-8") as f:
|
||||
read = True
|
||||
except:
|
||||
pass
|
||||
return read and os.access(resource, os.R_OK)
|
||||
|
||||
|
||||
def can_write(resource):
|
||||
write = False
|
||||
try:
|
||||
with open(resource, "w", encoding="utf-8") as f:
|
||||
write = True
|
||||
except:
|
||||
pass
|
||||
return write and os.access(resource, os.W_OK)
|
||||
|
||||
|
||||
def read_file_content(file):
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return content
|
||||
|
||||
|
||||
def is_empty_sync_folder(folder):
|
||||
ignore_files = ["Desktop.ini"]
|
||||
for item in os.listdir(folder):
|
||||
# do not count the hidden files as they are ignored by the client
|
||||
if not item.startswith(".") and not item in ignore_files:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_size_in_bytes(size):
|
||||
match = re.match(r"(\d+)((?: )?[KkMmGgBb]{0,2})?", str(size))
|
||||
units = ["b", "kb", "mb", "gb"]
|
||||
multiplier = 1024
|
||||
if match:
|
||||
size_num = int(match.group(1))
|
||||
size_unit = match.group(2)
|
||||
|
||||
if not (size_unit := size_unit.lower()):
|
||||
return size_num
|
||||
if size_unit in units:
|
||||
if size_unit == "b":
|
||||
return size_num
|
||||
if size_unit == "kb":
|
||||
return size_num * multiplier
|
||||
if size_unit == "mb":
|
||||
return size_num * (multiplier**2)
|
||||
if size_unit == "gb":
|
||||
return size_num * (multiplier**3)
|
||||
|
||||
raise ValueError("Invalid size: " + size)
|
||||
|
||||
|
||||
def get_file_size(resource_path):
|
||||
return os.stat(resource_path).st_size
|
||||
|
||||
|
||||
# temp paths created outside the temporary directory during the test
|
||||
CREATED_PATHS = []
|
||||
|
||||
|
||||
def remember_path(path):
|
||||
CREATED_PATHS.append(path)
|
||||
|
||||
|
||||
def cleanup_created_paths():
|
||||
global CREATED_PATHS
|
||||
for path in CREATED_PATHS:
|
||||
if os.path.exists(path):
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(prefix_path_namespace(path))
|
||||
else:
|
||||
os.unlink(prefix_path_namespace(path))
|
||||
CREATED_PATHS = []
|
||||
|
||||
|
||||
def get_file_for_upload(file_name):
|
||||
base_upload_dir = get_config("files_for_upload")
|
||||
return os.path.join(base_upload_dir, file_name)
|
||||
|
||||
|
||||
def convert_path_separators_for_os(path):
|
||||
"""
|
||||
Convert path separators to match the current operating system.
|
||||
On Windows, converts forward slashes to backslashes.
|
||||
On other systems, returns the path unchanged.
|
||||
"""
|
||||
if is_windows():
|
||||
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
|
||||
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
import test
|
||||
|
||||
from helpers.ConfigHelper import get_config
|
||||
from helpers.FilesHelper import prefix_path_namespace
|
||||
|
||||
|
||||
def get_screenrecords_path():
|
||||
return os.path.join(get_config("guiTestReportDir"), "screenrecords")
|
||||
|
||||
|
||||
def get_screenshots_path():
|
||||
return os.path.join(get_config("guiTestReportDir"), "screenshots")
|
||||
|
||||
|
||||
def is_video_enabled():
|
||||
return get_config("record_video_on_failure") and not reached_video_limit()
|
||||
|
||||
|
||||
def reached_video_limit():
|
||||
video_report_dir = get_screenrecords_path()
|
||||
if not os.path.exists(video_report_dir):
|
||||
return False
|
||||
entries = [f for f in os.scandir(video_report_dir) if f.is_file()]
|
||||
return len(entries) >= get_config("video_record_limit")
|
||||
|
||||
|
||||
def save_video_recording(filename, test_failed):
|
||||
try:
|
||||
# do not throw if stopVideoCapture() fails
|
||||
test.stopVideoCapture()
|
||||
except:
|
||||
test.log("Failed to stop screen recording")
|
||||
|
||||
if not (video_dir := squishinfo.resultDir):
|
||||
video_dir = squishinfo.testCase
|
||||
else:
|
||||
test_case = "/".join(squishinfo.testCase.split("/")[-2:])
|
||||
video_dir = os.path.join(video_dir, test_case)
|
||||
video_dir = os.path.join(video_dir, "attachments")
|
||||
|
||||
# if the test failed
|
||||
# move videos to the screenrecords directory
|
||||
if test_failed:
|
||||
video_files = glob.glob(f"{video_dir}/**/*.mp4", recursive=True)
|
||||
screenrecords_dir = get_screenrecords_path()
|
||||
if not os.path.exists(screenrecords_dir):
|
||||
os.makedirs(screenrecords_dir)
|
||||
# reverse the list to get the latest video first
|
||||
video_files.reverse()
|
||||
for idx, video in enumerate(video_files):
|
||||
if idx:
|
||||
file_parts = filename.rsplit(".", 1)
|
||||
filename = f"{file_parts[0]}_{idx+1}.{file_parts[1]}"
|
||||
shutil.move(video, os.path.join(screenrecords_dir, filename))
|
||||
# remove the video directory
|
||||
shutil.rmtree(prefix_path_namespace(video_dir))
|
||||
|
||||
|
||||
def take_screenshot(filename):
|
||||
directory = get_screenshots_path()
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
try:
|
||||
squish.saveDesktopScreenshot(os.path.join(directory, filename))
|
||||
except:
|
||||
test.log("Failed to save screenshot")
|
||||
@@ -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
|
||||
@@ -0,0 +1,227 @@
|
||||
import uuid
|
||||
import os
|
||||
import subprocess
|
||||
import test
|
||||
from urllib.parse import urlparse
|
||||
from os import makedirs
|
||||
from os.path import exists, join
|
||||
from PySide6.QtCore import QSettings, QUuid, QUrl, QJsonValue
|
||||
|
||||
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
|
||||
from helpers.api import provisioning
|
||||
from helpers.AppHelper import create_app_session
|
||||
|
||||
|
||||
def substitute_inline_codes(value):
|
||||
value = value.replace('%local_server%', get_config('localBackendUrl'))
|
||||
value = value.replace('%client_root_sync_path%', get_config('clientRootSyncPath'))
|
||||
value = value.replace('%current_user_sync_path%', get_config('currentUserSyncPath'))
|
||||
value = value.replace(
|
||||
'%local_server_hostname%', urlparse(get_config('localBackendUrl')).netloc
|
||||
)
|
||||
value = value.replace('%home%', get_config('home_dir'))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def get_client_details(table):
|
||||
client_details = {
|
||||
'server': '',
|
||||
'user': '',
|
||||
'password': '',
|
||||
'sync_folder': '',
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
def create_user_sync_path(username):
|
||||
# '' at the end adds '/' to the path
|
||||
user_sync_path = join(get_config('clientRootSyncPath'), username, '')
|
||||
|
||||
if not exists(user_sync_path):
|
||||
makedirs(user_sync_path)
|
||||
|
||||
set_current_user_sync_path(user_sync_path)
|
||||
return user_sync_path
|
||||
|
||||
|
||||
def create_space_path(username, space='Personal'):
|
||||
user_sync_path = create_user_sync_path(username)
|
||||
space_path = join(user_sync_path, space, '')
|
||||
if not exists(space_path):
|
||||
makedirs(space_path)
|
||||
return space_path
|
||||
|
||||
|
||||
def set_current_user_sync_path(sync_path):
|
||||
set_config('currentUserSyncPath', sync_path)
|
||||
|
||||
|
||||
def get_resource_path(resource='', user='', space=''):
|
||||
sync_path = get_config('currentUserSyncPath')
|
||||
if user:
|
||||
sync_path = user
|
||||
space = space or get_config('syncConnectionName')
|
||||
sync_path = join(sync_path, space)
|
||||
sync_path = join(get_config('clientRootSyncPath'), sync_path)
|
||||
resource = resource.replace(sync_path, '').strip('/').strip('\\')
|
||||
return join(
|
||||
sync_path,
|
||||
resource,
|
||||
)
|
||||
|
||||
|
||||
def parse_username_from_sync_path(sync_path):
|
||||
return sync_path.split('/').pop()
|
||||
|
||||
|
||||
def get_temp_resource_path(resource_name):
|
||||
return join(get_config('tempFolderPath'), resource_name)
|
||||
|
||||
|
||||
def get_current_user_sync_path():
|
||||
return get_config('currentUserSyncPath')
|
||||
|
||||
|
||||
def start_client():
|
||||
create_app_session()
|
||||
|
||||
|
||||
def get_polling_interval():
|
||||
polling_interval = '''
|
||||
[QSfera]
|
||||
remotePollInterval={polling_interval}
|
||||
'''
|
||||
args = {'polling_interval': 5000}
|
||||
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)
|
||||
users_uuids = {}
|
||||
server_url = get_config('localBackendUrl')
|
||||
capabilities = provisioning.get_capabilities()
|
||||
capabilities_variant = QJsonValue(capabilities).toVariant()
|
||||
|
||||
for idx, username in enumerate(users):
|
||||
users_uuids[username] = QUuid.createUuid()
|
||||
settings.beginGroup("Accounts")
|
||||
settings.beginWriteArray(str(idx + 1), len(users))
|
||||
|
||||
settings.setValue("capabilities", capabilities_variant)
|
||||
settings.setValue("default_sync_root", create_user_sync_path(username))
|
||||
settings.setValue("uuid", users_uuids[username])
|
||||
settings.setValue("display-name", get_displayname_for_user(username))
|
||||
settings.setValue("url", server_url)
|
||||
settings.setValue("userExplicitlySignedOut", 'false')
|
||||
|
||||
settings.endArray()
|
||||
settings.setValue("size", len(users))
|
||||
settings.endGroup()
|
||||
|
||||
settings.beginGroup("Folders")
|
||||
for idx, username in enumerate(users):
|
||||
sync_path = create_space_path(username, space)
|
||||
settings.beginWriteArray(str(idx + 1), len(users))
|
||||
|
||||
if space == 'Personal':
|
||||
space_id = get_personal_space_id(username)
|
||||
else:
|
||||
space_id = get_space_id(space, username)
|
||||
dav_endpoint = QUrl(url_join(server_url, '/dav/spaces/', space_id))
|
||||
settings.setValue("spaceId", space_id)
|
||||
settings.setValue("accountUUID", users_uuids[username])
|
||||
settings.setValue("davUrl", dav_endpoint)
|
||||
settings.setValue("deployed", 'false')
|
||||
settings.setValue("displayString", get_config('syncConnectionName'))
|
||||
settings.setValue("ignoreHiddenFiles", 'true')
|
||||
settings.setValue("localPath", sync_path)
|
||||
settings.setValue("paused", 'false')
|
||||
settings.setValue("priority", '50')
|
||||
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))
|
||||
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)
|
||||
start_client()
|
||||
for _, sync_path in sync_paths.items():
|
||||
listen_sync_status_for_item(sync_path)
|
||||
|
||||
|
||||
def generate_uuidv4():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
# sometimes the keyring is locked during the test execution, and we need to unlock it
|
||||
def unlock_keyring():
|
||||
if is_windows():
|
||||
return
|
||||
|
||||
stdout, stderr, _ = run_sys_command(
|
||||
[
|
||||
'busctl',
|
||||
'--user',
|
||||
'get-property',
|
||||
'org.freedesktop.secrets',
|
||||
'/org/freedesktop/secrets/collection/login',
|
||||
'org.freedesktop.Secret.Collection',
|
||||
'Locked',
|
||||
]
|
||||
)
|
||||
output = ''
|
||||
if stdout:
|
||||
output = stdout.decode('utf-8')
|
||||
if stderr:
|
||||
output = stderr.decode('utf-8')
|
||||
|
||||
if not output.strip().endswith('false'):
|
||||
test.log('Unlocking keyring...')
|
||||
password = os.getenv('VNC_PW')
|
||||
command = f'echo -n "{password}" | gnome-keyring-daemon -r -d --unlock'
|
||||
stdout, stderr, returncode = run_sys_command(command, True)
|
||||
if stdout:
|
||||
output = stdout.decode('utf-8')
|
||||
if stderr:
|
||||
output = stderr.decode('utf-8')
|
||||
if returncode:
|
||||
test.log(f'Failed to unlock keyring:\n{output}')
|
||||
|
||||
|
||||
def run_sys_command(command=None, shell=False):
|
||||
cmd = subprocess.run(
|
||||
command,
|
||||
shell=shell,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
return cmd.stdout, cmd.stderr, cmd.returncode
|
||||
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
from urllib import parse
|
||||
|
||||
from helpers.ConfigHelper import get_config
|
||||
from helpers.api.utils import url_join
|
||||
import helpers.api.http_helper as request
|
||||
|
||||
created_spaces = {}
|
||||
user_spaces = {}
|
||||
space_role = ['manager', 'editor', 'viewer']
|
||||
|
||||
|
||||
def get_space_endpint():
|
||||
return url_join(get_config('localBackendUrl'), 'graph', 'v1.0', 'drives')
|
||||
|
||||
|
||||
def get_dav_endpint():
|
||||
return url_join(get_config('localBackendUrl'), 'dav', 'spaces')
|
||||
|
||||
|
||||
def get_share_endpint():
|
||||
return url_join(
|
||||
get_config('localBackendUrl'),
|
||||
'ocs/v2.php/apps',
|
||||
'files_sharing/api/v1/shares',
|
||||
)
|
||||
|
||||
|
||||
def create_space(space_name):
|
||||
body = json.dumps({'name': space_name})
|
||||
response = request.post(get_space_endpint(), body)
|
||||
request.assert_http_status(response, 201, f'Failed to create space {space_name}')
|
||||
# save created space
|
||||
resp_object = response.json()
|
||||
created_spaces[space_name] = resp_object['id']
|
||||
|
||||
|
||||
def fetch_spaces(user=None, query=''):
|
||||
if query:
|
||||
query = '?' + query
|
||||
url = get_space_endpint() + query
|
||||
response = request.get(url=url, user=user)
|
||||
request.assert_http_status(response, 200, 'Failed to get spaces')
|
||||
return response.json()['value']
|
||||
|
||||
|
||||
def get_project_spaces(user=None):
|
||||
search_query = '$filter=driveType eq \'project\''
|
||||
return fetch_spaces(query=search_query, user=user)
|
||||
|
||||
def get_personal_space_id(user):
|
||||
search_query = '$filter=driveType eq \'personal\''
|
||||
space = fetch_spaces(query=search_query, user=user)
|
||||
return space[0]['id']
|
||||
|
||||
def get_space_id(space_name, user=None):
|
||||
spaces = {**created_spaces, **user_spaces}
|
||||
if not space_name in spaces.keys():
|
||||
return fetch_space_id(space_name, user)
|
||||
return spaces.get(space_name)
|
||||
|
||||
|
||||
def fetch_space_id(space_name, user=None):
|
||||
spaces = fetch_spaces(user=user)
|
||||
space_id = None
|
||||
for space in spaces:
|
||||
if space['name'] == space_name:
|
||||
user_spaces[space_name] = space['id']
|
||||
space_id = space['id']
|
||||
break
|
||||
return space_id
|
||||
|
||||
|
||||
def delete_project_spaces():
|
||||
global created_spaces, user_spaces
|
||||
for _, space_id in created_spaces.items():
|
||||
disable_project_space(space_id)
|
||||
delete_project_space(space_id)
|
||||
created_spaces = {}
|
||||
user_spaces = {}
|
||||
|
||||
|
||||
def disable_project_space(space_id):
|
||||
url = url_join(get_space_endpint(), space_id)
|
||||
response = request.delete(url)
|
||||
request.assert_http_status(response, 204, f'Failed to disable space {space_id}')
|
||||
|
||||
|
||||
def delete_project_space(space_id):
|
||||
url = url_join(get_space_endpint(), space_id)
|
||||
response = request.delete(url, {'Purge': 'T'})
|
||||
request.assert_http_status(response, 204, f'Failed to delete space {space_id}')
|
||||
|
||||
|
||||
def create_space_folder(space_name, folder_name):
|
||||
space_id = get_space_id(space_name)
|
||||
url = url_join(get_dav_endpint(), space_id, folder_name)
|
||||
response = request.mkcol(url)
|
||||
request.assert_http_status(
|
||||
response,
|
||||
201,
|
||||
f'Failed to create folder "{folder_name}" in space "{space_name}"',
|
||||
)
|
||||
|
||||
|
||||
def create_space_file(space_name, file_name, content):
|
||||
space_id = get_space_id(space_name)
|
||||
url = url_join(get_dav_endpint(), space_id, file_name)
|
||||
response = request.put(url, content)
|
||||
if response.status_code not in (201, 204):
|
||||
raise AssertionError(
|
||||
f"Creating file '{file_name}' in space '{space_name}' failed with {response.status_code}\n"
|
||||
+ response.text
|
||||
)
|
||||
|
||||
|
||||
def add_user_to_space(user, space_name, role):
|
||||
role = role.lower()
|
||||
if not role in space_role:
|
||||
raise ValueError(f"Cannot set the role '{role}' to a space")
|
||||
|
||||
space_id = get_space_id(space_name)
|
||||
url = get_share_endpint()
|
||||
body = parse.urlencode(
|
||||
{
|
||||
'space_ref': space_id,
|
||||
'shareType': 7,
|
||||
'shareWith': user,
|
||||
'role': role,
|
||||
}
|
||||
)
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
response = request.post(url, body, headers)
|
||||
request.assert_http_status(
|
||||
response, 200, f'Failed to add user "{user}" to space "{space_name}"'
|
||||
)
|
||||
|
||||
|
||||
def get_file_content(space_name, file_name, user=None):
|
||||
space_id = get_space_id(space_name, user)
|
||||
url = url_join(get_dav_endpint(), space_id, file_name)
|
||||
response = request.get(url=url, user=user)
|
||||
request.assert_http_status(response, 200, f'Failed to get file "{file_name}"')
|
||||
return response.text
|
||||
|
||||
|
||||
def resource_exists(space_name, resource, user=None):
|
||||
space_id = get_space_id(space_name, user)
|
||||
url = url_join(get_dav_endpint(), space_id, resource)
|
||||
response = request.get(url=url, user=user)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import subprocess
|
||||
import glob
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from helpers.ConfigHelper import is_windows
|
||||
|
||||
|
||||
def get_core_dumps():
|
||||
# TODO: find a way to use coredump in windows
|
||||
if is_windows():
|
||||
return False
|
||||
# read coredump location
|
||||
with open('/proc/sys/kernel/core_pattern', 'r', encoding='utf-8') as f:
|
||||
coredump_path = f.read().strip('\n')
|
||||
|
||||
# yields something like: /tmp/core-*-*-*-*
|
||||
coredump_file_pattern = re.sub(r'%[a-zA-Z]{1}', '*', coredump_path)
|
||||
return glob.glob(coredump_file_pattern)
|
||||
|
||||
|
||||
def generate_stacktrace(scenario_title, coredumps):
|
||||
message = ['###########################################']
|
||||
message.append(f'Scenario: {scenario_title}')
|
||||
|
||||
for coredump_file in coredumps:
|
||||
message.append(parse_stacktrace(coredump_file))
|
||||
|
||||
message.append('###########################################')
|
||||
message.append('')
|
||||
stacktrace = '\n'.join(message)
|
||||
|
||||
stacktrace_file = os.environ.get('STACKTRACE_FILE', '../stacktrace.log')
|
||||
# save stacktrace to a file
|
||||
with open(stacktrace_file, 'a', encoding='utf-8') as f:
|
||||
f.write(stacktrace)
|
||||
|
||||
|
||||
def parse_stacktrace(coredump_file):
|
||||
message = []
|
||||
if coredump_file:
|
||||
coredump_filename = os.path.basename(coredump_file)
|
||||
# example coredump file: core-1648445754-1001-11-!drone!src!build-GUI-tests!bin!qsfera
|
||||
patterns = coredump_filename.split('-')
|
||||
app_binary = 'qsfera'
|
||||
if len(patterns) == 1:
|
||||
patterns.append('N/A')
|
||||
patterns.append('N/A')
|
||||
patterns.append('N/A')
|
||||
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(timestamp)}')
|
||||
message.append(f'Process ID: {patterns[2]}')
|
||||
message.append(f'Signal Number: {patterns[3]}')
|
||||
message.append('-------------------------------------------')
|
||||
message.append('<<<<< STACKTRACE START >>>>>')
|
||||
message.append(
|
||||
subprocess.run(
|
||||
[
|
||||
'gdb',
|
||||
app_binary,
|
||||
coredump_file,
|
||||
'-batch',
|
||||
'-ex',
|
||||
'bt full',
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
check=False,
|
||||
).stdout.decode('utf-8')
|
||||
)
|
||||
message.append('<<<<< STACKTRACE END >>>>>')
|
||||
|
||||
# remove coredump file
|
||||
os.unlink(coredump_file)
|
||||
|
||||
return '\n'.join(message)
|
||||
@@ -0,0 +1,422 @@
|
||||
import os
|
||||
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
|
||||
|
||||
if is_windows():
|
||||
from helpers.WinPipeHelper import WinPipeConnect as SocketConnect
|
||||
else:
|
||||
# NOTE: 'syncstate.py' was removed from client
|
||||
# and is now available at https://github.com/opencloud-eu/desktop-shell-integration-nautilus
|
||||
# check if 'syncstate.py' is available, if not, download it
|
||||
custom_lib = get_config('custom_lib')
|
||||
syncstate_lib_file = os.path.join(custom_lib, 'syncstate.py')
|
||||
os.makedirs(custom_lib, exist_ok=True)
|
||||
|
||||
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',
|
||||
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/
|
||||
# if the IDE fails to reference the script,
|
||||
# add the folder in Edit->Preferences->PyDev->Interpreters->Libraries
|
||||
from helpers.custom_lib.syncstate import SocketConnect
|
||||
|
||||
# socket messages
|
||||
socket_messages = []
|
||||
SOCKET_CONNECT = None
|
||||
# Whether wait has been made or not after account is set up
|
||||
# This is useful for waiting only for the first time
|
||||
WAITED_AFTER_SYNC = False
|
||||
|
||||
# File syncing in client has the following status
|
||||
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
|
||||
'REGISTER': 'REGISTER_PATH',
|
||||
'UNREGISTER': 'UNREGISTER_PATH',
|
||||
'UPDATE': 'UPDATE_VIEW',
|
||||
}
|
||||
|
||||
SYNC_PATTERNS = {
|
||||
# default sync patterns for the initial sync (after adding account)
|
||||
# the pattern can be of TWO types depending on the available resources (files/folders)
|
||||
'initial': [
|
||||
# when adding account via New Account wizard
|
||||
[
|
||||
SYNC_STATUS['NOP'],
|
||||
SYNC_STATUS['REGISTER'],
|
||||
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']],
|
||||
# initial root sync
|
||||
[
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['OK'],
|
||||
SYNC_STATUS['UPDATE'],
|
||||
],
|
||||
],
|
||||
'root_synced': [
|
||||
[
|
||||
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']],
|
||||
# file/folder deletion
|
||||
[SYNC_STATUS['SYNC'], SYNC_STATUS['NOP']],
|
||||
],
|
||||
'error': [SYNC_STATUS['ERROR']],
|
||||
}
|
||||
|
||||
|
||||
def get_socket_connection():
|
||||
global SOCKET_CONNECT
|
||||
if not SOCKET_CONNECT or not SOCKET_CONNECT.connected:
|
||||
SOCKET_CONNECT = SocketConnect()
|
||||
return SOCKET_CONNECT
|
||||
|
||||
|
||||
def read_socket_messages():
|
||||
messages = []
|
||||
socket_connect = get_socket_connection()
|
||||
socket_connect.read_socket_data_with_timeout(0.1)
|
||||
for line in socket_connect.get_available_responses():
|
||||
messages.append(line)
|
||||
return messages
|
||||
|
||||
|
||||
def read_and_update_socket_messages():
|
||||
messages = read_socket_messages()
|
||||
return update_socket_messages(messages)
|
||||
|
||||
|
||||
def update_socket_messages(messages):
|
||||
socket_messages.extend(filter_sync_messages(messages))
|
||||
return socket_messages
|
||||
|
||||
|
||||
def clear_socket_messages(resource=''):
|
||||
global socket_messages
|
||||
if resource:
|
||||
resource_messages = set(filter_messages_for_item(socket_messages, resource))
|
||||
socket_messages = [
|
||||
msg for msg in socket_messages if msg not in resource_messages
|
||||
]
|
||||
else:
|
||||
socket_messages.clear()
|
||||
|
||||
|
||||
def close_socket_connection():
|
||||
socket_messages.clear()
|
||||
if SOCKET_CONNECT:
|
||||
SOCKET_CONNECT.connected = False
|
||||
if is_windows():
|
||||
SOCKET_CONNECT.close_conn()
|
||||
elif is_linux():
|
||||
SOCKET_CONNECT._sock.close() # pylint: disable=protected-access
|
||||
|
||||
|
||||
def get_initial_sync_patterns():
|
||||
return SYNC_PATTERNS['initial']
|
||||
|
||||
|
||||
def get_synced_pattern(resource=''):
|
||||
# get only the resource path
|
||||
sync_path = get_config('currentUserSyncPath')
|
||||
sync_path = os.path.join(sync_path, get_config('syncConnectionName'))
|
||||
|
||||
if resource := resource.replace(sync_path, '').strip('\\').strip('/'):
|
||||
return SYNC_PATTERNS['single_synced']
|
||||
return SYNC_PATTERNS['root_synced']
|
||||
|
||||
|
||||
# generate sync pattern from the socket messages
|
||||
#
|
||||
# returns List
|
||||
# e.g: ['UPDATE_VIEW', 'STATUS:OK']
|
||||
def generate_sync_pattern_from_messages(messages):
|
||||
pattern = []
|
||||
if not messages:
|
||||
return pattern
|
||||
|
||||
sync_messages = filter_sync_messages(messages)
|
||||
for message in sync_messages:
|
||||
# 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(r':(/|[A-Za-z]:[\\/]).*', message):
|
||||
end, _ = match.span()
|
||||
# shared resources will have status like "STATUS:OK+SWM"
|
||||
status = message[:end].replace('+SWM', '')
|
||||
pattern.append(status)
|
||||
return pattern
|
||||
|
||||
|
||||
# strip out the messages that are not related to sync
|
||||
def filter_sync_messages(messages):
|
||||
start_idx = 0
|
||||
if 'GET_STRINGS:END' in messages:
|
||||
start_idx = messages.index('GET_STRINGS:END') + 1
|
||||
return messages[start_idx:]
|
||||
|
||||
|
||||
def filter_messages_for_item(messages, item):
|
||||
filtered_messages = []
|
||||
for msg in messages:
|
||||
msg = msg.rstrip('/').rstrip('\\')
|
||||
item = item.rstrip('/').rstrip('\\')
|
||||
if msg.endswith(item):
|
||||
filtered_messages.append(msg)
|
||||
return filtered_messages
|
||||
|
||||
|
||||
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('\\').rstrip('/')
|
||||
socket_connect.sendCommand(f'RETRIEVE_{resource_type}_STATUS:{item}\n')
|
||||
|
||||
|
||||
def get_current_sync_status(resource, resource_type):
|
||||
listen_sync_status_for_item(resource, resource_type)
|
||||
messages = filter_messages_for_item(read_socket_messages(), resource)
|
||||
# return the last message from the list
|
||||
return messages[-1]
|
||||
|
||||
|
||||
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 - initial_timeout,
|
||||
)
|
||||
|
||||
messages = read_and_update_socket_messages()
|
||||
messages = filter_messages_for_item(messages, resource)
|
||||
clear_socket_messages(resource)
|
||||
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)
|
||||
if status.startswith(SYNC_STATUS['OK']):
|
||||
print(
|
||||
'[WARN] Failed to match sync pattern for resource: '
|
||||
+ resource
|
||||
+ f'\nBut its last status is "{SYNC_STATUS["OK"]}"'
|
||||
+ '. So passing the step.'
|
||||
)
|
||||
return
|
||||
raise TimeoutError(
|
||||
'Timeout while waiting for sync to complete for '
|
||||
+ str(timeout)
|
||||
+ ' milliseconds'
|
||||
)
|
||||
|
||||
|
||||
def wait_for_initial_sync_to_complete(path):
|
||||
wait_for_resource_to_sync(
|
||||
path,
|
||||
'FOLDER',
|
||||
get_initial_sync_patterns(),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def has_sync_pattern(patterns, resource=None):
|
||||
if isinstance(patterns[0], str):
|
||||
patterns = [patterns]
|
||||
messages = read_and_update_socket_messages()
|
||||
if resource:
|
||||
messages = filter_messages_for_item(messages, resource)
|
||||
for pattern in patterns:
|
||||
pattern_len = len(pattern)
|
||||
for idx, _ in enumerate(messages):
|
||||
actual_pattern = generate_sync_pattern_from_messages(
|
||||
messages[idx : idx + pattern_len]
|
||||
)
|
||||
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)
|
||||
return False
|
||||
|
||||
|
||||
# Using socket API to check file sync status
|
||||
def has_sync_status(item_name, status):
|
||||
sync_messages = read_and_update_socket_messages()
|
||||
sync_messages = filter_messages_for_item(sync_messages, item_name)
|
||||
for line in sync_messages:
|
||||
line = line.rstrip('/').rstrip('\\')
|
||||
item_name = item_name.rstrip('/').rstrip('\\')
|
||||
if line.startswith(status) and line.endswith(item_name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# useful for checking sync status such as 'error', 'ignore'
|
||||
# but not quite so reliable for checking 'ok' sync status
|
||||
def wait_for_resource_to_have_sync_status(
|
||||
resource, resource_type, status=SYNC_STATUS['OK'], timeout=None
|
||||
):
|
||||
resource = sanitize_path(resource)
|
||||
|
||||
listen_sync_status_for_item(resource, resource_type)
|
||||
|
||||
if not timeout:
|
||||
timeout = get_config('maxSyncTimeout') * 1000
|
||||
|
||||
result = wait_for(
|
||||
lambda: has_sync_status(resource, status),
|
||||
timeout,
|
||||
)
|
||||
|
||||
if not result:
|
||||
if status == SYNC_STATUS['ERROR']:
|
||||
expected = 'have sync error'
|
||||
elif status == SYNC_STATUS['IGNORE']:
|
||||
expected = 'be sync ignored'
|
||||
else:
|
||||
expected = 'be synced'
|
||||
raise ValueError(
|
||||
f'Expected {resource_type} "{resource}" to {expected}, but not.'
|
||||
)
|
||||
|
||||
|
||||
def wait_for_resource_to_have_sync_error(resource, resource_type):
|
||||
wait_for_resource_to_have_sync_status(resource, resource_type, SYNC_STATUS['ERROR'])
|
||||
|
||||
|
||||
# performing actions immediately after completing the sync from the server does not work
|
||||
# The test should wait for a while before performing the action
|
||||
# issue: https://github.com/owncloud/client/issues/8832
|
||||
def wait_for_client_to_be_ready():
|
||||
global WAITED_AFTER_SYNC
|
||||
if not WAITED_AFTER_SYNC:
|
||||
time.sleep(get_config('minSyncTimeout'))
|
||||
WAITED_AFTER_SYNC = True
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,74 @@
|
||||
from base64 import b64encode
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class User(NamedTuple):
|
||||
username: str
|
||||
password: str
|
||||
displayname: str
|
||||
email: str
|
||||
|
||||
|
||||
test_users = {
|
||||
"admin": User(
|
||||
username="admin",
|
||||
password="admin",
|
||||
displayname="adminUsername",
|
||||
email="admin@example.org",
|
||||
),
|
||||
"Alice": User(
|
||||
username="Alice",
|
||||
password="1234",
|
||||
displayname="Alice Hansen",
|
||||
email="alice@example.org",
|
||||
),
|
||||
"Brian": User(
|
||||
username="Brian",
|
||||
password="AaBb2Cc3Dd4",
|
||||
displayname="Brian Murphy",
|
||||
email="brian@example.org",
|
||||
),
|
||||
"Carol": User(
|
||||
username="Carol",
|
||||
password="1234",
|
||||
displayname="Carol King",
|
||||
email="carol@example.org",
|
||||
),
|
||||
"David": User(
|
||||
username="David",
|
||||
password="1234",
|
||||
displayname="David Lopez",
|
||||
email="david@example.org",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_default_password():
|
||||
return "1234"
|
||||
|
||||
|
||||
def basic_auth_header(user=None, password=None):
|
||||
if not user and not password:
|
||||
user = "admin"
|
||||
password = "admin"
|
||||
elif not user == "public" and not password:
|
||||
password = get_password_for_user(user)
|
||||
|
||||
token = b64encode((f"{user}:{password}").encode()).decode()
|
||||
return {"Authorization": "Basic " + token}
|
||||
|
||||
|
||||
def get_user_info(username, attribute):
|
||||
if username in test_users:
|
||||
return getattr(test_users[username], attribute)
|
||||
if attribute == "password":
|
||||
return get_default_password()
|
||||
raise ValueError(f"Invalid user attribute: {attribute}")
|
||||
|
||||
|
||||
def get_displayname_for_user(username):
|
||||
return get_user_info(username, "displayname")
|
||||
|
||||
|
||||
def get_password_for_user(username):
|
||||
return get_user_info(username, "password")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
# pylint: disable=import-error
|
||||
import win32pipe
|
||||
import win32file
|
||||
import winerror
|
||||
import pywintypes
|
||||
import win32event
|
||||
|
||||
TIMEOUT = 100
|
||||
DEFAULT_BUFLEN = 4096
|
||||
CLIENT_MESSAGES = [
|
||||
'STATUS',
|
||||
'REGISTER_PATH',
|
||||
'UNREGISTER_PATH',
|
||||
'UPDATE_VIEW',
|
||||
'GET_STRINGS',
|
||||
'STRING',
|
||||
'VERSION',
|
||||
'SHARE',
|
||||
'GET_MENU_ITEMS',
|
||||
'MENU_ITEM',
|
||||
]
|
||||
|
||||
|
||||
def get_pipe_path():
|
||||
pipename = r'\\.\\pipe\\'
|
||||
pipename = os.path.join(pipename, 'openCloud-' + os.getenv('USERNAME'))
|
||||
return pipename
|
||||
|
||||
|
||||
class WinPipeConnect:
|
||||
def __init__(self):
|
||||
self.connected = False
|
||||
self._pipe = None
|
||||
self._remainder = ''.encode('utf-8')
|
||||
self._overlapped = pywintypes.OVERLAPPED()
|
||||
self._overlapped.hEvent = win32event.CreateEvent(None, 1, 0, None)
|
||||
self.connect_to_pipe_server()
|
||||
|
||||
def connect_to_pipe_server(self):
|
||||
try:
|
||||
pipename = get_pipe_path()
|
||||
|
||||
self._pipe = win32file.CreateFile(
|
||||
pipename,
|
||||
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
|
||||
0,
|
||||
None,
|
||||
win32file.OPEN_EXISTING,
|
||||
win32file.FILE_FLAG_OVERLAPPED,
|
||||
None,
|
||||
)
|
||||
|
||||
if self._pipe == win32file.INVALID_HANDLE_VALUE:
|
||||
win32pipe.CloseHandle(self._pipe)
|
||||
raise OSError('Invalid _pipe value')
|
||||
|
||||
self.connected = True
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f'Could not connect to named pipe {pipename}\n' + str(e))
|
||||
win32file.CloseHandle(self._pipe)
|
||||
|
||||
def sendCommand(self, cmd): # pylint: disable=invalid-name
|
||||
if self.connected:
|
||||
w_res, _ = win32file.WriteFile(
|
||||
self._pipe, cmd.encode('utf-8'), self._overlapped
|
||||
)
|
||||
if w_res == winerror.ERROR_IO_PENDING:
|
||||
res = win32event.WaitForSingleObject(self._overlapped.hEvent, TIMEOUT)
|
||||
if res != win32event.WAIT_OBJECT_0:
|
||||
print('Sending timed out!')
|
||||
return False
|
||||
if not win32file.GetOverlappedResult(
|
||||
self._pipe, self._overlapped, False
|
||||
):
|
||||
print('GetOverlappedResult failed')
|
||||
return False
|
||||
else:
|
||||
print('Cannot send, not connected!')
|
||||
return False
|
||||
return True
|
||||
|
||||
# Reads data that becomes available.
|
||||
# New responses can be accessed with get_available_responses().
|
||||
def read_socket_data_with_timeout(self, timeout):
|
||||
messages = b''
|
||||
start_time = time.time()
|
||||
while True: # pylint: disable=too-many-nested-blocks
|
||||
if (time.time() - start_time) >= timeout:
|
||||
self._remainder += messages
|
||||
break
|
||||
|
||||
peek_bytes = win32pipe.PeekNamedPipe(self._pipe, DEFAULT_BUFLEN)[1]
|
||||
if isinstance(peek_bytes, int) and peek_bytes > 0:
|
||||
_, message_mem = win32file.ReadFile(
|
||||
self._pipe, DEFAULT_BUFLEN, self._overlapped
|
||||
)
|
||||
if message_mem:
|
||||
m_bytes = bytes(message_mem)
|
||||
if b'\n' in m_bytes:
|
||||
for m in m_bytes.split(b'\n'):
|
||||
try:
|
||||
# append decodable and client specific bytes
|
||||
m.decode('utf-8')
|
||||
start = m.split(b':', 1)[0]
|
||||
if start.decode('utf-8') in CLIENT_MESSAGES:
|
||||
messages += m + b'\n'
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
res = win32event.WaitForSingleObject(
|
||||
self._overlapped.hEvent, int(timeout * 1000)
|
||||
)
|
||||
if res != win32event.WAIT_OBJECT_0:
|
||||
print('Reading timed out!')
|
||||
return False
|
||||
if not win32file.GetOverlappedResult(
|
||||
self._pipe, self._overlapped, False
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Parses response lines out of collected data, returns list of strings
|
||||
def get_available_responses(self):
|
||||
if (end := self._remainder.rfind(b'\n')) == -1:
|
||||
return []
|
||||
data = self._remainder[:end]
|
||||
self._remainder = self._remainder[end + 1 :]
|
||||
return data.decode('utf-8').split('\n')
|
||||
|
||||
def close_conn(self):
|
||||
win32file.CloseHandle(self._pipe)
|
||||
@@ -0,0 +1,60 @@
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from helpers.UserHelper import basic_auth_header
|
||||
from helpers.ConfigHelper import get_config
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
def send_request(url, method, body=None, headers=None, user=None, password=None):
|
||||
auth_header = basic_auth_header(user, password)
|
||||
if not headers:
|
||||
headers = {}
|
||||
headers.update(auth_header)
|
||||
return requests.request(
|
||||
method,
|
||||
url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
# in seconds
|
||||
# e.g.: 60
|
||||
timeout=get_config("maxSyncTimeout"),
|
||||
)
|
||||
|
||||
|
||||
def get(url, headers=None, user=None, password=None):
|
||||
return send_request(
|
||||
url=url, method="GET", headers=headers, user=user, password=password
|
||||
)
|
||||
|
||||
|
||||
def post(url, body=None, headers=None, user=None):
|
||||
return send_request(url, "POST", body, headers, user)
|
||||
|
||||
|
||||
def put(url, body=None, headers=None, user=None):
|
||||
return send_request(url, "PUT", body, headers, user)
|
||||
|
||||
|
||||
def delete(url, headers=None, user=None):
|
||||
return send_request(url=url, method="DELETE", headers=headers, user=user)
|
||||
|
||||
|
||||
def mkcol(url, headers=None, user=None):
|
||||
return send_request(url=url, method="MKCOL", headers=headers, user=user)
|
||||
|
||||
|
||||
def propfind(url, body=None, headers=None, user=None, password=None):
|
||||
return send_request(url, "PROPFIND", body, headers, user, password)
|
||||
|
||||
|
||||
def assert_http_status(response, expected_code, message=""):
|
||||
response_body = ""
|
||||
if response.text:
|
||||
response_body = response.text
|
||||
|
||||
assert (
|
||||
response.status_code == expected_code
|
||||
), f"{message}\nRequest failed with status code '{response.status_code}'\n{response_body}"
|
||||
@@ -0,0 +1,70 @@
|
||||
import json
|
||||
from PySide6.QtCore import QJsonDocument
|
||||
|
||||
import helpers.api.http_helper as request
|
||||
from helpers.ConfigHelper import get_config
|
||||
from helpers import UserHelper
|
||||
from helpers.api.utils import url_join
|
||||
|
||||
|
||||
created_groups = {}
|
||||
created_users = {}
|
||||
|
||||
|
||||
def get_graph_url():
|
||||
return url_join(get_config("localBackendUrl"), "graph", "v1.0")
|
||||
|
||||
|
||||
def create_user(username):
|
||||
if username in UserHelper.test_users:
|
||||
user = UserHelper.test_users[username]
|
||||
else:
|
||||
user = UserHelper.User(
|
||||
username=username,
|
||||
displayname=username,
|
||||
email=f'{username}@mail.com',
|
||||
password=UserHelper.get_default_password(),
|
||||
)
|
||||
|
||||
url = url_join(get_graph_url(), "users")
|
||||
body = json.dumps(
|
||||
{
|
||||
"onPremisesSamAccountName": user.username,
|
||||
"passwordProfile": {"password": user.password},
|
||||
"displayName": user.displayname,
|
||||
"mail": user.email,
|
||||
}
|
||||
)
|
||||
response = request.post(url, body)
|
||||
request.assert_http_status(response, 201, f"Failed to create user '{username}'")
|
||||
resp_object = response.json()
|
||||
|
||||
# Check if the user already exists
|
||||
user_info = {
|
||||
"id": resp_object["id"],
|
||||
"username": user.username,
|
||||
"password": user.password,
|
||||
"displayname": resp_object["displayName"],
|
||||
"email": resp_object["mail"],
|
||||
}
|
||||
created_users[username] = user_info
|
||||
|
||||
|
||||
def delete_created_users():
|
||||
for username, user_info in list(created_users.items()):
|
||||
user_id = user_info['id']
|
||||
url = url_join(get_graph_url(), "users", user_id)
|
||||
response = request.delete(url)
|
||||
request.assert_http_status(response, 204, "Failed to delete user")
|
||||
del created_users[username]
|
||||
|
||||
|
||||
def get_capabilities():
|
||||
server_url = get_config('localBackendUrl')
|
||||
url = url_join(server_url, '/ocs/v1.php/cloud/capabilities?format=json')
|
||||
response = request.get(url)
|
||||
response_str = response.text
|
||||
response_doc = QJsonDocument.fromJson(response_str.encode("utf-8"))
|
||||
response_obj = response_doc.object()
|
||||
capabilities = response_obj.get('ocs').get('data').get('capabilities')
|
||||
return capabilities
|
||||
@@ -0,0 +1,8 @@
|
||||
from posixpath import join
|
||||
|
||||
|
||||
def url_join(*args):
|
||||
paths = []
|
||||
for path in list(args):
|
||||
paths.append(path.strip("/"))
|
||||
return join("", *paths)
|
||||
@@ -0,0 +1,147 @@
|
||||
from urllib.parse import quote
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
|
||||
import helpers.api.http_helper as request
|
||||
from helpers.api.utils import url_join
|
||||
from helpers.ConfigHelper import get_config, PERMISSION_ROLES
|
||||
from helpers.FilesHelper import get_file_for_upload
|
||||
from helpers.SpaceHelper import get_personal_space_id
|
||||
from helpers.api import provisioning
|
||||
|
||||
|
||||
def get_webdav_url():
|
||||
return url_join(get_config('localBackendUrl'), 'remote.php/dav/files')
|
||||
|
||||
|
||||
def get_beta_graph_url():
|
||||
return url_join(get_config("localBackendUrl"), "graph", "v1beta1")
|
||||
|
||||
|
||||
def get_resource_path(user, resource):
|
||||
resource = resource.strip('/').replace('\\', '/')
|
||||
encoded_resource_path = [quote(path, safe='') for path in resource.split('/')]
|
||||
encoded_resource_path = '/'.join(encoded_resource_path)
|
||||
url = url_join(get_webdav_url(), user, encoded_resource_path)
|
||||
return url
|
||||
|
||||
|
||||
def resource_exists(user, resource):
|
||||
response = request.propfind(get_resource_path(user, resource), user=user)
|
||||
if response.status_code == 207:
|
||||
return True
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
raise AssertionError(f'Server returned status code: {response.status_code}')
|
||||
|
||||
|
||||
def get_file_content(user, resource):
|
||||
response = request.get(get_resource_path(user, resource), user=user)
|
||||
if resource.lower().endswith('.txt'):
|
||||
return response.text
|
||||
return response.content
|
||||
|
||||
|
||||
def get_folder_items(user, resource):
|
||||
"""Get the root XML element from a PROPFIND request"""
|
||||
path = get_resource_path(user, resource)
|
||||
xml_response = request.propfind(path, user=user)
|
||||
|
||||
if xml_response.status_code != 207:
|
||||
raise AssertionError(f'Failed to get resource properties: {xml_response.status_code}')
|
||||
|
||||
return ET.fromstring(xml_response.content)
|
||||
|
||||
|
||||
def get_folder_items_count(user, folder_name):
|
||||
folder_name = folder_name.strip('/')
|
||||
root_element = get_folder_items(user, folder_name)
|
||||
total_items = 0
|
||||
for response_element in root_element:
|
||||
for href_element in response_element:
|
||||
# The first item is folder itself so excluding it
|
||||
if href_element.tag == '{DAV:}href' and not href_element.text.endswith(
|
||||
f'{user}/{folder_name}/'
|
||||
):
|
||||
total_items += 1
|
||||
return str(total_items)
|
||||
|
||||
|
||||
def create_folder(user, folder_name):
|
||||
url = get_resource_path(user, folder_name)
|
||||
response = request.mkcol(url, user=user)
|
||||
assert (
|
||||
response.status_code == 201
|
||||
), f'Could not create the folder: {folder_name} for user {user}'
|
||||
|
||||
|
||||
def create_file(user, file_name, contents):
|
||||
url = get_resource_path(user, file_name)
|
||||
response = request.put(url, body=contents, user=user)
|
||||
assert response.status_code in [
|
||||
201,
|
||||
204,
|
||||
], f"Could not create file '{file_name}' for user {user}"
|
||||
|
||||
|
||||
def upload_file(user, file_name, destination):
|
||||
file_path = get_file_for_upload(file_name)
|
||||
with open(file_path, 'rb') as file:
|
||||
contents = file.read()
|
||||
create_file(user, destination, contents)
|
||||
|
||||
|
||||
def delete_resource(user, resource):
|
||||
url = get_resource_path(user, resource)
|
||||
response = request.delete(url, user=user)
|
||||
assert response.status_code == 204, f"Could not delete folder '{resource}'"
|
||||
|
||||
|
||||
def get_resource_id(user, resource):
|
||||
root_element = get_folder_items(user, resource)
|
||||
|
||||
# Finding the fileid elements using XPath with namespace notation
|
||||
fileid_elements = root_element.findall(".//{http://owncloud.org/ns}fileid")
|
||||
|
||||
# The First element is the desired resource's file id
|
||||
if fileid_elements:
|
||||
return fileid_elements[0].text
|
||||
|
||||
raise AssertionError(f'Could not find resource ID for {resource}')
|
||||
|
||||
|
||||
def get_permission_role_id(role_name):
|
||||
"""Get the permission role ID for a given role name"""
|
||||
if role_name not in PERMISSION_ROLES:
|
||||
raise ValueError(f"Unknown permission role: {role_name}")
|
||||
return PERMISSION_ROLES[role_name]
|
||||
|
||||
|
||||
def get_user_id(username):
|
||||
"""Get the user ID for a given username from created users"""
|
||||
if username in provisioning.created_users:
|
||||
return provisioning.created_users[username]['id']
|
||||
|
||||
raise AssertionError(f'User {username} not found in created users. Make sure the user is created first.')
|
||||
|
||||
|
||||
def send_resource_share_invitation(user, resource, sharee, permission_role):
|
||||
space_id = get_personal_space_id(user)
|
||||
resource_id = get_resource_id(user, resource)
|
||||
recipient_user_id = get_user_id(sharee)
|
||||
role_id = get_permission_role_id(permission_role)
|
||||
|
||||
url = url_join(get_beta_graph_url(), "drives", space_id, "items", resource_id, "invite")
|
||||
|
||||
body = {
|
||||
"roles": [role_id],
|
||||
"recipients": [
|
||||
{
|
||||
"objectId": recipient_user_id,
|
||||
"@libre.graph.recipient.type": "user"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = request.post(url, body=json.dumps(body), user=user)
|
||||
assert response.status_code in [200, 201], f"Failed to send share invitation: {response.status_code}"
|
||||
@@ -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
|
||||
@@ -0,0 +1,236 @@
|
||||
import os
|
||||
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
|
||||
from helpers.SetupClientHelper import (
|
||||
create_user_sync_path,
|
||||
get_temp_resource_path,
|
||||
set_current_user_sync_path,
|
||||
)
|
||||
from helpers.SyncHelper import listen_sync_status_for_item
|
||||
from helpers.AppHelper import app
|
||||
|
||||
|
||||
class AccountConnectionWizard:
|
||||
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=By.NAME, selector="Configure synchronization manually"
|
||||
)
|
||||
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):
|
||||
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():
|
||||
buttons = app().find_elements(
|
||||
AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.by,
|
||||
AccountConnectionWizard.ACCEPT_CERTIFICATE_YES.selector,
|
||||
)
|
||||
# click the last button
|
||||
last_button = buttons.pop()
|
||||
last_button.click()
|
||||
|
||||
@staticmethod
|
||||
def add_user_credentials(username, password):
|
||||
AccountConnectionWizard.oidc_login(username, password)
|
||||
|
||||
@staticmethod
|
||||
def oidc_login(username, password):
|
||||
AccountConnectionWizard.browser_login(username, password)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
def next_step():
|
||||
app().find_element(
|
||||
AccountConnectionWizard.NEXT_BUTTON.by,
|
||||
AccountConnectionWizard.NEXT_BUTTON.selector,
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def select_sync_folder(user):
|
||||
# create sync folder for user
|
||||
sync_path = create_user_sync_path(user)
|
||||
|
||||
AccountConnectionWizard.select_advanced_config()
|
||||
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,
|
||||
)
|
||||
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
|
||||
def set_temp_folder_as_sync_folder(folder_name):
|
||||
sync_path = get_temp_resource_path(folder_name)
|
||||
|
||||
# clear the current path
|
||||
squish.mouseClick(
|
||||
squish.waitForObject(AccountConnectionWizard.SELECT_LOCAL_FOLDER)
|
||||
)
|
||||
|
||||
squish.waitForObject(AccountConnectionWizard.SELECT_LOCAL_FOLDER).setText("")
|
||||
|
||||
squish.type(
|
||||
squish.waitForObject(AccountConnectionWizard.SELECT_LOCAL_FOLDER),
|
||||
sync_path,
|
||||
)
|
||||
set_current_user_sync_path(sync_path)
|
||||
return sync_path
|
||||
|
||||
@staticmethod
|
||||
def add_account(account_details):
|
||||
AccountConnectionWizard.add_account_information(account_details)
|
||||
AccountConnectionWizard.next_step()
|
||||
|
||||
@staticmethod
|
||||
def add_account_information(account_details):
|
||||
if account_details["server"]:
|
||||
AccountConnectionWizard.add_server(account_details["server"])
|
||||
AccountConnectionWizard.accept_certificate()
|
||||
if account_details["user"]:
|
||||
AccountConnectionWizard.add_user_credentials(
|
||||
account_details["user"],
|
||||
account_details["password"],
|
||||
)
|
||||
sync_path = ""
|
||||
if account_details["sync_folder"]:
|
||||
AccountConnectionWizard.select_advanced_config()
|
||||
sync_path = AccountConnectionWizard.set_temp_folder_as_sync_folder(
|
||||
account_details["sync_folder"]
|
||||
)
|
||||
elif account_details["user"]:
|
||||
sync_path = AccountConnectionWizard.select_sync_folder(
|
||||
account_details["user"]
|
||||
)
|
||||
if sync_path:
|
||||
# listen for sync status
|
||||
listen_sync_status_for_item(sync_path)
|
||||
|
||||
@staticmethod
|
||||
def select_manual_sync_folder_option():
|
||||
app().find_element(
|
||||
AccountConnectionWizard.CONF_SYNC_MANUALLY_RADIO_BUTTON.by,
|
||||
AccountConnectionWizard.CONF_SYNC_MANUALLY_RADIO_BUTTON.selector,
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def select_download_everything_option():
|
||||
squish.clickButton(
|
||||
squish.waitForObject(AccountConnectionWizard.SYNC_EVERYTHING_RADIO_BUTTON)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_new_connection_window_visible():
|
||||
visible = False
|
||||
try:
|
||||
squish.waitForObject(AccountConnectionWizard.SERVER_ADDRESS_BOX)
|
||||
visible = True
|
||||
except:
|
||||
pass
|
||||
return visible
|
||||
|
||||
@staticmethod
|
||||
def is_credential_window_visible():
|
||||
visible = False
|
||||
try:
|
||||
squish.waitForObject(AccountConnectionWizard.OAUTH_CREDENTIAL_PAGE)
|
||||
visible = True
|
||||
except:
|
||||
pass
|
||||
return visible
|
||||
|
||||
@staticmethod
|
||||
def select_advanced_config():
|
||||
app().find_element(
|
||||
AccountConnectionWizard.ADVANCED_CONFIGURATION_CHECKBOX.by,
|
||||
AccountConnectionWizard.ADVANCED_CONFIGURATION_CHECKBOX.selector,
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def can_change_local_sync_dir():
|
||||
can_change = False
|
||||
try:
|
||||
squish.waitForObjectExists(AccountConnectionWizard.SELECT_LOCAL_FOLDER)
|
||||
squish.clickButton(
|
||||
squish.waitForObject(AccountConnectionWizard.DIRECTORY_NAME_BOX)
|
||||
)
|
||||
squish.waitForObjectExists(AccountConnectionWizard.CHOOSE_FOLDER_BUTTON)
|
||||
can_change = True
|
||||
except:
|
||||
pass
|
||||
return can_change
|
||||
|
||||
@staticmethod
|
||||
def is_sync_everything_option_checked():
|
||||
return squish.waitForObjectExists(
|
||||
AccountConnectionWizard.SYNC_EVERYTHING_RADIO_BUTTON
|
||||
).checked
|
||||
|
||||
@staticmethod
|
||||
def get_local_sync_path():
|
||||
return str(
|
||||
squish.waitForObjectExists(
|
||||
AccountConnectionWizard.SELECT_LOCAL_FOLDER
|
||||
).displayText
|
||||
)
|
||||
@@ -0,0 +1,172 @@
|
||||
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 substitute_inline_codes
|
||||
from helpers.UserHelper import get_displayname_for_user
|
||||
from helpers.AppHelper import app
|
||||
from helpers.SyncHelper import wait_for
|
||||
|
||||
|
||||
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(
|
||||
by=By.NAME, selector="Remove connection"
|
||||
)
|
||||
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)
|
||||
CONFIRMATION_YES_BUTTON = SimpleNamespace(by=None, selector=None)
|
||||
|
||||
@staticmethod
|
||||
def account_action(action):
|
||||
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),
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def remove_account_connection():
|
||||
AccountSetting.account_action("Remove")
|
||||
app().find_element(
|
||||
AccountSetting.CONFIRM_REMOVE_CONNECTION_BUTTON.by,
|
||||
AccountSetting.CONFIRM_REMOVE_CONNECTION_BUTTON.selector,
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def logout():
|
||||
AccountSetting.account_action("Log out")
|
||||
|
||||
@staticmethod
|
||||
def login():
|
||||
AccountSetting.account_action("Log in")
|
||||
|
||||
@staticmethod
|
||||
def get_account_connection_label():
|
||||
labels = app().find_elements(
|
||||
AccountSetting.ACCOUNT_CONNECTION_LABEL.by,
|
||||
AccountSetting.ACCOUNT_CONNECTION_LABEL.selector,
|
||||
)
|
||||
# first label is the sync status label
|
||||
return labels[0].text
|
||||
|
||||
@staticmethod
|
||||
def is_connecting():
|
||||
return "Connecting to" in AccountSetting.get_account_connection_label()
|
||||
|
||||
@staticmethod
|
||||
def is_user_signed_out():
|
||||
return "Signed out" in AccountSetting.get_account_connection_label()
|
||||
|
||||
@staticmethod
|
||||
def is_user_signed_in():
|
||||
return "Connected" in AccountSetting.get_account_connection_label()
|
||||
|
||||
@staticmethod
|
||||
def wait_until_connection_is_configured(timeout=5000):
|
||||
result = squish.waitFor(
|
||||
AccountSetting.is_connecting,
|
||||
timeout,
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise TimeoutError(
|
||||
"Timeout waiting for connection to be configured for "
|
||||
+ str(timeout)
|
||||
+ " milliseconds"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def wait_until_account_is_connected(timeout=5000):
|
||||
result = wait_for(
|
||||
AccountSetting.is_user_signed_in,
|
||||
timeout,
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise TimeoutError(
|
||||
"Timeout waiting for the account to be connected for "
|
||||
+ str(timeout)
|
||||
+ " milliseconds"
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def wait_until_sync_folder_is_configured(timeout=5000):
|
||||
result = squish.waitFor(
|
||||
lambda: not squish.waitForObjectExists(
|
||||
AccountSetting.ACCOUNT_LOADING
|
||||
).visible,
|
||||
timeout,
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise TimeoutError(
|
||||
"Timeout waiting for sync folder to be connected for "
|
||||
+ str(timeout)
|
||||
+ " milliseconds"
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def press_key(key):
|
||||
key = key.replace('"', "")
|
||||
key = f"<{key}>"
|
||||
squish.nativeType(key)
|
||||
|
||||
@staticmethod
|
||||
def is_log_dialog_visible():
|
||||
visible = False
|
||||
try:
|
||||
visible = squish.waitForObjectExists(
|
||||
AccountSetting.LOG_BROWSER_WINDOW
|
||||
).visible
|
||||
except:
|
||||
pass
|
||||
return visible
|
||||
|
||||
@staticmethod
|
||||
def remove_connection_for_user(username):
|
||||
Toolbar.open_account(username)
|
||||
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_removed():
|
||||
account = Toolbar.get_account(username)
|
||||
return account is None
|
||||
|
||||
result = wait_for(account_removed, timeout)
|
||||
|
||||
if not result:
|
||||
raise TimeoutError(
|
||||
"Timeout waiting for account to be removed for "
|
||||
+ str(timeout)
|
||||
+ " milliseconds"
|
||||
)
|
||||
@@ -0,0 +1,216 @@
|
||||
# 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
|
||||
from helpers.ConfigHelper import get_config
|
||||
from helpers.AppHelper import app
|
||||
|
||||
|
||||
class Activity:
|
||||
TAB_CONTAINER = SimpleNamespace(by=None, selector=None)
|
||||
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")
|
||||
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.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)
|
||||
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 {
|
||||
"column": 1,
|
||||
"container": Activity.NOT_SYNCED_TABLE,
|
||||
"text": resource,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_not_synced_status(row):
|
||||
return squish.waitForObjectExists(
|
||||
{
|
||||
"column": 6,
|
||||
"row": row,
|
||||
"container": Activity.NOT_SYNCED_TABLE,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
).text
|
||||
|
||||
@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()
|
||||
|
||||
@staticmethod
|
||||
def check_file_exist(filename):
|
||||
squish.waitForObjectExists(
|
||||
Activity.get_not_synced_file_selector(
|
||||
RegularExpression(build_conflicted_regex(filename))
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_resource_blacklisted(filename):
|
||||
result = squish.waitFor(
|
||||
lambda: Activity.has_sync_status(filename, "Blacklisted"),
|
||||
get_config("maxSyncTimeout") * 1000,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_resource_ignored(filename):
|
||||
result = squish.waitFor(
|
||||
lambda: Activity.has_sync_status(filename, "File Ignored"),
|
||||
get_config("maxSyncTimeout") * 1000,
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
return True
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def select_synced_filter(sync_filter):
|
||||
menu = app().find_element(
|
||||
Activity.LOCAL_ACTIVITY_FILTER_BUTTON.by,
|
||||
Activity.LOCAL_ACTIVITY_FILTER_BUTTON.selector,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_synced_file_selector(resource):
|
||||
return {
|
||||
"column": Activity.get_synced_table_column_number_by_name("File"),
|
||||
"container": Activity.LOCAL_ACTIVITY_TABLE,
|
||||
"text": resource,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_synced_table_column_number_by_name(column_name):
|
||||
return squish.waitForObject(
|
||||
{
|
||||
"container": Activity.SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR,
|
||||
"text": column_name,
|
||||
"type": "HeaderViewItem",
|
||||
"visible": True,
|
||||
}
|
||||
)["section"]
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
squish.clickButton(squish.waitForObject(Activity.NOT_SYNCED_FILTER_BUTTON))
|
||||
squish.activateItem(
|
||||
squish.waitForObjectItem(
|
||||
Activity.NOT_SYNCED_FILTER_OPTION_SELECTOR, filter_option
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_not_synced_table_column_number_by_name(column_name):
|
||||
return squish.waitForObject(
|
||||
{
|
||||
"container": Activity.NOT_SYNCED_ACTIVITY_TABLE_HEADER_SELECTOR,
|
||||
"text": column_name,
|
||||
"type": "HeaderViewItem",
|
||||
"visible": True,
|
||||
}
|
||||
)["section"]
|
||||
|
||||
@staticmethod
|
||||
def check_not_synced_table(resource, status, account):
|
||||
try:
|
||||
file_row = squish.waitForObject(
|
||||
Activity.get_not_synced_file_selector(resource),
|
||||
get_config("lowestSyncTimeout") * 1000,
|
||||
)["row"]
|
||||
squish.waitForObjectExists(
|
||||
{
|
||||
"column": Activity.get_not_synced_table_column_number_by_name(
|
||||
"Status"
|
||||
),
|
||||
"row": file_row,
|
||||
"container": Activity.NOT_SYNCED_TABLE,
|
||||
"text": status,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
)
|
||||
squish.waitForObjectExists(
|
||||
{
|
||||
"column": Activity.get_not_synced_table_column_number_by_name(
|
||||
"Account"
|
||||
),
|
||||
"row": file_row,
|
||||
"container": Activity.NOT_SYNCED_TABLE,
|
||||
"text": account,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
@@ -0,0 +1,42 @@
|
||||
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.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')]",
|
||||
)
|
||||
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
|
||||
)
|
||||
username = label.split(" ", maxsplit=2)[1]
|
||||
return username.capitalize()
|
||||
|
||||
def oidc_relogin(self, username, password):
|
||||
AccountConnectionWizard.copy_login_url()
|
||||
authorize_via_webui(username, password)
|
||||
|
||||
def relogin(self, username, password, oauth=False):
|
||||
self.oidc_relogin(username, password)
|
||||
|
||||
def login_after_setup(self, username, password):
|
||||
self.oidc_relogin(username, password)
|
||||
|
||||
def accept_certificate(self):
|
||||
AccountConnectionWizard.accept_certificate()
|
||||
@@ -0,0 +1,297 @@
|
||||
from datetime import datetime
|
||||
import test
|
||||
import names
|
||||
import squish
|
||||
import object # pylint: disable=redefined-builtin
|
||||
|
||||
|
||||
class PublicLinkDialog:
|
||||
PUBLIC_LINKS_TAB = {
|
||||
"container": names.sharingDialog_qt_tabwidget_tabbar_QTabBar,
|
||||
"text": "Public Links",
|
||||
"type": "TabItem",
|
||||
}
|
||||
PASSWORD_CHECKBOX = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "checkBox_password",
|
||||
"type": "QCheckBox",
|
||||
"visible": 1,
|
||||
}
|
||||
PASSWORD_INPUT_FIELD = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "lineEdit_password",
|
||||
"type": "QLineEdit",
|
||||
"visible": 1,
|
||||
}
|
||||
EXPIRYDATE_CHECKBOX = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "checkBox_expire",
|
||||
"type": "QCheckBox",
|
||||
"visible": 1,
|
||||
}
|
||||
CREATE_SHARE_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "createShareButton",
|
||||
"type": "QPushButton",
|
||||
"visible": 1,
|
||||
}
|
||||
PUBLIC_LINK_NAME = {
|
||||
"column": 0,
|
||||
"container": names.oCC_ShareLinkWidget_linkShares_QTableWidget,
|
||||
"row": 0,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
EXPIRATION_DATE_FIELD = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "qt_spinbox_lineedit",
|
||||
"type": "QLineEdit",
|
||||
"visible": 1,
|
||||
}
|
||||
READ_ONLY_RADIO_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "radio_readOnly",
|
||||
"type": "QRadioButton",
|
||||
"visible": 1,
|
||||
}
|
||||
READ_WRITE_RADIO_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "radio_readWrite",
|
||||
"type": "QRadioButton",
|
||||
"visible": 1,
|
||||
}
|
||||
UPLOAD_ONLY_RADIO_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "radio_uploadOnly",
|
||||
"type": "QRadioButton",
|
||||
"visible": 1,
|
||||
}
|
||||
DELETE_LINK_BUTTON = {
|
||||
"container": names.oCC_ShareLinkWidget_linkShares_QTableWidget,
|
||||
"type": "QToolButton",
|
||||
"occurrence": 2,
|
||||
}
|
||||
CONFIRM_LINK_DELETE_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"text": "Delete",
|
||||
"type": "QPushButton",
|
||||
}
|
||||
UPDATE_PASSWORD_BUTTON = {
|
||||
"container": names.qt_tabwidget_stackedwidget_OCC_ShareLinkWidget_OCC_ShareLinkWidget,
|
||||
"name": "pushButton_setPassword",
|
||||
"type": "QPushButton",
|
||||
"visible": 1,
|
||||
}
|
||||
|
||||
DATE_FORMATS = [
|
||||
"%m/%d/%Y",
|
||||
"%d/%m/%Y",
|
||||
"%Y-%m-%d",
|
||||
"%d-%m-%Y",
|
||||
"%Y/%m/%d",
|
||||
"%m/%d/%y",
|
||||
"%d/%m/%y",
|
||||
]
|
||||
# to store current default public link expiry date
|
||||
default_expiry_date = ""
|
||||
|
||||
@staticmethod
|
||||
def parse_date(date):
|
||||
for date_format in PublicLinkDialog.DATE_FORMATS:
|
||||
try:
|
||||
date = str(datetime.strptime(date, date_format))
|
||||
date = date.split(" ", maxsplit=1)[0]
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return date
|
||||
|
||||
@staticmethod
|
||||
def set_default_expiry_date(default_date):
|
||||
PublicLinkDialog.default_expiry_date = PublicLinkDialog.parse_date(default_date)
|
||||
|
||||
@staticmethod
|
||||
def get_default_expiry_date():
|
||||
return PublicLinkDialog.default_expiry_date
|
||||
|
||||
@staticmethod
|
||||
def open_public_link_tab():
|
||||
squish.mouseClick(squish.waitForObject(PublicLinkDialog.PUBLIC_LINKS_TAB))
|
||||
|
||||
@staticmethod
|
||||
def create_public_link(password="", permissions="", expire_date=""):
|
||||
permission_locator = ""
|
||||
if permissions:
|
||||
permission_locator = PublicLinkDialog.get_radio_object_for_permssion(
|
||||
permissions
|
||||
)
|
||||
|
||||
if permission_locator:
|
||||
squish.clickButton(squish.waitForObject(permission_locator))
|
||||
|
||||
if password:
|
||||
PublicLinkDialog.set_password(password)
|
||||
|
||||
if expire_date:
|
||||
PublicLinkDialog.set_expiration_date(expire_date)
|
||||
|
||||
squish.clickButton(squish.waitForObject(PublicLinkDialog.CREATE_SHARE_BUTTON))
|
||||
squish.waitFor(
|
||||
lambda: (
|
||||
squish.waitForObject(PublicLinkDialog.PUBLIC_LINK_NAME).displayText
|
||||
== "Public link"
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def toggle_password():
|
||||
squish.clickButton(squish.waitForObject(PublicLinkDialog.PASSWORD_CHECKBOX))
|
||||
|
||||
@staticmethod
|
||||
def set_password(password):
|
||||
if not squish.waitForObjectExists(PublicLinkDialog.PASSWORD_CHECKBOX).checked:
|
||||
PublicLinkDialog.toggle_password()
|
||||
|
||||
squish.mouseClick(squish.waitForObject(PublicLinkDialog.PASSWORD_INPUT_FIELD))
|
||||
squish.type(
|
||||
squish.waitForObject(PublicLinkDialog.PASSWORD_INPUT_FIELD),
|
||||
password,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def toggle_expiration_date():
|
||||
squish.clickButton(squish.waitForObject(PublicLinkDialog.EXPIRYDATE_CHECKBOX))
|
||||
|
||||
@staticmethod
|
||||
def set_expiration_date(expire_date):
|
||||
enabled = squish.waitForObjectExists(
|
||||
PublicLinkDialog.EXPIRYDATE_CHECKBOX
|
||||
).checked
|
||||
if not enabled:
|
||||
PublicLinkDialog.toggle_expiration_date()
|
||||
|
||||
if not expire_date == "default":
|
||||
exp_date = datetime.strptime(expire_date, "%Y-%m-%d")
|
||||
exp_year = exp_date.year - 2000
|
||||
squish.mouseClick(
|
||||
squish.waitForObject(PublicLinkDialog.EXPIRATION_DATE_FIELD)
|
||||
)
|
||||
# Move the cursor to year (last) field and enter the year
|
||||
squish.nativeType("<Ctrl+Right>")
|
||||
squish.nativeType("<Ctrl+Right>")
|
||||
squish.nativeType(exp_year)
|
||||
|
||||
# Move the cursor to day (middle) field and enter the day
|
||||
squish.nativeType("<Ctrl+Left>")
|
||||
squish.nativeType(exp_date.day)
|
||||
|
||||
# Move the cursor to month (first) field and enter the month
|
||||
# Backspace works most of the time, so we clear the data using backspace
|
||||
squish.nativeType("<Ctrl+Left>")
|
||||
squish.nativeType("<Ctrl+Left>")
|
||||
squish.nativeType("<Right>")
|
||||
squish.nativeType("<Backspace>")
|
||||
squish.nativeType("<Backspace>")
|
||||
squish.nativeType(exp_date.month)
|
||||
squish.nativeType("<Return>")
|
||||
|
||||
actual_date = str(
|
||||
squish.waitForObjectExists(
|
||||
PublicLinkDialog.EXPIRATION_DATE_FIELD
|
||||
).displayText
|
||||
)
|
||||
expected_date = f"{exp_date.month}/{exp_date.day}/{exp_year}"
|
||||
if not actual_date == expected_date:
|
||||
test.log(
|
||||
f"Expected date: {expected_date} is not same as that of actual date: {actual_date}"
|
||||
+ ", trying workaround"
|
||||
)
|
||||
# retry with workaround
|
||||
PublicLinkDialog.set_expiration_date_with_workaround(
|
||||
exp_year, exp_date.month, exp_date.day
|
||||
)
|
||||
actual_date = str(
|
||||
squish.waitForObjectExists(
|
||||
PublicLinkDialog.EXPIRATION_DATE_FIELD
|
||||
).displayText
|
||||
)
|
||||
if not actual_date == expected_date:
|
||||
test.fail(
|
||||
f"workaround failed:\nactual date: {actual_date}\nexpected date: {expected_date}"
|
||||
)
|
||||
else:
|
||||
default_date = str(
|
||||
squish.waitForObjectExists(
|
||||
PublicLinkDialog.EXPIRATION_DATE_FIELD
|
||||
).displayText
|
||||
)
|
||||
PublicLinkDialog.set_default_expiry_date(default_date)
|
||||
|
||||
# This workaround is needed because the above function 'set_expiration_date' can not set the month field sometime.
|
||||
# See for more details: https://github.com/owncloud/client/issues/9218
|
||||
@staticmethod
|
||||
def set_expiration_date_with_workaround(year, month, day):
|
||||
squish.mouseClick(squish.waitForObject(PublicLinkDialog.EXPIRATION_DATE_FIELD))
|
||||
|
||||
# date can only be set to future date. But sometimes it can not modify the month field in first attempt.
|
||||
# date format is 'm/d/yy', so we have to edit 'month' first
|
||||
# then 'day' and then 'year'.
|
||||
# Delete data from month field
|
||||
squish.nativeType("<Delete>")
|
||||
squish.nativeType("<Delete>")
|
||||
squish.nativeType(month)
|
||||
squish.nativeType(day)
|
||||
squish.nativeType(year)
|
||||
squish.nativeType("<Return>")
|
||||
|
||||
@staticmethod
|
||||
def get_radio_object_for_permssion(permissions):
|
||||
permission_locator = None
|
||||
if permissions in ("Download / View", "Viewer"):
|
||||
permission_locator = PublicLinkDialog.READ_ONLY_RADIO_BUTTON
|
||||
elif permissions in ("Download / View / Edit", "Editor"):
|
||||
permission_locator = PublicLinkDialog.READ_WRITE_RADIO_BUTTON
|
||||
elif permissions in ("Upload only (File Drop)", "Contributor"):
|
||||
permission_locator = PublicLinkDialog.UPLOAD_ONLY_RADIO_BUTTON
|
||||
else:
|
||||
raise LookupError("No such radio object found for given permission")
|
||||
|
||||
return permission_locator
|
||||
|
||||
@staticmethod
|
||||
def create_public_link_with_role(role):
|
||||
permission_locator = PublicLinkDialog.get_radio_object_for_permssion(role)
|
||||
|
||||
squish.clickButton(squish.waitForObject(permission_locator))
|
||||
squish.clickButton(squish.waitForObject(PublicLinkDialog.CREATE_SHARE_BUTTON))
|
||||
squish.waitFor(
|
||||
lambda: (
|
||||
squish.findObject(PublicLinkDialog.PUBLIC_LINK_NAME).displayText
|
||||
== "Public link"
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def change_password(password):
|
||||
PublicLinkDialog.set_password(password)
|
||||
squish.clickButton(
|
||||
squish.waitForObject(PublicLinkDialog.UPDATE_PASSWORD_BUTTON)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_public_link():
|
||||
squish.clickButton(squish.waitForObject(PublicLinkDialog.DELETE_LINK_BUTTON))
|
||||
squish.clickButton(
|
||||
squish.waitForObject(PublicLinkDialog.CONFIRM_LINK_DELETE_BUTTON)
|
||||
)
|
||||
squish.waitFor(
|
||||
lambda: (not object.exists(PublicLinkDialog.DELETE_LINK_BUTTON)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_expiration_date():
|
||||
default_date = str(
|
||||
squish.waitForObjectExists(
|
||||
PublicLinkDialog.EXPIRATION_DATE_FIELD
|
||||
).displayText
|
||||
)
|
||||
return PublicLinkDialog.parse_date(default_date)
|
||||
@@ -0,0 +1,101 @@
|
||||
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=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):
|
||||
selector = Settings.CHECKBOX_OPTION_ITEM.copy()
|
||||
selector.update({"name": name})
|
||||
if name == "languageDropdown":
|
||||
selector.update({"type": "QComboBox"})
|
||||
elif name in ("ignoredFilesButton", "logSettingsButton"):
|
||||
selector.update({"type": "QPushButton"})
|
||||
return selector
|
||||
|
||||
@staticmethod
|
||||
def get_network_option_selector(name):
|
||||
selector = Settings.NETWORK_OPTION_ITEM.copy()
|
||||
selector.update({"name": name})
|
||||
return selector
|
||||
|
||||
@staticmethod
|
||||
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 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 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_dialog():
|
||||
app().find_element(
|
||||
Settings.ABOUT_BUTTON.by, Settings.ABOUT_BUTTON.selector
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
def has_about_dialog():
|
||||
return (
|
||||
app()
|
||||
.find_element(Settings.ABOUT_DIALOG.by, Settings.ABOUT_DIALOG.selector)
|
||||
.is_displayed()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def close_about_dialog():
|
||||
app().find_element(
|
||||
Settings.ABOUT_DIALOG_OK_BUTTON.by, Settings.ABOUT_DIALOG_OK_BUTTON.selector
|
||||
).click()
|
||||
@@ -0,0 +1,131 @@
|
||||
from types import SimpleNamespace
|
||||
from appium.webdriver.common.appiumby import AppiumBy as By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
from helpers.ConfigHelper import get_config
|
||||
from helpers.AppHelper import app
|
||||
|
||||
|
||||
class SyncConnection:
|
||||
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 get_current_account_connection():
|
||||
connections = app().find_elements(
|
||||
SyncConnection.ACCOUNT_CONNECTION_CONTAINER.by,
|
||||
SyncConnection.ACCOUNT_CONNECTION_CONTAINER.selector,
|
||||
)
|
||||
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'),
|
||||
),
|
||||
)
|
||||
menu_button.native_click(button='right')
|
||||
|
||||
@staticmethod
|
||||
def perform_action(action):
|
||||
SyncConnection.open_menu()
|
||||
app().find_element(SyncConnection.MENU_ITEM.by, action).click()
|
||||
|
||||
@staticmethod
|
||||
def force_sync():
|
||||
SyncConnection.perform_action("Force sync now")
|
||||
|
||||
@staticmethod
|
||||
def pause_sync():
|
||||
SyncConnection.perform_action("Pause sync")
|
||||
|
||||
@staticmethod
|
||||
def resume_sync():
|
||||
SyncConnection.perform_action("Resume sync")
|
||||
|
||||
@staticmethod
|
||||
def has_menu_item(item):
|
||||
return squish.waitForObjectItem(SyncConnection.MENU_ITEM, item)
|
||||
|
||||
@staticmethod
|
||||
def menu_item_exists(menu_item):
|
||||
obj = SyncConnection.MENU_ITEM.copy()
|
||||
obj.update({"type": "QAction", "text": menu_item})
|
||||
return object.exists(obj)
|
||||
|
||||
@staticmethod
|
||||
def choose_what_to_sync():
|
||||
SyncConnection.open_menu()
|
||||
SyncConnection.perform_action("Choose what to sync")
|
||||
|
||||
@staticmethod
|
||||
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 NoSuchElementException:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def remove_folder_sync_connection():
|
||||
SyncConnection.perform_action("Remove Space")
|
||||
|
||||
@staticmethod
|
||||
def cancel_folder_sync_connection_removal():
|
||||
squish.clickButton(
|
||||
squish.waitForObject(SyncConnection.CANCEL_FOLDER_SYNC_CONNECTION_DIALOG)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def confirm_folder_sync_connection_removal():
|
||||
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):
|
||||
"""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,
|
||||
)
|
||||
if not status:
|
||||
action = "appear" if to_exist else "disappear"
|
||||
raise AssertionError(f"Permission error label did not {action}")
|
||||
|
||||
@staticmethod
|
||||
def get_permission_error_message():
|
||||
"""Get the permission error message text"""
|
||||
SyncConnection.wait_for_error_label(True) # Wait for label to appear
|
||||
return str(squish.waitForObject(SyncConnection.PERMISSION_ERROR_LABEL).text)
|
||||
@@ -0,0 +1,283 @@
|
||||
from types import SimpleNamespace
|
||||
from appium.webdriver.common.appiumby import AppiumBy as By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from helpers.SetupClientHelper import get_current_user_sync_path
|
||||
from helpers.AppHelper import app
|
||||
|
||||
|
||||
class SyncConnectionWizard:
|
||||
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_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=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)
|
||||
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()
|
||||
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
|
||||
def set_sync_path(sync_path=""):
|
||||
SyncConnectionWizard.set_sync_path_oc(sync_path)
|
||||
|
||||
@staticmethod
|
||||
def next_step():
|
||||
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():
|
||||
squish.clickButton(squish.waitForObject(SyncConnectionWizard.BACK_BUTTON))
|
||||
|
||||
@staticmethod
|
||||
def select_remote_destination_folder(folder):
|
||||
squish.mouseClick(
|
||||
squish.waitForObjectItem(SyncConnectionWizard.REMOTE_FOLDER_TREE, folder)
|
||||
)
|
||||
SyncConnectionWizard.next_step()
|
||||
|
||||
@staticmethod
|
||||
def deselect_all_remote_folders():
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def sort_by(header_text):
|
||||
squish.mouseClick(
|
||||
squish.waitForObject(
|
||||
{
|
||||
"container": SyncConnectionWizard.SELECTIVE_SYNC_TREE_HEADER,
|
||||
"text": header_text,
|
||||
"type": "HeaderViewItem",
|
||||
"visible": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def add_sync_connection():
|
||||
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):
|
||||
folder_row = {
|
||||
"row": row_index,
|
||||
"container": SyncConnectionWizard.SELECTIVE_SYNC_ROOT_FOLDER,
|
||||
"type": "QModelIndex",
|
||||
}
|
||||
return str(squish.waitForObjectExists(folder_row).displayText)
|
||||
|
||||
@staticmethod
|
||||
def is_root_folder_checked():
|
||||
state = squish.waitForObject(SyncConnectionWizard.SELECTIVE_SYNC_ROOT_FOLDER)[
|
||||
"checkState"
|
||||
]
|
||||
return state == "checked"
|
||||
|
||||
@staticmethod
|
||||
def 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):
|
||||
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
|
||||
),
|
||||
)
|
||||
# ISSUE: https://github.com/opencloud-eu/desktop/pull/879
|
||||
# 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()
|
||||
if space_item.get_attribute("selected") != "true":
|
||||
raise AssertionError("Failed to select the space: " + space_name)
|
||||
|
||||
@staticmethod
|
||||
def sync_space(space_name):
|
||||
SyncConnectionWizard.set_sync_path(get_current_user_sync_path())
|
||||
SyncConnectionWizard.select_space(space_name)
|
||||
SyncConnectionWizard.next_step()
|
||||
SyncConnectionWizard.add_sync_connection()
|
||||
|
||||
@staticmethod
|
||||
def create_folder_in_remote_destination(folder_name):
|
||||
squish.clickButton(
|
||||
squish.waitForObject(SyncConnectionWizard.CREATE_REMOTE_FOLDER_BUTTON)
|
||||
)
|
||||
squish.type(
|
||||
squish.waitForObject(SyncConnectionWizard.CREATE_REMOTE_FOLDER_INPUT),
|
||||
folder_name,
|
||||
)
|
||||
squish.clickButton(
|
||||
squish.waitForObject(
|
||||
SyncConnectionWizard.CREATE_REMOTE_FOLDER_CONFIRM_BUTTON
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def refresh_remote():
|
||||
squish.clickButton(squish.waitForObject(SyncConnectionWizard.REFRESH_BUTTON))
|
||||
|
||||
@staticmethod
|
||||
def is_remote_folder_selected(folder_selector):
|
||||
return squish.waitForObjectExists(folder_selector).selected
|
||||
|
||||
@staticmethod
|
||||
def open_sync_connection_wizard():
|
||||
squish.mouseClick(
|
||||
squish.waitForObject(SyncConnectionWizard.ADD_FOLDER_SYNC_BUTTON)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_local_sync_path():
|
||||
return str(
|
||||
squish.waitForObjectExists(
|
||||
SyncConnectionWizard.CHOOSE_LOCAL_SYNC_FOLDER
|
||||
).displayText
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_warn_label():
|
||||
return str(squish.waitForObjectExists(SyncConnectionWizard.WARN_LABEL).text)
|
||||
|
||||
@staticmethod
|
||||
def is_add_sync_folder_button_enabled():
|
||||
return squish.waitForObjectExists(
|
||||
SyncConnectionWizard.ADD_FOLDER_SYNC_BUTTON
|
||||
).enabled
|
||||
|
||||
@staticmethod
|
||||
def select_or_unselect_folders_to_sync(folders, select=True):
|
||||
expected_state = "true" if select else "false"
|
||||
|
||||
if select:
|
||||
SyncConnectionWizard.deselect_all_remote_folders()
|
||||
|
||||
for folder_path in folders:
|
||||
parents = folder_path.strip("/").split("/")
|
||||
target_folder = parents.pop()
|
||||
|
||||
parent_element = None
|
||||
parent_position = 0
|
||||
for parent in parents:
|
||||
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'
|
||||
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():
|
||||
print('[WARN] Folder was not expanded, retrying with space key')
|
||||
# expand using space key
|
||||
parent_element.native_click()
|
||||
parent_element.native_send_keys(Keys.SPACE)
|
||||
if parent_element.is_selected():
|
||||
raise AssertionError(f'Failed to expand folder: {parent}')
|
||||
|
||||
folder_element = None
|
||||
target_folders = app().find_elements(By.NAME, target_folder)
|
||||
# select the folder that is inside the current parent position
|
||||
for folder in target_folders:
|
||||
if folder.rect["x"] > parent_position:
|
||||
folder_element = folder
|
||||
break
|
||||
is_checked = folder_element.get_attribute("checked")
|
||||
# return early if the folder is already in the expected state.
|
||||
if is_checked == expected_state:
|
||||
return
|
||||
|
||||
folder_element.native_click()
|
||||
if not folder_element.is_selected():
|
||||
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:
|
||||
raise AssertionError(
|
||||
f"Failed to {'select' if select else 'unselect'} folder: {folder_path}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def confirm_choose_what_to_sync_selection():
|
||||
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)
|
||||
|
||||
if new_sync_connection_wizard:
|
||||
SyncConnectionWizard.add_sync_connection()
|
||||
else:
|
||||
SyncConnectionWizard.confirm_choose_what_to_sync_selection()
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
@@ -0,0 +1,181 @@
|
||||
from types import SimpleNamespace
|
||||
from urllib.parse import urlparse
|
||||
from appium.webdriver.common.appiumby import AppiumBy as By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
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_TAB = SimpleNamespace(by=By.CLASS_NAME, selector="[page tab | {text}]")
|
||||
ADD_ACCOUNT_BUTTON = SimpleNamespace(
|
||||
by=By.CLASS_NAME, selector="[push button | Add Account]"
|
||||
)
|
||||
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,
|
||||
selector="Yes",
|
||||
)
|
||||
|
||||
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 {
|
||||
"container": names.dialogStack_quickWidget_QQuickWidget,
|
||||
"text": item_name,
|
||||
"type": "Label",
|
||||
"visible": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
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_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
|
||||
# TODO: Remove the workaround and uncomment 'click' action
|
||||
# tab.click()
|
||||
tab.native_click()
|
||||
if tab.get_attribute("checked") != "true":
|
||||
raise AssertionError("Activity tab is not active")
|
||||
|
||||
@staticmethod
|
||||
def open_new_account_setup():
|
||||
app().find_element(
|
||||
Toolbar.ADD_ACCOUNT_BUTTON.by,
|
||||
Toolbar.ADD_ACCOUNT_BUTTON.selector,
|
||||
).click()
|
||||
|
||||
@staticmethod
|
||||
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.native_click()
|
||||
# confirm account is active
|
||||
if account_tab.get_attribute("checked") != "true":
|
||||
raise AssertionError(f"Account is not active: {username}")
|
||||
|
||||
@staticmethod
|
||||
def get_displayed_account_text(displayname, host):
|
||||
return str(
|
||||
squish.waitForObjectExists(
|
||||
Toolbar.get_item_selector(displayname + "\n" + host)
|
||||
).text
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def open_settings_tab():
|
||||
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_qsfera():
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def get_accounts():
|
||||
accounts = {}
|
||||
selectors = {}
|
||||
children_obj = object.children(squish.waitForObjectExists(Toolbar.TOOLBAR_ROW))
|
||||
account_idx = 1
|
||||
for obj in children_obj:
|
||||
if hasattr(obj, "accountState"):
|
||||
account_info = {
|
||||
"displayname": str(obj.accountState.account.davDisplayName),
|
||||
"hostname": str(obj.accountState.account.hostName),
|
||||
"initials": str(obj.accountState.account.initials),
|
||||
"current": obj.checked,
|
||||
}
|
||||
account_locator = Toolbar.ACCOUNT_TAB.copy()
|
||||
if account_idx > 1:
|
||||
account_locator.update({"occurrence": account_idx})
|
||||
account_locator.update({"text": account_info["hostname"]})
|
||||
|
||||
accounts[account_info["displayname"]] = account_info
|
||||
selectors[account_info["displayname"]] = obj
|
||||
account_idx += 1
|
||||
return accounts, selectors
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
try:
|
||||
account = app().find_element(
|
||||
Toolbar.ACCOUNT_TAB.by,
|
||||
Toolbar.ACCOUNT_TAB.selector.format(text=account_label),
|
||||
)
|
||||
except NoSuchElementException:
|
||||
pass
|
||||
return account
|
||||
|
||||
@staticmethod
|
||||
def get_active_account():
|
||||
accounts, selectors = Toolbar.get_accounts()
|
||||
for account, info in accounts.items():
|
||||
if info["current"]:
|
||||
return info, selectors[account]
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def account_has_focus(username):
|
||||
account = Toolbar.get_account(username)
|
||||
return account.get_attribute("checked") == "true"
|
||||
|
||||
@staticmethod
|
||||
def account_exists(username):
|
||||
account = Toolbar.get_account(username)
|
||||
return account is not None
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,239 @@
|
||||
## Preparation
|
||||
|
||||
- create a release or test plan issue in GitHub
|
||||
- copy each test section of this test plan into a separate comment in the issue
|
||||
- add links to every section in the main comment
|
||||
```
|
||||
* [ ] 1. Install, Setup and Configuration @tester1
|
||||
-> https://github.com/opencloud-eu/desktop/issues/xxxx#issuecomment-xxxxxxxxx
|
||||
* [ ] 2. Folders
|
||||
-> ...
|
||||
* [ ] 3. Files
|
||||
-> ...
|
||||
* [ ] 4. Move files and folders
|
||||
-> ...
|
||||
* [ ] 5. Edit Files
|
||||
-> ...
|
||||
* [ ] 6. Delete Files and Folders
|
||||
-> ...
|
||||
* [ ] 7. Sync process
|
||||
-> ...
|
||||
* [ ] 8. Spaces and Sharing
|
||||
-> ...
|
||||
* [ ] 9. Pause and resume sync
|
||||
-> ...
|
||||
* [ ] 10. Advanced configuration
|
||||
-> ...
|
||||
* [ ] 11. Selective sync
|
||||
-> ...
|
||||
* [ ] 12. Overlay icons
|
||||
-> ...
|
||||
* [ ] 13. Context menu
|
||||
-> ...
|
||||
* [ ] 14. VFS
|
||||
-> ...
|
||||
```
|
||||
|
||||
- If not stated otherwise each test should be run on every platform win/linux/mac. Record the result as per platform.
|
||||
- "Enable logging to temporary folder" and "Log Http traffic" (tab 'Settings', button 'Log Settings') to have log-files available if needed to report an issue.
|
||||
- Make sure all automated UI tests run successfully in CI and run them locally on Windows.
|
||||
- all tests that are automated are marked with a :robot: in the 'Result' column (for the corresponding platform) and the name of the test in the 'Related Comment' column
|
||||
- If multiple people are working on the tests at the same time, mark the tests everyone is working on with the GitHub name of the tester.
|
||||
- Add the test result to the 'Result' column. For success: :heavy_check_mark:, failure: :x: (link the reported #issue to 'Related Comment')
|
||||
|
||||
## Testplan
|
||||
|
||||
### 1. Install, Setup and Configuration
|
||||
|
||||
Try everything here once with Keycloak and once with Lico (the single binary builtin IdP).
|
||||
To easily run a Keycloak setup, use the [deployment examples](https://github.com/opencloud-eu/opencloud/tree/main/deployments/examples/opencloud_full).
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|---------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Update Installation | 1. Install a previous version<br>2. Update to the new version | | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 2 | Install the new version | Delete any previous version<br><br> **Windows:** <br> - Install the desktop client using .exe installer.<br><br> **Mac:** <br> - Install the desktop client using .pkg installer <br><br> **Ubuntu or Debian with GNOME desktop:** <br> - Run the client as AppImage. <br><br> **Fedora with GNOME desktop:** <br> - Run the client as AppImage. <br><br> **All platforms:** <br> - Check the version from the Settings tab -> About | installed version is correct | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 3 | Verify that you can enter a server address (self signed cert) | 1. Launch desktop client<br>2. Enter a server address<br>3. Click on Next<br>4. If it is the first time you should accept the certificate | | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
| 4 | Valid Login | 1. Log in with the correct username and password | Login successful | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
| 5 | Invalid Login | 1. Try to log in with wrong username or password | Error message `Logon failed. Please verify your credentials and try again.` is shown | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 6 | Skip sync folder | Skip sync folder configuration during setup | Setup is completed, no folder is synced, but can be added later | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 7 | Configure sync folder | Configure sync folder configuration during setup | Setup is completed, folder is synced | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
| 8 | Remove an account | 1. Add an account to desktop client<br>2. Upload files and folders via WebUI<br>3. Wait for sync to complete<br>4. Remove the account | The account is removed from the Desktop client but the files and folder exist on the file system | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_removeAccountConnection |
|
||||
| 9 | re-add an account | 1. Add an account to desktop client<br>2. Upload files and folders via WebUI<br>3. Wait for sync to complete<br>4. Remove the account<br>5. Add the same account again | The account is added, all files and folder are synced | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
### 2. Folders
|
||||
|
||||
- 'Go to local sync folder and create a folder' means: "Open folder" on client dot ... menu, create a new folder in file browser
|
||||
- 'Verify on the server' means: the folder is sent from client to server / check either with web browser or another client
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|-------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | create a folder | 1. Add an account to desktop client<br>2. Open local sync folder<br>3. Create a folder<br>4. Wait for sync to complete | The folder is available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 2 | folder with long name | 1. Create a folder with a long name (~100 characters)<br>2. Wait for sync to complete | The folder is available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 3 | folder with special characters | 1. Create a folder with special characters and emojis (e.g. `$%ñ&💥🫨❤️🔥`) in the local sync folder<br>2. Wait for sync to complete | The folder is available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 4 | folder with many subfolders | 1. Inside the sync root, create a folder with 5 empty subfolders and 5 subfolders containing files<br>2. Wait for sync to complete | All 10 subfolders are available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 5 | sync many folders at once | 1. Create 400 folders outside the sync root<br>2. Move those folders into the sync root<br>3. Wait for sync to complete | All 400 folders are available on the server | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 6 | copy of a folder | 1. Open local sync folder<br>2. Create a folder with some files in it<br>3. Copy and paste that folder<br>4. Wait for sync to complete<br>5. Check the folders and the content of file inside it | Both original and copied folders are available and contains the file on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 7 | subfolder with long name | 1. Open local sync folder<br>2. Create a folder<br>3. Create a subfolder with long name (~220 characters)<br>4. Wait for sync to complete | Folder and subfolder are available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 8 | pre-existing folders | 1. Quit the desktop client<br>2. Goto the local sync folder<br>3. Create folders at several levels<br>4. Open the desktop client<br>5. Wait for sync to complete | All folders are available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 9 | .zip/.rar files with elaborate internal folder structures | 1. Create a zip file with many internal folders and files<br>2. Copy that zip file into the sync root<br>3. Unzip that zip file inside the sync root | All extracted files and folders are available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 10 | Invalid system names | 1. On the server, create folders `CON`, `COM1` and `test%` and files `PRN` and `foo%`<br>2. Add that account to the desktop client | - MacOS client syncs down `CON`, `COM1` and `PRN` but not `test%` and `foo%`<br>- Windows client syncs down `test%` and `foo%` but not `CON`, `COM1` and `PRN`<br>- Linux client syncs down all | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 11 | Long path on Windows | 1. Make sure LongPathsEnabled in Windows registry is 'off' (see https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd) <br>2. On the server, create a file (~30 chars) inside 6 subfolders (each ~50 chars) to get a path length > 260 chars (> MAX_PATH)<br>3. Start desktop client<br>4. Open local sync folder and enter the subfolders<br>5. Create a new file | 1. All subfolders and the file are available in the local sync folder<br>2. New file is synced to the server | :construction: Win | |
|
||||
|
||||
### 3. Files
|
||||
|
||||
Note: "Via Web" means check files on server in the web browser
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | create a file | 1. Add an account to desktop client<br>2. Open local sync folder<br>3. Create a file | - observe a BLUE progress icon followed by a GREEN icon in the file explorer<br>- the file is available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 2 | Add various types of files | 1. Open local sync folder<br>2. Create/upload various types of files `Microsoft Word`, `Microsoft Excel`, `Microsoft Powerpoint`, `JPG`, `PNG`, `JPEG`, `PDF`, `MP3`, `MP4`, etc | - the Sync files tab shows all the files as synced<br>- all files are available on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 3 | File with long name can be synced | 1. Open local sync folder<br>2. Create a file with long name: `thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIsAVeryLongFileNameToCheckThatItWorks-thisIs.txt` | Sync is successful | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 4 | copy/drag&drops multiple files at a time | 1. Copy some files to the local sync folder<br>2. Drag&Drop some files to the local sync folder | - user can see the completed GREEN icon overlay on all types of files<br>- the Sync files tab shows all files as synced | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 5 | Sync files (from server and desktop client) at the same time | 1. Add a (big) file inside of the local sync folder, and in the same time, upload another file through the webUI in the same folder (make sure that both files are uploaded at the same time) | Both files are synced correctly. Check via Web UI and the Desktop Client | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 6 | Same name files but with different extensions | 1. Open local sync folder<br>2. Create two files with same name but different extensions | Both files sync correctly | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 7 | File with spaces in the name | 1. Open local sync folder<br>2. Create, copy or move a file (having space in its name) in the local sync folder | File syncs correctly | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 8 | Create and delete a file with special characters in the name | 1. Open local sync folder<br>2. Create/upload a file with special characters in its name (e.g. **~`!@#$^&()-_=+{[}];',$%ñ&💥🫨❤️🔥**)<br>3. Wait for sync to complete<br>4. Delete the file from the local sync folder | Look via the Web and make sure that the file got deleted | :construction: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders |
|
||||
| 9 | Upload a 50MB file | 1. Create a folder inside the local sync folder<br>2. Copy a large file (50MB) in this folder | The file is synced to the server | :construction: Win<br>:construction: macOS<br>:robot: Linux | |
|
||||
| 10 | Upload a 3GB file | 1. On the server, upload a large 3GB file<br>2. Add that account to desktop client | File is synced to the desktop client | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 11 | Upload multiple files (with a sum of 1000MB) | 1. Open local sync folder<br>2. Upload a folder containing 1000 files (1MB each) | All files are synced to the server | :construction: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 12 | Upload 500MB + 500MB files | 1. Open temp folder<br>2. Create two folders containing 500 files each(1MB per file)<br>3. Move those folders from temp folder to sync folder<br>4. Wait for folders to be synced | All folders and files are synced to the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 13 | Upload a 1024MB file | 1. Open local sync folder<br>2. Upload a 1GB file | The file is synced to the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 14 | quota limit | 1. Open local sync folder<br>2. Upload the necessary large files to fill up the quota | Warning: "Space quota exceeded. Please contact the Administrator of this space." | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
|
||||
### 4. Move files and folders
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|-------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Move from sub-levels to root | 1. Move couple of files and folders from different sub-levels to the sync root<br>2. Wait for sync to complete<br>3. Check the contents of files and folders | The contents are correct (on the server) | :construction: Win<br>:construction: macOS<br>:robot: Linux | tst_moveFilesFolders |
|
||||
| 2 | Move a folder and file down to sub-folder | 1. Move a folder and file from sync root to a 5 level deep sub-folder<br>2. Wait for sync to complete<br>3. Check the content of file and folder exist | The content of the file is correct and folder exists (on the server) | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_moveFilesFolders |
|
||||
| 3 | Move a folder and file up from sub-folder to the root | 1. Move a folder and file from a 5 level deep folder to the sync root<br>2. Wait for sync to complete<br>3. Check the content of file and folder exist | The content of the file is correct and folder exists (on the server) | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_moveFilesFolders |
|
||||
| 4 | Move files from one folder to another | 1. Move some files from `Folder1` to `Folder2`<br>2. Wait for sync to complete<br>3. Check both folders contents | Both folders have the correct content (on the server) | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_moveFilesFolders |
|
||||
|
||||
### 5. Edit Files
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Edit a .txt file | 1. Create a .txt file in the local sync folder<br>2. Add some text<br>3. Wait for it to sync<br>4. Modify the .txt file and add more content<br>5. Wait for it to sync | The file at the server had the same content after the sync is completed | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_editFiles |
|
||||
| 2 | Edit a .docx file | 1. Create a .docx file in the local sync folder<br>2. Add some text<br>3. Wait for it to sync<br>4. Modify the .docx file and add more content<br>5. Wait for it to sync<br>6. Modify the .docx file and add more content<br>7. Wait for it to sync | The file at the server had the same content after the sync is completed | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_editFiles |
|
||||
| 3 | Edit a .xlsx file | 1. Create a .xlsx file in the local sync folder<br>2. Add some text<br>3. Wait for it to sync<br>4. Modify the .xlsx file and add more content<br>5. Wait for it to sync<br>6. Modify the .xlsx file and add more content<br>7. Wait for it to sync | The file at the server had the same content after the sync is completed | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_editFiles |
|
||||
| 4 | Replace a .pdf file | 1. Create a .pdf file in the local sync folder<br>2. Replace it with a same name pdf file having different content<br>3. Wait for it to sync<br>4. Modify the .pdf file and add more content<br>5. Wait for it to sync<br>6. Modify the .pdf file and add more content<br>7. Wait for it to sync | The file at the server had the same content after the sync is completed | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_editFiles |
|
||||
| 5 | Edit a file while the folder is renamed | 1. Add an account to desktop client that has a folder containing a file in it<br>2. From the file explorer, open that file and edit it<br>3. From the web, rename the folder<br>4. Sync with the oc-worker<br>5. Do not refresh the browser and download the file edited | The file on the server has the same content | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 6 | Rename file and folder at the same time | 1. Add an account to desktop client that has a folder containing a file in it<br>2. From the web, rename that folder and at the same time from the file explorer, rename a file contained in that folder | File and folder should be renamed both in the server and locally | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
|
||||
### 6. Delete Files and Folders
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|--------------------------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| 1 | Delete a file | 1. On the server, create a file<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete that file | The file is deleted on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders |
|
||||
| 2 | Delete a file with long name | 1. On the server, create a file with long name<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete that file | The file is deleted on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders (Maximum length of filename is 228 chraracter) |
|
||||
| 3 | Delete a folder | 1. On the server, create a folder<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete that folder | The folder is deleted on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders |
|
||||
| 4 | Delete a folder with long name | 1. On the server, create a folder with long name<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete that folder | The folder is deleted on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders |
|
||||
| 5 | Delete multiple files | 1. On the server, create some files<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete some files | The files get deleted on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_deleteFilesFolders |
|
||||
| 6 | Delete a large file (2048MB) | 1. On the server, create a large file (2048MB)<br>2. Add that account to the desktop client<br>3. Open the local sync folder<br>4. Delete that file | The file gets deleted on the server | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
|
||||
### 7. Sync process
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Move a file while in sync | 1. Move couple of files (`File1` and `File2`) with different contents to the root sync folder<br>2. Let them sync<br>3. Move other files to the root sync folder and while in sync, delete `File1` and rename `File2` to`File1` | The content of file is correct | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 2 | Edit a file while in sync | 1. Create a .txt file with some content<br>2. Let it sync<br>3. Rename the file and while in sync, edit the content of the .txt file | The content of file is correct | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 3 | Delete folders while in sync | 1. Create folders tree folders+subfolders (e.g folder1, folder2, folder3 and some .txt files in this folder)<br>2. Delete some folders while in sync | Folders get deleted on the server | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 4 | Delete folders while sync with two clients | 1. Create a tree of folders+subfolders (e.g folder1, folder2, folder3 and some .txt files in this folder)<br>2. Delete some folders and while in sync, sync with another client at the same time | On the server, make sure that the folders got deleted and the remaining folders sync correctly | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
|
||||
### 8. Spaces and Sharing
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Viewer can view files in Space | 1. On the server, create a space, upload a file in it and add a user with Viewer role<br>2. As a space member (Viewer), add that space to the desktop client<br>3. Open the local sync folder and open that file | The file can be opened | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 2 | Viewer cannot edit files in Space | 1. On the server, create a space, upload a file in it and add a user with Viewer role<br>2. As a space member (Viewer), add that space to the desktop client<br>3. Open the local sync folder<br>4. Edit that file and save it | Changes are not synced. Locally a conflicted copy is created | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 3 | Editor can rename files in Space | 1. On the server, create a space, upload a file in it and add a user with Editor role<br>2. As a space member (Editor), add that space to the desktop client<br>3. Open the local sync folder<br>4. Rename that file | The files is renamed and synced | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 4 | Manager can add new folders in Space | 1. On the server, create a space, upload a file in it and add a user with Manager role<br>2. As a space member (Manager), add that space to the desktop client<br>3. Open the local sync folder<br>4. Create a new folder | New folder is synced | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 5 | Remove a project space sync connection | 1. On the server, create a space, upload a file in it and add a user with Manager role<br>2. As a space member (Manager), add that space to the desktop client<br>3. Wait till all files and folder are synced<br>4. Remove the space from the desktop client | Space is deleted from the list, all files and folders still exist in the local sync folder | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 6 | sync a received shared folder `can view(secure)` | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can view(secure)` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. create folders locally in the received share<br>6. copy files into the received local folder | Folder is shown, but no files are synced in ether direction, error message shown by the client | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 7 | sync a received shared folder `can view` | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can view` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. create folders locally in the received share<br>6. copy files into the received local folder | Folder and files are downloaded, but no folder/files are uploaded to the server, error message shown by the client | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_spaces |
|
||||
| 8 | sync a received shared folder `can upload` | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can upload` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. create folders locally in the received share<br>6. copy files into the received local folder<br>7. edit a local file | Folder and files are synced in both directions, but editing results in a new conflict file and the change is not uploaded | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 9 | sync a received shared folder `can edit` | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can edit` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. create folders locally in the received share<br>6. copy files into the received local folder<br>7. edit a local file | Folder and files are synced in both directions | :construction: Win<br>:construction: macOS<br>:robot: Linux | tst-spaces |
|
||||
| 10 | remove a share | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can edit` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. wait till the folder is synced locally<br>6. Delete the share on the server | Folder and files disappear from the local sync folder | :construction: Win<br>:construction: macOS<br>:robot: Linux | |
|
||||
| 11 | remove a share while offline | 1. On the server, create a folder<br>2. upload some files into it<br>3. Share the folder with an other user with `can edit` permissions<br>4. As the share receiver, add the "Shares" space to the desktop client<br>5. wait till the folder is synced locally<br>6. Disconnect from the server e.g. by disconnecting from the internet<br>7. Add a new file to the shared folder locally<br>8. Delete the share on the server<br>9. reestablish the connection to the server | Files or folder are deleted locally | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 12 | Viewer creates folder in a project space | 1. On the server, create a project space<br>2. Add an user in that space with `Can view` permission role.<br>3. As a space member (Viewer), add that space to the desktop client<br>4. Open the local sync folder and create a folder | Folder is blacklisted | :construction: Win<br>:construction: macOS<br>:robot: Linux | |
|
||||
|
||||
### 9. Pause and resume sync
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Sync after reestablishing the connection | 1. Add an account to the desktop client<br>2. Disconnect from the server e.g. by disconnecting from the internet<br>3. Copy several files and folders into the sync folder<br>4. reestablish the connection to the server<br>5. Wait for sync | The files and folder are synced to the server | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 2 | Merge folder content after reestablishing the connection | 1. Add an account to the desktop client<br>2. Disconnect from the server e.g. by disconnecting from the internet<br>3. Upload a folder with some files to the server e.g. by the webUI or an other client<br>4. In the local sync folder, create a folder with the same name but different files<br>5. reestablish the connection to the server<br>6. Wait for sync | All files and folder are synced to the server and to the local sync folder | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 3 | Deletion after reestablishing the connection | 1. Add an account to the desktop client<br>2. Disconnect from the server e.g. by disconnecting from the internet<br>3. Delete a folder from the local sync folder<br>4. Connect the Internet<br>5. Wait for sync | The folder is deleted from the server | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 4 | manually pause/resume a sync | 1. Add an account to the desktop client<br>2. Upload big files to the server into a project space & personal space via WebUI<br>3. Copy other big files into the local sync folder of the project space & personal space<br>4. Pause the sync while files are uploaded/downloaded<br>5. Resume the sync. | All files are folders are synced correctly in both directions | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 5 | Force a sync while an Upload to a different space is running | 1. Add an account to the desktop client<br>2. Copy small files into the local sync folder of a project space<br>3. Copy big files into the local sync folder of the personal space<br>4. Wait till the project space sync is finished and personal space sync still runs<br>5. Force a sync of the project space. | All files are folders are synced correctly in both directions | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 6 | Force a sync while Download from a different space is running | 1. Add an account to the desktop client<br>2. Upload small files into a project space using the webUI<br>3. Upload big files into a personal space using the webUI<br>4. Wait till the project space sync is finished and personal space sync still runs<br>5. Force a sync of the project space. | All files are folders are synced correctly in both directions | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 7 | Conflicting file | 1. Add an account to the desktop client<br>2. save a file in the local sync folder<br>3. Disconnect from the server e.g. by disconnecting from the internet<br>4. Edit the same file locally and on the server<br>4. reestablish the connection to the server<br>5. Wait for sync | Conflict file are created locally and on the server | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
|
||||
### 10. Advanced configuration
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|------------------------------------------------------|-------------------------------|
|
||||
| 1 | Advanced configuration | 1. Start the desktop client and fill in the server details<br>2. Check the advanced configuration checkbox | `Download everything` option is selected | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
| 2 | Configure synchronization manually | 1. Start the desktop client and fill in the server details<br>2. Check the advanced configuration checkbox<br>3. choose `Configure synchronization manually`<br>4. Connect the account<br>5. Choose a space and a local folder | The space is synced into the chosen folder | :robot: Win<br>:construction: macOS<br>:robot: Linux | tst_addAccount |
|
||||
| 3 | Configure synchronization manually, a space | 1. Start the desktop client and fill in the server details<br>2. Check the advanced configuration checkbox<br>3. choose `Configure synchronization manually`<br>4. Connect the account<br>5. Choose "Cancel" in the next screen | - No local sync folder is created<br>- The setting window is opened and the account is registered | :robot: Win<br>:construction: macOS<br>: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<br>2. add an account to the desktop client with manual sync configuration<br>3. Choose the personal space to be synced<br>4. choose a local folder<br>5. Select only one folder to be synced and add the connection | Only one folder is synced | :construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
| 2 | unselected subfolders | 1. Upload a folder that has many subfolders to the server<br>2. Connect the desktop client and sync the personal space<br>From the `...` button for the space select "Chose what to sync" window, select the folder that has many subfolders<br>3. Extend that folder and unselect some subfolders<br>3. Click "OK" | The parent folder is synced but not the unselected subfolders | :construction: macOS<br>: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<br>: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<br>2. Upload some files via webUI into that folder<br>3. Copy some other files into the corresponding local folder<br>4. Wait for sync | Files are synced both ways | :construction: macOS<br>:construction: Linux | |
|
||||
| 5 | sync files for unselected folder | 1. From the `Deselect remote folders...` window, unselect a folder and add connection<br>2. From the server, upload some files in that unselected folder | The folder and files are not available in the sync folder<br>Previously synced folders are deleted | :construction: macOS<br>:robot: Linux | tst-syncing |
|
||||
| 6 | sync of files in root folder | 1. From the `Deselect remote folders...` window, unselect all the folders<br>2. Add the connection | files that are in the root folder are synced | :construction: macOS<br>:construction: Linux | |
|
||||
| 7 | sorting of folders | 1. In the `Deselect remote folders...` window, sort the folders by name and size | Sorting works | :construction: macOS<br>:robot: Linux | tst_syncing |
|
||||
|
||||
### 12. Overlay icons
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|--------------------|-------------------------------|
|
||||
| 1 | overlay icons | 1. Launch the Desktop client <br> 2. Open the file explorer | Check qsfera overlay icons are present. | :construction: Win | |
|
||||
| 2 | offline state | 1. Disconnect from the server e.g. by disconnecting from the internet<br>2. Launch the Desktop client<br>3. Check the overlay icons in the local sync folder | The overlay icons are not shown | :construction: Win | |
|
||||
| 3 | paused sync | 1. Launch the Desktop client<br>2. Check the overlay icons<br>3. Pause the sync<br>4. Check the overlay icons | The overlay icons disappear | :construction: Win | |
|
||||
| 4 | everything synced | 1. Open the local sync folder<br>2. Add a folder having multiple nested files and folders<br>3. Check the overlay icons | The green check is shown in all the folders/files | :construction: Win | |
|
||||
| 5 | sync in progress | 1. Open the local sync folder<br>2. Add some files and folders<br>3. Check the overlay icons | The files/folder that are waiting to sync have the blue icons | :construction: Win | |
|
||||
| 6 | warning during sync | 1. Open the local sync folder<br>3. Add a problematic file (path longer than 255 characters)<br>3. Check the overlay icons | The files that are not synced due to a problem have the yellow warning triangle icons | :construction: Win | |
|
||||
| 7 | fatal error during sync | 1. Open the local sync folder<br>3. Create a sync error<br>3. Check the overlay icons | The files/folders that are not synced due to a fatal problem have the red error icons | :construction: Win | |
|
||||
|
||||
### 13. Context menu
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|-------------------------------|
|
||||
| 1 | Sharing | 1. Open the context menu of a synced file <br>2. Click on "Share ..." | A browser window opens and takes the user to the sharing page of the file | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 2 | Private Link | 1. Open the context menu of a synced file <br>2. Click on "Copy private Link to clipboard"<br>3. Paste the link from the clipboard into a browser window | The browser takes the user to the file | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 3 | Show in web browser | 1. Open the context menu of a synced markdown file <br>2. Click on "Show in web browser" | A browser window opens and takes the user to the file, opened in the markdown editor | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
| 4 | file versions | 1. Open the context menu of a synced markdown file <br>2. Click on "Show file versions in web browser" | A browser window opens and takes the user to the file, with the side bar opened, showind the versions | :construction: Win<br>:construction: macOS<br>:construction: Linux | |
|
||||
|
||||
### 14. VFS
|
||||
> [!NOTE]
|
||||
> **“Always keep on this device”** = you’ve pinned the file locally, so it stays on your device at all times and won’t be evicted automatically. <br>
|
||||
> **“Available on this device”** = the file is locally present now, but may be removed automatically to free up space. <br>
|
||||
> **“Hydrated (full) file”** = the file’s full data is present locally (downloaded or created), and you can convert it into a placeholder (dehydrated) by using the ‘Free up space’ action; additionally, the system could later turn it into a placeholder automatically if space is needed. <br>
|
||||
> **“Dehydrated (placeholder) file”** = the file appears locally but only as a lightweight placeholder with minimal footprint; actual data isn’t downloaded until accessed. <br>
|
||||
> The following image demonstrates how the full pinned, full and placeholder file states are shown in File Explorer:
|
||||
> ```
|
||||
> 1. Full Pinned.txt = Always keep on this device
|
||||
> 2. Full.txt = Available on this device (Hydrated)
|
||||
> 3. Placeholder.txt = Placeholder (Dehydrated)
|
||||
>```
|
||||
> 
|
||||
|
||||
| ID | Test Case | Steps to reproduce | Expected Result | Result | Related Comment (Squish-test) |
|
||||
|----|-----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|-------------------------------|
|
||||
| 1 | Add account and connect to server | 1. Create two files with some content inside a folder in the server <br>2. Add an account to the desktop client <br>3. Open local sync folder and check status of all resources (2 files and their parent folder) | All resources (2 files and their parent folder) are virtual (Status: dehydrated) | :construction: Win | |
|
||||
| 2 | Open a file | 1. Create a file with some content in the server <br>2. Add an account to the desktop client <br>3. Open local sync folder and open that file <br>4. Create a new file locally in the local sync folder, edit, save and exit it | Both files are physically available (Status: hydrated) | :construction: Win | |
|
||||
| 3 | Free up space | 1. Create several new files in a new folder inside local sync folder <br>2. Right click on local folder, click on `Free up space` | Folder along with files are dehydrated. Icon changed to cloud | :construction: Win | |
|
||||
| 4 | Always keep | 1. Create a file with content inside a local sync folder <br>2. Right click on that file and click on "Always keep on this device" | File is physically available, same as `Always keep on this device` | :construction: Win | |
|
||||
| 5 | Locked file | 1. Create a file with content inside a local sync folder <br>Open it in LibreOffice(LO)(Opening a resource in LO makes it locked), edit and save but don't exit <br>2. 'Free up space' in Explorer on the locked file → no error message, client shows "i" (Status: spinning icon), Activity tells that file is in use <br>3. Exit libreoffice | File is dehydrated correctly (Status: placeholder) after few seconds | :construction: Win | |
|
||||
| 6 | Move file and directory locally | 1. Create a file and a directory (with some files) inside the local sync root (some hydrated, some dehydrated) <br>2. Move (Cut + Paste) the file and the directory to a different folder inside the sync root | Both hydrated and dehydrated items are moved accordingly on the server (i.e., the same structure appears on the server) | :construction: Win | |
|
||||
| 7 | Move file and directory outside sync root | 1. Create a folder inside the sync root (which may be hydrated or dehydrated) <br>2. Move this folder to a location outside the sync root | The folder is removed on the server as well and no residual placeholder remains locally | :construction: Win | |
|
||||
| 8 | Move file/directory on the server | 1. Within the server (via web UI) move a file and a directory (including some dehydrated files) from one location to another | The local sync root reflects the move: both hydrated and dehydrated files show up in the new location and removed from the old location | :construction: Win | |
|
||||
| 9 | Remove directory while the file is still open locally | 1. Create a directory inside local sync folder; create a file inside and open it in LibreOffice (LO) (file locked) <br>2. On the server, delete/remove the directory <br>3. While LO is still open, edit and save the file <br>4. Close LO | After closing LO, the directory and file are recreated locally (and synced to server) | :construction: Win | |
|
||||
| 10 | File name clash due to same name (but different case) in folder | 1. On the server: create a folder **FolderA**.<br>2. Inside FolderA create two files with identical names (e.g., **NewFile** and **Newfile**) <br>3. On the client: monitor sync and observe warning message: “file name clash…” appears for some seconds.<br>4. In the local sync folder: open FolderA and check status of both files and the folder.<br>5. Move FolderA from the sync-root to another location outside the sync-root | The client first displays the “file name clash” warning and lists the clash in Activity tab under “Not synced”.<br>The folder FolderA and the file(s) show a placeholder/cloud icon status. <br>After moving FolderA outside the sync-root, the file that had previously been synced will be deleted locally and from the server, and the file that had not synced will now be synced and present inside FolderA both on the server and locally. | :construction: Win | |
|
||||
@@ -0,0 +1,22 @@
|
||||
requests==2.32.*; 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; 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"
|
||||
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'
|
||||
sure==2.0.*; python_version >= "3.10"
|
||||
pyautogui==0.9.*; python_version >= "3.10"
|
||||
behave-html-pretty-formatter==1.16.*; python_version >= "3.10"
|
||||
mss==9.*; python_version >= "3.10"
|
||||
imageio-ffmpeg==0.6.*; python_version >= "3.10"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,292 @@
|
||||
import shutil
|
||||
import os
|
||||
from behave import given as Given, when as When, then as Then
|
||||
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 helpers.SetupClientHelper import (
|
||||
start_client,
|
||||
setup_client,
|
||||
substitute_inline_codes,
|
||||
get_client_details,
|
||||
generate_account_config,
|
||||
get_resource_path,
|
||||
)
|
||||
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
|
||||
from helpers.AppHelper import close_and_kill_app
|
||||
|
||||
|
||||
@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('"{username}" account should be added')
|
||||
def step(context, username):
|
||||
username = substitute_inline_codes(username)
|
||||
expect(Toolbar.account_exists(username)).to.be.true
|
||||
|
||||
|
||||
@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')
|
||||
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))
|
||||
Toolbar.wait_toolbar_enabled()
|
||||
|
||||
|
||||
@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()
|
||||
enter_password.accept_certificate()
|
||||
|
||||
for idx, _ in enumerate(sync_paths.values()):
|
||||
# login from last dialog
|
||||
enter_password = EnterPassword()
|
||||
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])
|
||||
Toolbar.wait_toolbar_enabled()
|
||||
|
||||
|
||||
@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_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']))
|
||||
Toolbar.wait_toolbar_enabled()
|
||||
|
||||
|
||||
@Given('the user has entered the following account information:')
|
||||
def step(context):
|
||||
data = table_rows_hash(context.table)
|
||||
account_details = get_client_details(data)
|
||||
AccountConnectionWizard.add_account_information(account_details)
|
||||
|
||||
|
||||
@When('the user "{username}" logs out using the client-UI')
|
||||
def step(context, username):
|
||||
AccountSetting.logout()
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@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 "{username}" 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))
|
||||
Toolbar.wait_toolbar_enabled()
|
||||
|
||||
|
||||
@When('user "|any|" opens login dialog')
|
||||
def step(context, _):
|
||||
AccountSetting.login()
|
||||
|
||||
|
||||
@Then('user "{username}" should be connected to the server')
|
||||
def step(context, username):
|
||||
AccountSetting.wait_until_account_is_connected()
|
||||
|
||||
|
||||
@When('the user removes the connection for user "{username}"')
|
||||
def step(context, username):
|
||||
username = substitute_inline_codes(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')
|
||||
|
||||
|
||||
@When('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_qsfera()
|
||||
close_and_kill_app()
|
||||
|
||||
|
||||
@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(
|
||||
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 "{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))
|
||||
@@ -0,0 +1,458 @@
|
||||
import os
|
||||
import re
|
||||
import builtins
|
||||
import shutil
|
||||
import zipfile
|
||||
from os.path import isfile, join, isdir, exists
|
||||
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,
|
||||
)
|
||||
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 wait_for(
|
||||
lambda: isdir(sanitize_path(folder_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
|
||||
# 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):
|
||||
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)
|
||||
else:
|
||||
shutil.rmtree(resource_path)
|
||||
|
||||
|
||||
@When(
|
||||
'user "{username}" creates a file "{filename}" with the following content inside the sync folder'
|
||||
)
|
||||
def step(context, username, filename):
|
||||
file = get_resource_path(filename, username)
|
||||
wait_and_write_file(convert_path_separators_for_os(file), context.text)
|
||||
|
||||
|
||||
@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 "{username}" has created a folder "{foldername}" inside the sync folder')
|
||||
def step(context, username, foldername):
|
||||
create_folder(foldername, username)
|
||||
|
||||
|
||||
@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(
|
||||
'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)
|
||||
|
||||
|
||||
@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 "{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,
|
||||
):
|
||||
contents.should.equal(expected)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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)
|
||||
with ensure(
|
||||
'{0} "{1}" should not exist, but it does',
|
||||
resource_type.capitalize(),
|
||||
resource,
|
||||
):
|
||||
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)
|
||||
|
||||
|
||||
@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 "{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('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):
|
||||
deleteResource(resource_name, resource_type)
|
||||
|
||||
|
||||
@When('user "|any|" creates the following files inside the sync folder:')
|
||||
def step(context, username):
|
||||
for row in context.table[1:]:
|
||||
file = get_resource_path(row[0], username)
|
||||
wait_and_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(
|
||||
'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 "{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
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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)
|
||||
@@ -0,0 +1,148 @@
|
||||
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'
|
||||
)
|
||||
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(
|
||||
'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 "{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(
|
||||
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')
|
||||
def step(context, user, folder_name):
|
||||
webdav.create_folder(user, folder_name)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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 "{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)
|
||||
|
||||
|
||||
@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'],
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
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
|
||||
from helpers.SpaceHelper import (
|
||||
create_space,
|
||||
create_space_folder,
|
||||
create_space_file,
|
||||
add_user_to_space,
|
||||
get_file_content,
|
||||
resource_exists,
|
||||
)
|
||||
from helpers.ConfigHelper import get_config, set_config
|
||||
|
||||
|
||||
@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 "{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 "{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 "{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 "{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)
|
||||
setup_client(user, space_name)
|
||||
enter_password = EnterPassword()
|
||||
enter_password.accept_certificate()
|
||||
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(
|
||||
'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)
|
||||
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(
|
||||
'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)
|
||||
with ensure('Resource "{0}" should exist but it does not', resource_name):
|
||||
exists.should.be.true
|
||||
@@ -0,0 +1,377 @@
|
||||
from behave import when as When, then as Then
|
||||
from sure import ensure
|
||||
|
||||
from pageObjects.SyncConnectionWizard import SyncConnectionWizard
|
||||
from pageObjects.Toolbar import Toolbar
|
||||
from pageObjects.Activity import Activity
|
||||
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,
|
||||
)
|
||||
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
|
||||
from helpers.TableParser import table_hashes, table_raw
|
||||
|
||||
|
||||
@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('/'), force_sync=True)
|
||||
|
||||
|
||||
@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)
|
||||
Toolbar.wait_toolbar_enabled()
|
||||
|
||||
|
||||
@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 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)
|
||||
|
||||
|
||||
@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 "{tab_name}" 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):
|
||||
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:')
|
||||
def step(context):
|
||||
folders = []
|
||||
for row in context.table:
|
||||
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 "{space_name}" 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 "{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:')
|
||||
def step(context):
|
||||
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):
|
||||
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):
|
||||
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_dialog()
|
||||
|
||||
|
||||
@Then('the about dialog should be opened')
|
||||
def step(context):
|
||||
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')
|
||||
def step(context):
|
||||
SyncConnectionWizard.add_sync_connection()
|
||||
|
||||
|
||||
@When('user unselects all the remote folders')
|
||||
def step(context):
|
||||
SyncConnectionWizard.deselect_all_remote_folders()
|
||||
|
||||
|
||||
@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 removes the folder sync connection')
|
||||
def step(context):
|
||||
SyncConnection.remove_folder_sync_connection()
|
||||
SyncConnection.confirm_folder_sync_connection_removal()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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 "{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):
|
||||
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(
|
||||
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:
|
||||
folders.append(row[0])
|
||||
SyncConnectionWizard.unselect_folders_to_sync(
|
||||
folders, new_sync_connection_wizard=False
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"server": "https://opencloud:9200",
|
||||
"theme": "https://opencloud:9200/themes/opencloud/theme.json",
|
||||
"openIdConnect": {
|
||||
"metadata_url": "https://opencloud:9200/.well-known/openid-configuration",
|
||||
"authority": "https://opencloud:9200",
|
||||
"client_id": "web",
|
||||
"response_type": "code"
|
||||
},
|
||||
"apps": [
|
||||
"files",
|
||||
"text-editor",
|
||||
"preview",
|
||||
"pdf-viewer",
|
||||
"search",
|
||||
"admin-settings",
|
||||
"ocm",
|
||||
"app-store"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
REPORT_PATH="$PUBLIC_BUCKET/desktop/testlogs/$CI_PIPELINE_NUMBER/$MATRIX_NAME/reports"
|
||||
REPORT_URL="$MC_HOST/$REPORT_PATH"
|
||||
|
||||
echo ""
|
||||
echo "--- GUI Test Reports ---"
|
||||
echo "Test Report: $REPORT_URL/report.html"
|
||||
echo "Client Log: $REPORT_URL/qsfera.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
|
||||
echo "Screenshots:"
|
||||
for f in $screenshots; do
|
||||
# remove 's3/' prefix
|
||||
f=${f/s3\//}
|
||||
echo " - $MC_HOST/$f"
|
||||
done
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
touch .woodpecker.env
|
||||
|
||||
PY_REQUIREMENTS_PATH="test/gui/requirements.txt"
|
||||
|
||||
# get playwright version from requirements.txt
|
||||
get_playwright_version() {
|
||||
if [[ ! -f "$PY_REQUIREMENTS_PATH" ]]; then
|
||||
echo "Error: file not found: $PY_REQUIREMENTS_PATH"
|
||||
fi
|
||||
|
||||
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 requirements.txt" >&2
|
||||
exit 78
|
||||
fi
|
||||
|
||||
echo "$playwright_version"
|
||||
}
|
||||
|
||||
# Function to check if the cache exists for the given commit ID
|
||||
check_browsers_cache() {
|
||||
playwright_version=$(get_playwright_version)
|
||||
|
||||
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 "Browsers cache for playwright v$playwright_version not found in cache."
|
||||
ENV="BROWSER_CACHE_FOUND=false\n"
|
||||
else
|
||||
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=$(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 for '$requirements_sha' hash not found in cache."
|
||||
ENV="PYTHON_CACHE_FOUND=false\n"
|
||||
else
|
||||
echo "Python cache for '$requirements_sha' hash found in cache."
|
||||
ENV="PYTHON_CACHE_FOUND=true\n"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$1" == "" ]]; then
|
||||
echo "Usage: $0 [COMMAND]"
|
||||
echo "Commands:"
|
||||
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
|
||||
fi
|
||||
|
||||
$1
|
||||
|
||||
echo -e $ENV >> .woodpecker.env
|
||||
@@ -0,0 +1,3 @@
|
||||
[General]
|
||||
AUT/qsfera = "/woodpecker/desktop/build/bin"
|
||||
AUTPMTimeout = "15000"
|
||||
Reference in New Issue
Block a user