Test(pytest) start aut (#11482)
* test(pytest) The driver methods added. Wrappers for UI elements added. #67 * test(pytest) Squishserver added #68 * test(pytest) Attach/Detach AUT methods added #69 * test(pytest) Main window handler added #70 * test(pytest) Save screenshot on fail added #71 * test(pytest) Wait for squishserver added #71 * test(pytest) Setup Windows #71 * Generate new keys (#11804) * test(pytest) Image comparison methods added #76 * test(pytest) Tesseract methods added #77 * test(pytest) The Methods to search color on image added #80 * test(onboarding) Test on generation new keys added #75 * test(pytest) Handlers for OS Native File dialog added #81 * test(Onboarding) Test on Profile image added #83 * Allure and TestRail integration (#11806) * test(Allure) Steps descriptions added #72 * test(TestRail) Integration #72
This commit is contained in:
parent
20790a4c2a
commit
b09504be36
|
@ -105,3 +105,5 @@ nimbus-build-system.paths
|
|||
# ui-tests
|
||||
/test/ui-pytest/configs/_local.py
|
||||
*.pyc
|
||||
|
||||
test/ui-pytest/squish_server.ini
|
||||
|
|
|
@ -1,153 +1,4 @@
|
|||
# Status desktop ui-tests
|
||||
|
||||
# Setup:
|
||||
Skip any of the steps, if sure that you have the correct version of the required tool.
|
||||
## All Platforms
|
||||
### 1. Install Qt 5.15
|
||||
https://doc.qt.io/qt-6/get-and-install-qt.html
|
||||
### 2. Setup Squish License Server
|
||||
https://hackmd.io/@status-desktop/HkbWpk2e5
|
||||
### 3. Install PyCharm
|
||||
Download and install:
|
||||
https://www.jetbrains.com/pycharm/download/other.html
|
||||
Please, select any build depending on OS, but NOT an Apple Silicon (dmg)
|
||||
|
||||
How to: https://www.jetbrains.com/help/pycharm/installation-guide.html
|
||||
|
||||
## Windows
|
||||
### 4. Install Squish
|
||||
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230301-1424-qt515x-win64-msvc142.exe
|
||||
### 5. Install Python
|
||||
Download and install for all users: https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe
|
||||
### 6. Install Requirements
|
||||
```
|
||||
YOUR_PYTHON_PATH/pip3.exe install -r ./requirements.txt
|
||||
```
|
||||
### 7. Setup Environment Variables
|
||||
Add in system environment variables:
|
||||
```
|
||||
SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
|
||||
PYTHONPATH=%SQUISH_DIR%/lib;%SQUISH_DIR%/lib/python;%PYTHONPATH%
|
||||
```
|
||||
RESTART PC
|
||||
### 8. Verify environment variables
|
||||
```
|
||||
echo %SQUISH_DIR%
|
||||
echo %PYTHONPATH%
|
||||
```
|
||||
### 9. Setup Python for Squish
|
||||
Download 'PythonChanger.py' in %SQUISH_DIR%:
|
||||
https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/PythonChanger.py
|
||||
```
|
||||
YOUR_PYTHON_PATH/python3.10 SQUISH_DIR/PythonChanger.py --revert
|
||||
YOUR_PYTHON_PATH/python3.10 SQUISH_DIR/PythonChanger.py
|
||||
```
|
||||
- Replace "YOUR PYTHON PATH" on to Python3.10 file location path
|
||||
- Replace "SQUISH DIR" on to the Squish root folder path
|
||||
### 10 Test:
|
||||
Executing tests located in 'test_self.py' file
|
||||
```
|
||||
pytest ./tests/test_self.py
|
||||
```
|
||||
Executing test 'test_import_squish' from 'test_self.py' file
|
||||
```
|
||||
pytest ./tests/test_self.py::test_import_squish
|
||||
```
|
||||
Executing all tests with 'import_squish' in test name
|
||||
```
|
||||
pytest -k import_squish
|
||||
```
|
||||
Executing all tests with tag 'self'
|
||||
```
|
||||
pytest -m self
|
||||
```
|
||||
|
||||
## Linux
|
||||
### 4. Install Squish
|
||||
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230222-1555-qt515x-linux64.run
|
||||
### 5. Install Python
|
||||
```bash
|
||||
sudo apt-get install software-properties-common
|
||||
```
|
||||
```bash
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
```
|
||||
```bash
|
||||
sudo apt-get update
|
||||
```
|
||||
```bash
|
||||
sudo apt-get install python3.10
|
||||
```
|
||||
```bash
|
||||
sudo apt install python3-pip
|
||||
```
|
||||
### 6. Install Requirements
|
||||
```bash
|
||||
sudo pip3 install -r ./requirements.txt
|
||||
```
|
||||
### 7. Setup Environment Variables
|
||||
```bash
|
||||
gedit ~/.profile
|
||||
```
|
||||
```
|
||||
export SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
|
||||
export PYTHONPATH=$SQUISH_DIR/lib:$SQUISH_DIR/lib/python:$PYTHONPATH
|
||||
export LD_LIBRARY_PATH=$SQUISH_DIR/lib:$SQUISH_DIR/python3/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
RESTART PC
|
||||
|
||||
## Mac
|
||||
### 4. Install Squish
|
||||
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230328-1608-qt515x-macaarch64.dmg
|
||||
### 5. Install Python
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
```bash
|
||||
brew update --auto-update
|
||||
brew install wget
|
||||
brew install python@3.10
|
||||
```
|
||||
### 6. Install Requirements
|
||||
```bash
|
||||
sudo pip3 install -r ./requirements.txt
|
||||
```
|
||||
### 7. Setup Environment Variables
|
||||
```bash
|
||||
touch ~/.zprofile
|
||||
open ~/.zprofile
|
||||
```
|
||||
```
|
||||
export SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
|
||||
export PYTHONPATH=$SQUISH_DIR/lib:$SQUISH_DIR/lib/python:$PYTHONPATH
|
||||
export LD_LIBRARY_PATH=$SQUISH_DIR/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
RESTART PC
|
||||
|
||||
## Linux or MAC:
|
||||
### 8. Verify environment variables
|
||||
```bash
|
||||
echo $USERNAME
|
||||
echo $PYTHONPATH
|
||||
echo $LD_LIBRARY_PATH
|
||||
```
|
||||
### 9. Setup Python for Squish
|
||||
https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/
|
||||
```bash
|
||||
brew install wget
|
||||
wget -O $SQUISH_DIR/PythonChanger.py https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/PythonChanger.py
|
||||
python3.10 $SQUISH_DIR/PythonChanger.py --revert
|
||||
python3.10 $SQUISH_DIR/PythonChanger.py
|
||||
```
|
||||
### 10 Test:
|
||||
```bash
|
||||
echo "Executing tests located in 'test_self.py' file"
|
||||
pytest ./tests/test_self.py
|
||||
echo "Executing test 'test_import_squish' from 'test_self.py' file"
|
||||
pytest ./tests/test_self.py::test_import_squish
|
||||
echo "Executing all tests with 'import_squish' in test name"
|
||||
pytest -k import_squish
|
||||
echo "Executing all tests with tag 'self'"
|
||||
pytest -m self
|
||||
```
|
||||
For more info, read: https://docs.pytest.org/en/latest/getting-started.html
|
||||
# Status desktop ui-tests
|
||||
|
||||
Setup:
|
||||
https://www.notion.so/Setup-Environment-e5d88399027042a0992e85fd9b0e5167?pvs=4
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import logging
|
||||
|
||||
from . import testpath, timeouts
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from ._local import *
|
||||
except ImportError:
|
||||
exit(
|
||||
'Config file: "_local.py" not found in "./configs".\n'
|
||||
'Please use template "_.local.py.default" to create file or execute command: \n'
|
||||
rf'cp {testpath.ROOT}/configs/_local.py.default {testpath.ROOT}/configs/_local.py'
|
||||
)
|
||||
import logging
|
||||
|
||||
from scripts.utils.system_path import SystemPath
|
||||
from . import testpath, timeouts, testrail, system
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from ._local import *
|
||||
except ImportError:
|
||||
exit(
|
||||
'Config file: "_local.py" not found in "./configs".\n'
|
||||
'Please use template "_.local.py.default" to create file or execute command: \n'
|
||||
rf'cp {testpath.ROOT}/configs/_local.py.default {testpath.ROOT}/configs/_local.py'
|
||||
)
|
||||
|
||||
if APP_DIR is None:
|
||||
exit('Please add "APP_DIR" in ./configs/_local.py')
|
||||
if system.IS_WIN and 'bin' not in APP_DIR:
|
||||
exit('Please use launcher from "bin" folder in "APP_DIR"')
|
||||
APP_DIR = SystemPath(APP_DIR)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import os
|
||||
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
LOCAL_RUN = False
|
||||
|
||||
APP_DIR = SystemPath(os.getenv('APP_DIR')
|
||||
ATTACH_MODE = False
|
||||
APP_DIR = os.getenv('APP_DIR')
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
LOCAL_RUN = True
|
||||
|
||||
ATTACH_MODE = True
|
||||
APP_DIR = None
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import platform
|
||||
|
||||
IS_LIN = True if platform.system() == 'Linux' else False
|
||||
IS_MAC = True if platform.system() == 'Darwin' else False
|
||||
IS_WIN = True if platform.system() == 'Windows' else False
|
||||
|
||||
OS_ID = 'lin' if IS_LIN else 'mac' if IS_MAC else 'win'
|
|
@ -1,15 +1,27 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import typing
|
||||
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
ROOT: SystemPath = SystemPath(__file__).resolve().parent.parent
|
||||
|
||||
# Runtime initialisation
|
||||
TEST: typing.Optional[SystemPath] = None
|
||||
TEST_VP: typing.Optional[SystemPath] = None
|
||||
TEST_ARTIFACTS: typing.Optional[SystemPath] = None
|
||||
|
||||
# Test Directories
|
||||
RUN_ID = os.getenv('RUN_DIR', f'run_{datetime.now():%d%m%Y_%H%M%S}')
|
||||
TEMP: SystemPath = ROOT / 'tmp'
|
||||
RESULTS: SystemPath = TEMP / 'results'
|
||||
RUN: SystemPath = RESULTS / RUN_ID
|
||||
VP: SystemPath = ROOT / 'ext' / 'vp'
|
||||
TEST_FILES: SystemPath = ROOT / 'ext' / 'test_files'
|
||||
|
||||
# Driver Directories
|
||||
SQUISH_DIR = os.getenv('RUN_DIR')
|
||||
SQUISH_DIR = SystemPath(os.getenv('SQUISH_DIR'))
|
||||
|
||||
# Status Application
|
||||
STATUS_DATA: SystemPath = RUN / 'status'
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import os
|
||||
|
||||
TESTRAIL_RUN_ID = os.getenv('TESTRAIL_URL', '').strip()
|
||||
TESTRAIL_URL = os.getenv('TESTRAIL_URL', None)
|
||||
TESTRAIL_USER = os.getenv('TESTRAIL_USER', None)
|
||||
TESTRAIL_PWD = os.getenv('TESTRAIL_PWD', None)
|
|
@ -1,3 +1,6 @@
|
|||
# Timoeuts before raising errors
|
||||
|
||||
UI_LOAD_TIMEOUT_MSEC = 5000
|
||||
UI_LOAD_TIMEOUT_SEC = 5
|
||||
UI_LOAD_TIMEOUT_MSEC = UI_LOAD_TIMEOUT_SEC * 1000
|
||||
PROCESS_TIMEOUT_SEC = 10
|
||||
APP_LOAD_TIMEOUT_MSEC = 60000
|
||||
|
|
|
@ -1,17 +1,63 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import allure
|
||||
import pytest
|
||||
from PIL import ImageGrab
|
||||
|
||||
import configs
|
||||
from driver.aut import AUT
|
||||
from scripts.utils.system_path import SystemPath
|
||||
from tests.fixtures.path import generate_test_info
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
pytest_plugins = [
|
||||
'tests.fixtures.aut',
|
||||
'tests.fixtures.path',
|
||||
'tests.fixtures.squish',
|
||||
'tests.fixtures.testrail',
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def setup_session_scope(
|
||||
run_dir,
|
||||
init_testrail_api,
|
||||
prepare_test_directory,
|
||||
start_squish_server,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_function_scope(
|
||||
generate_test_data,
|
||||
check_result
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
setattr(item, 'rep_' + rep.when, rep)
|
||||
|
||||
|
||||
def pytest_exception_interact(node):
|
||||
"""Handles test on fail."""
|
||||
pass
|
||||
try:
|
||||
test_path, test_name, test_params = generate_test_info(node)
|
||||
node_dir: SystemPath = configs.testpath.RUN / test_path / test_name / test_params
|
||||
node_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
screenshot = node_dir / 'screenshot.png'
|
||||
if screenshot.exists():
|
||||
screenshot = node_dir / f'screenshot_{datetime.now():%H%M%S}.png'
|
||||
ImageGrab.grab().save(screenshot)
|
||||
allure.attach(
|
||||
name='Screenshot on fail',
|
||||
body=screenshot.read_bytes(),
|
||||
attachment_type=allure.attachment_type.PNG)
|
||||
AUT().stop()
|
||||
except Exception as ex:
|
||||
_logger.debug(ex)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
from . import commands
|
||||
from .colors import *
|
||||
from .tesseract import *
|
||||
from .user import *
|
|
@ -0,0 +1,49 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
WHITE = 1
|
||||
BLACK = 2
|
||||
RED = 3
|
||||
BLUE = 4
|
||||
GREEN = 5
|
||||
YELLOW = 6
|
||||
ORANGE = 7
|
||||
|
||||
|
||||
boundaries = {
|
||||
Color.WHITE: [
|
||||
[0, 0, 0],
|
||||
[0, 0, 255]
|
||||
],
|
||||
Color.BLACK: [
|
||||
[0, 0, 0],
|
||||
[179, 100, 130]
|
||||
],
|
||||
Color.RED: [
|
||||
[
|
||||
[0, 100, 20],
|
||||
[10, 255, 255]
|
||||
],
|
||||
[
|
||||
[160, 100, 20],
|
||||
[179, 255, 255]
|
||||
]
|
||||
],
|
||||
Color.BLUE: [
|
||||
[110, 50, 50],
|
||||
[130, 255, 255]
|
||||
],
|
||||
Color.GREEN: [
|
||||
[36, 25, 25],
|
||||
[70, 255, 255]
|
||||
],
|
||||
Color.YELLOW: [
|
||||
[20, 100, 0],
|
||||
[45, 255, 255]
|
||||
],
|
||||
Color.ORANGE: [
|
||||
[10, 100, 20],
|
||||
[25, 255, 255]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import configs.system
|
||||
|
||||
# Buttons
|
||||
BACKSPACE = 'Backspace'
|
||||
COMMAND = 'Command'
|
||||
CTRL = 'Ctrl'
|
||||
ESCAPE = 'Escape'
|
||||
RETURN = 'Return'
|
||||
SHIFT = 'Shift'
|
||||
|
||||
# Combinations
|
||||
SELECT_ALL = f'{CTRL if configs.system.IS_WIN else COMMAND}+A'
|
||||
OPEN_GOTO = f'{COMMAND}+{SHIFT}+G'
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
Tesseract provides various configuration parameters that can be used to customize the OCR process. These parameters are passed as command-line arguments to Tesseract through the --oem and --psm options or through the config parameter in pytesseract. Here are some commonly used Tesseract configuration parameters:
|
||||
|
||||
--oem (OCR Engine Mode): This parameter specifies the OCR engine mode to use. The available options are:
|
||||
|
||||
0: Original Tesseract only.
|
||||
1: Neural nets LSTM only.
|
||||
2: Tesseract + LSTM.
|
||||
3: Default, based on what is available.
|
||||
--psm (Page Segmentation Mode): This parameter defines the page layout analysis mode to use. The available options are:
|
||||
|
||||
0: Orientation and script detection (OSD) only.
|
||||
1: Automatic page segmentation with OSD.
|
||||
2: Automatic page segmentation, but no OSD or OCR.
|
||||
3: Fully automatic page segmentation, but no OSD. (Default)
|
||||
4: Assume a single column of text of variable sizes.
|
||||
5: Assume a single uniform block of vertically aligned text.
|
||||
6: Assume a single uniform block of text.
|
||||
7: Treat the image as a single text line.
|
||||
8: Treat the image as a single word.
|
||||
9: Treat the image as a single word in a circle.
|
||||
10: Treat the image as a single character.
|
||||
--lang (Language): This parameter specifies the language(s) to use for OCR. Multiple languages can be specified separated by plus (+) signs. For example, --lang eng+fra for English and French.
|
||||
|
||||
--tessdata-dir (Tessdata Directory): This parameter sets the path to the directory containing Tesseract's language data files.
|
||||
|
||||
These are just a few examples of the commonly used configuration parameters in Tesseract. There are many more options available for advanced customization and fine-tuning of OCR results. You can refer to the official Tesseract documentation for a comprehensive list of configuration parameters and their descriptions: https://tesseract-ocr.github.io/tessdoc/Command-Line-Usage.html
|
||||
"""
|
||||
|
||||
text_on_profile_image = r'--oem 3 --psm 10'
|
|
@ -0,0 +1,7 @@
|
|||
from collections import namedtuple
|
||||
|
||||
UserAccount = namedtuple('User', ['name', 'password'])
|
||||
user_account = UserAccount('squisher', '*P@ssw0rd*')
|
||||
user_account_one = UserAccount('tester123', 'TesTEr16843/!@00')
|
||||
user_account_two = UserAccount('Athletic', 'TesTEr16843/!@00')
|
||||
user_account_three = UserAccount('Nervous', 'TesTEr16843/!@00')
|
|
@ -1,16 +1,27 @@
|
|||
import squishtest # noqa
|
||||
|
||||
import configs
|
||||
from . import server, context, objects_access, toplevel_window, aut, atomacos, mouse
|
||||
|
||||
imports = {module.__name__: module for module in [
|
||||
# import any modules from driver folder
|
||||
atomacos,
|
||||
aut,
|
||||
context,
|
||||
objects_access,
|
||||
mouse,
|
||||
server,
|
||||
toplevel_window
|
||||
|
||||
]}
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name in imports:
|
||||
return imports[name]
|
||||
return getattr(squishtest, name)
|
||||
try:
|
||||
return getattr(squishtest, name)
|
||||
except AttributeError:
|
||||
raise ImportError(f'Module "driver" has no attribute "{name}"')
|
||||
|
||||
|
||||
squishtest.testSettings.waitForObjectTimeout = configs.timeouts.UI_LOAD_TIMEOUT_MSEC
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import time
|
||||
from copy import deepcopy
|
||||
|
||||
import configs.timeouts
|
||||
|
||||
if configs.system.IS_MAC:
|
||||
import atomacos
|
||||
|
||||
BUNDLE_ID = 'im.Status.NimStatusClient'
|
||||
|
||||
|
||||
# https://pypi.org/project/atomacos/
|
||||
|
||||
|
||||
def attach_atomac(timeout_sec: int = configs.timeouts.UI_LOAD_TIMEOUT_SEC):
|
||||
atomator = atomacos.getAppRefByBundleId(BUNDLE_ID)
|
||||
started_at = time.monotonic()
|
||||
while not hasattr(atomator, 'AXMainWindow'):
|
||||
time.sleep(1)
|
||||
assert time.monotonic() - started_at < timeout_sec, f'Attach error: {BUNDLE_ID}'
|
||||
return atomator
|
||||
|
||||
|
||||
def find_object(object_name: dict):
|
||||
_object_name = deepcopy(object_name)
|
||||
if 'container' in _object_name:
|
||||
parent = find_object(_object_name['container'])
|
||||
del _object_name['container']
|
||||
else:
|
||||
return attach_atomac().windows()[0]
|
||||
|
||||
assert parent is not None, f'Object not found: {object_name["container"]}'
|
||||
_object = parent.findFirst(**_object_name)
|
||||
assert _object is not None, f'Object not found: {_object_name}'
|
||||
return _object
|
||||
|
||||
|
||||
def wait_for_object(object_name: dict, timeout_sec: int = configs.timeouts.UI_LOAD_TIMEOUT_SEC):
|
||||
started_at = time.monotonic()
|
||||
while True:
|
||||
try:
|
||||
return find_object(object_name)
|
||||
except AssertionError as err:
|
||||
if time.monotonic() - started_at > timeout_sec:
|
||||
raise LookupError(f'Object: {object_name} not found. Error: {err}')
|
|
@ -0,0 +1,78 @@
|
|||
import allure
|
||||
import squish
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from configs.system import IS_WIN, IS_LIN
|
||||
from driver import context
|
||||
from driver.server import SquishServer
|
||||
from scripts.utils import system_path, local_system
|
||||
|
||||
|
||||
class AUT:
|
||||
def __init__(
|
||||
self,
|
||||
app_path: system_path.SystemPath = configs.APP_DIR,
|
||||
host: str = '127.0.0.1',
|
||||
port: int = 61500
|
||||
):
|
||||
super(AUT, self).__init__()
|
||||
self.path = app_path
|
||||
self.host = host
|
||||
self.port = int(port)
|
||||
self.ctx = None
|
||||
self.aut_id = self.path.name if IS_LIN else self.path.stem
|
||||
self.process_name = 'Status' if IS_WIN else 'nim_status_client'
|
||||
driver.testSettings.setWrappersForApplication(self.aut_id, ['Qt'])
|
||||
|
||||
def __str__(self):
|
||||
return type(self).__qualname__
|
||||
|
||||
@allure.step('Attach Squish to Test Application')
|
||||
def attach(self, timeout_sec: int = configs.timeouts.PROCESS_TIMEOUT_SEC, attempt: int = 2):
|
||||
if self.ctx is None:
|
||||
self.ctx = context.attach('AUT', timeout_sec)
|
||||
try:
|
||||
squish.setApplicationContext(self.ctx)
|
||||
except TypeError as err:
|
||||
if attempt:
|
||||
return self.attach(timeout_sec, attempt - 1)
|
||||
else:
|
||||
raise err
|
||||
|
||||
@allure.step('Detach Squish and Application')
|
||||
def detach(self):
|
||||
if self.ctx is not None:
|
||||
squish.currentApplicationContext().detach()
|
||||
assert squish.waitFor(lambda: not self.ctx.isRunning, configs.timeouts.PROCESS_TIMEOUT_SEC)
|
||||
self.ctx = None
|
||||
return self
|
||||
|
||||
@allure.step('Close application by process name')
|
||||
def stop(self, verify: bool = True):
|
||||
local_system.kill_process_by_name(self.process_name, verify=verify)
|
||||
|
||||
@allure.step("Start application")
|
||||
def launch(self, *args) -> 'AUT':
|
||||
SquishServer().set_aut_timeout()
|
||||
if configs.ATTACH_MODE:
|
||||
SquishServer().add_attachable_aut('AUT', self.port)
|
||||
command = [
|
||||
configs.testpath.SQUISH_DIR / 'bin' / 'startaut',
|
||||
f'--port={self.port}',
|
||||
f'"{self.path}"'
|
||||
] + list(args)
|
||||
local_system.execute(command)
|
||||
try:
|
||||
local_system.wait_for_started(self.process_name)
|
||||
except AssertionError:
|
||||
local_system.execute(command, check=True)
|
||||
else:
|
||||
SquishServer().add_executable_aut(self.aut_id, self.path.parent)
|
||||
command = [self.aut_id] + list(args)
|
||||
self.ctx = squish.startApplication(
|
||||
' '.join(command), configs.timeouts.PROCESS_TIMEOUT_SEC)
|
||||
|
||||
self.attach()
|
||||
assert squish.waitFor(lambda: self.ctx.isRunning, configs.timeouts.PROCESS_TIMEOUT_SEC)
|
||||
return self
|
|
@ -0,0 +1,31 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import allure
|
||||
import squish
|
||||
|
||||
import configs
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@allure.step('Attaching to "{0}"')
|
||||
def attach(aut_name: str, timeout_sec: int = configs.timeouts.PROCESS_TIMEOUT_SEC):
|
||||
started_at = time.monotonic()
|
||||
while True:
|
||||
try:
|
||||
context = squish.attachToApplication(aut_name)
|
||||
_logger.info(f'AUT: {aut_name} attached')
|
||||
return context
|
||||
except RuntimeError as err:
|
||||
_logger.debug(err)
|
||||
time.sleep(1)
|
||||
assert time.monotonic() - started_at < timeout_sec, f'Attach error: {aut_name}'
|
||||
|
||||
|
||||
@allure.step('Detaching')
|
||||
def detach():
|
||||
for ctx in squish.applicationContextList():
|
||||
ctx.detach()
|
||||
assert squish.waitFor(lambda: not ctx.isRunning, configs.timeouts.APP_LOAD_TIMEOUT_MSEC)
|
||||
_logger.info(f'All AUTs detached')
|
|
@ -0,0 +1,44 @@
|
|||
import time
|
||||
|
||||
import squish
|
||||
|
||||
|
||||
def move(obj: object, x: int, y: int, dx: int, dy: int, step: int, sleep: float = 0):
|
||||
while True:
|
||||
if x > dx:
|
||||
x -= step
|
||||
if x < x:
|
||||
x = dx
|
||||
elif x < dx:
|
||||
x += step
|
||||
if x > dx:
|
||||
x = dx
|
||||
if y > dy:
|
||||
y -= step
|
||||
if y < dy:
|
||||
y = dy
|
||||
elif y < dy:
|
||||
y += step
|
||||
if y > dy:
|
||||
y = dy
|
||||
squish.mouseMove(obj, x, y)
|
||||
time.sleep(sleep)
|
||||
if x == dx and y == dy:
|
||||
break
|
||||
|
||||
|
||||
def press_and_move(
|
||||
obj,
|
||||
x: int,
|
||||
y: int,
|
||||
dx: int,
|
||||
dy: int,
|
||||
mouse: int = squish.MouseButton.LeftButton,
|
||||
step: int = 1,
|
||||
sleep: float = 0
|
||||
):
|
||||
squish.mouseMove(obj, x, y)
|
||||
squish.mousePress(obj, x, y, mouse)
|
||||
move(obj, x, y, dx, dy, step, sleep)
|
||||
squish.mouseRelease(mouse)
|
||||
time.sleep(1)
|
|
@ -0,0 +1,12 @@
|
|||
import logging
|
||||
|
||||
import object
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def walk_children(parent, depth: int = 1000):
|
||||
for child in object.children(parent):
|
||||
yield child
|
||||
if depth:
|
||||
yield from walk_children(child, depth - 1)
|
|
@ -0,0 +1,51 @@
|
|||
import typing
|
||||
|
||||
import configs.testpath
|
||||
from scripts.utils import local_system
|
||||
|
||||
_PROCESS_NAME = '_squishserver'
|
||||
|
||||
|
||||
class SquishServer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = '127.0.0.1',
|
||||
port: int = 4322
|
||||
):
|
||||
self.path = configs.testpath.SQUISH_DIR / 'bin' / 'squishserver'
|
||||
self.config = configs.testpath.ROOT / 'squish_server.ini'
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def start(self):
|
||||
cmd = [
|
||||
f'"{self.path}"',
|
||||
'--configfile', str(self.config),
|
||||
'--verbose',
|
||||
f'--host={self.host}',
|
||||
f'--port={self.port}',
|
||||
]
|
||||
local_system.execute(cmd)
|
||||
try:
|
||||
local_system.wait_for_started(_PROCESS_NAME)
|
||||
except AssertionError:
|
||||
local_system.execute(cmd, check=True)
|
||||
|
||||
@classmethod
|
||||
def stop(cls, attempt: int = 2):
|
||||
local_system.kill_process_by_name(_PROCESS_NAME, verify=False)
|
||||
|
||||
# https://doc-snapshots.qt.io/squish/cli-squishserver.html
|
||||
def configuring(self, action: str, options: typing.Union[int, str, list]):
|
||||
local_system.run(
|
||||
[f'"{self.path}"', '--configfile', str(self.config), '--config', action, ' '.join(options)])
|
||||
|
||||
def add_executable_aut(self, aut_id, app_dir):
|
||||
self.configuring('addAUT', [aut_id, f'"{app_dir}"'])
|
||||
|
||||
def add_attachable_aut(self, aut_id: str, port: int):
|
||||
self.configuring('addAttachableAUT', [aut_id, f'localhost:{port}'])
|
||||
|
||||
def set_aut_timeout(self, value: int = configs.timeouts.PROCESS_TIMEOUT_SEC):
|
||||
self.configuring('setAUTTimeout', [str(value)])
|
|
@ -0,0 +1,52 @@
|
|||
import squish
|
||||
import toplevelwindow
|
||||
|
||||
import configs
|
||||
|
||||
|
||||
def maximize(object_name):
|
||||
def _maximize() -> bool:
|
||||
try:
|
||||
toplevelwindow.ToplevelWindow(squish.waitForObject(object_name)).maximize()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return squish.waitFor(lambda: _maximize(), configs.timeouts.UI_LOAD_TIMEOUT_MSEC)
|
||||
|
||||
|
||||
def minimize(object_name):
|
||||
def _minimize() -> bool:
|
||||
try:
|
||||
toplevelwindow.ToplevelWindow(squish.waitForObject(object_name)).minimize()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return squish.waitFor(lambda: _minimize(), configs.timeouts.UI_LOAD_TIMEOUT_MSEC)
|
||||
|
||||
|
||||
def set_focus(object_name):
|
||||
def _set_focus() -> bool:
|
||||
try:
|
||||
toplevelwindow.ToplevelWindow(squish.waitForObject(object_name)).setFocus()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return squish.waitFor(lambda: _set_focus(), configs.timeouts.UI_LOAD_TIMEOUT_MSEC)
|
||||
|
||||
|
||||
def on_top_level(object_name):
|
||||
def _on_top() -> bool:
|
||||
try:
|
||||
toplevelwindow.ToplevelWindow(squish.waitForObject(object_name)).setForeground()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return squish.waitFor(lambda: _on_top(), configs.timeouts.UI_LOAD_TIMEOUT_MSEC)
|
||||
|
||||
|
||||
def close(object_name):
|
||||
squish.sendEvent("QCloseEvent", squish.waitForObject(object_name))
|
Binary file not shown.
After Width: | Height: | Size: 759 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,15 @@
|
|||
import allure
|
||||
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class BasePopup(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super(BasePopup, self).__init__('statusDesktop_mainWindow_overlay')
|
||||
|
||||
@allure.step('Close')
|
||||
def close(self):
|
||||
driver.nativeType('<Escape>')
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,26 @@
|
|||
import allure
|
||||
|
||||
from gui.components.base_popup import BasePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.check_box import CheckBox
|
||||
|
||||
|
||||
class BeforeStartedPopUp(BasePopup):
|
||||
|
||||
def __init__(self):
|
||||
super(BeforeStartedPopUp, self).__init__()
|
||||
self._acknowledge_checkbox = CheckBox('acknowledge_checkbox')
|
||||
self._terms_of_use_checkBox = CheckBox('termsOfUseCheckBox_StatusCheckBox')
|
||||
self._get_started_button = Button('getStartedStatusButton_StatusButton')
|
||||
|
||||
@property
|
||||
@allure.step('Get visible attribute')
|
||||
def is_visible(self) -> bool:
|
||||
return self._get_started_button.is_visible
|
||||
|
||||
@allure.step('Allow all and get started')
|
||||
def get_started(self):
|
||||
self._acknowledge_checkbox.set(True)
|
||||
self._terms_of_use_checkBox.set(True, x=10)
|
||||
self._get_started_button.click()
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,22 @@
|
|||
import allure
|
||||
|
||||
import constants.commands
|
||||
import driver
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.text_edit import TextEdit
|
||||
from gui.elements.qt.window import Window
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
|
||||
class OpenFileDialog(Window):
|
||||
|
||||
def __init__(self):
|
||||
super(OpenFileDialog, self).__init__('please_choose_an_image_QQuickWindow')
|
||||
self._path_text_edit = TextEdit('titleBar_textInput_TextInputWithHandles')
|
||||
self._open_button = Button('please_choose_an_image_Open_Button')
|
||||
|
||||
@allure.step('Open file')
|
||||
def open_file(self, fp: SystemPath):
|
||||
self._path_text_edit.text = str(fp)
|
||||
driver.type(self._path_text_edit.object, f'<{constants.commands.RETURN}>')
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,59 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import allure
|
||||
|
||||
import constants
|
||||
import driver
|
||||
from gui.elements.os.mac.button import Button
|
||||
from gui.elements.os.mac.object import NativeObject
|
||||
from gui.elements.os.mac.text_edit import TextEdit
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenFileDialog(NativeObject):
|
||||
|
||||
def __init__(self):
|
||||
super(OpenFileDialog, self).__init__('openFileDialog')
|
||||
self._open_button = Button('openButton')
|
||||
|
||||
def _open_go_to_dialog(self, attempt: int = 2):
|
||||
# Set focus
|
||||
driver.nativeMouseClick(int(self.bounds.x + 10), int(self.bounds.y + 10), driver.Qt.LeftButton)
|
||||
time.sleep(1)
|
||||
driver.nativeType(f'<{constants.commands.OPEN_GOTO}>')
|
||||
try:
|
||||
return _GoToDialog().wait_until_appears()
|
||||
except LookupError as err:
|
||||
_logger.debug(err)
|
||||
if attempt:
|
||||
self._open_go_to_dialog(attempt - 1)
|
||||
else:
|
||||
raise err
|
||||
|
||||
@allure.step('Open file')
|
||||
def open_file(self, fp: SystemPath):
|
||||
# Set focus
|
||||
driver.nativeMouseClick(int(self.bounds.x + 10), int(self.bounds.y + 10), driver.Qt.LeftButton)
|
||||
time.sleep(1)
|
||||
driver.nativeType(f'<{constants.commands.OPEN_GOTO}>')
|
||||
self._open_go_to_dialog().select_file(fp)
|
||||
self._open_button.click()
|
||||
self.wait_until_hidden()
|
||||
|
||||
|
||||
class _GoToDialog(NativeObject):
|
||||
|
||||
def __init__(self):
|
||||
self.go_to_text_edit = TextEdit('pathTextField')
|
||||
super(_GoToDialog, self).__init__('goToDialog')
|
||||
|
||||
@allure.step('Select file')
|
||||
def select_file(self, fp: SystemPath):
|
||||
self.go_to_text_edit.text = str(fp)
|
||||
driver.nativeMouseClick(int(self.bounds.x + 10), int(self.bounds.y + 10), driver.Qt.LeftButton)
|
||||
time.sleep(1)
|
||||
driver.nativeType(f'<{constants.commands.RETURN}>')
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,12 @@
|
|||
import configs
|
||||
|
||||
if configs.system.IS_WIN:
|
||||
from .win.open_file_dialogs import OpenFileDialog as BaseOpenFileDialog
|
||||
elif configs.system.IS_MAC:
|
||||
from .mac.open_file_dialogs import OpenFileDialog as BaseOpenFileDialog
|
||||
else:
|
||||
from .lin.open_file_dialog import OpenFileDialog as BaseOpenFileDialog
|
||||
|
||||
|
||||
class OpenFileDialog(BaseOpenFileDialog):
|
||||
pass
|
|
@ -0,0 +1,24 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
from gui.elements.os.win.button import Button
|
||||
from gui.elements.os.win.object import NativeObject
|
||||
from gui.elements.os.win.text_edit import TextEdit
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenFileDialog(NativeObject):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('file_Dialog')
|
||||
self._file_path_text_edit = TextEdit('choose_file_Edit')
|
||||
self._select_button = Button('choose_Open_Button')
|
||||
|
||||
@allure.step('Open file')
|
||||
def open_file(self, fp: SystemPath):
|
||||
self._file_path_text_edit.text = str(fp)
|
||||
self._select_button.click()
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,52 @@
|
|||
import time
|
||||
from collections import namedtuple
|
||||
|
||||
import allure
|
||||
|
||||
import driver.mouse
|
||||
from gui.components.base_popup import BasePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.object import QObject
|
||||
from gui.elements.qt.slider import Slider
|
||||
|
||||
shift_image = namedtuple('Shift', ['left', 'right', 'top', 'bottom'])
|
||||
|
||||
|
||||
class ProfilePicturePopup(BasePopup):
|
||||
|
||||
def __init__(self):
|
||||
super(ProfilePicturePopup, self).__init__()
|
||||
self._zoom_slider = Slider('o_StatusSlider')
|
||||
self._view = QObject('cropSpaceItem_Item')
|
||||
self._make_profile_picture_button = Button('make_this_my_profile_picture_StatusButton')
|
||||
self._slider_handler = QObject('o_DropShadow')
|
||||
|
||||
@allure.step('Make profile image')
|
||||
def make_profile_picture(
|
||||
self,
|
||||
zoom: int = None,
|
||||
shift: shift_image = None
|
||||
):
|
||||
if zoom is not None:
|
||||
self._zoom_slider.value = zoom
|
||||
# The slider changed value, but image updates only after click on slider
|
||||
self._slider_handler.click()
|
||||
time.sleep(1)
|
||||
if shift is not None:
|
||||
if shift.left:
|
||||
driver.mouse.press_and_move(self._view.object, 1, 1, shift.left, 1)
|
||||
time.sleep(1)
|
||||
if shift.right:
|
||||
driver.mouse.press_and_move(
|
||||
self._view.object, self._view.width, 1, self._view.width - shift.right, 1)
|
||||
time.sleep(1)
|
||||
if shift.top:
|
||||
driver.mouse.press_and_move(self._view.object, 1, 1, 1, shift.top, step=1)
|
||||
time.sleep(1)
|
||||
if shift.bottom:
|
||||
driver.mouse.press_and_move(
|
||||
self._view.object, 1, self._view.height, 1, self._view.height - shift.bottom, step=1)
|
||||
time.sleep(1)
|
||||
|
||||
self._make_profile_picture_button.click()
|
||||
self.wait_until_hidden()
|
|
@ -0,0 +1,62 @@
|
|||
import allure
|
||||
|
||||
import constants
|
||||
import driver
|
||||
from gui.components.base_popup import BasePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.object import QObject
|
||||
from gui.elements.qt.text_label import TextLabel
|
||||
from scripts.tools.image import Image
|
||||
|
||||
|
||||
class ProfilePopup(BasePopup):
|
||||
|
||||
def __init__(self):
|
||||
super(ProfilePopup, self).__init__()
|
||||
self._profile_image = QObject('ProfileHeader_userImage')
|
||||
self._user_name_label = TextLabel('ProfilePopup_displayName')
|
||||
self._edit_profile_button = Button('ProfilePopup_editButton')
|
||||
self._chat_key_text_label = TextLabel('https_status_app_StatusBaseText')
|
||||
self._emoji_hash = QObject('profileDialog_userEmojiHash_EmojiHash')
|
||||
|
||||
@property
|
||||
@allure.step('Get profile image')
|
||||
def profile_image(self):
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get image without identicon_ring')
|
||||
def cropped_profile_image(self):
|
||||
# Profile image without identicon_ring
|
||||
self._profile_image.image.update_view()
|
||||
self._profile_image.image.crop(
|
||||
driver.UiTypes.ScreenRectangle(
|
||||
15, 15, self._profile_image.image.width-30, self._profile_image.image.height-30
|
||||
))
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get user name')
|
||||
def user_name(self) -> str:
|
||||
return self._user_name_label.text
|
||||
|
||||
@property
|
||||
@allure.step('Get chat key')
|
||||
def chat_key(self) -> str:
|
||||
chat_key = self._chat_key_text_label.text.split('https://status.app/u/')[1].strip()
|
||||
if '#' in chat_key:
|
||||
chat_key = chat_key.split('#')[1]
|
||||
return chat_key
|
||||
|
||||
@property
|
||||
@allure.step('Get emoji hash image')
|
||||
def emoji_hash(self) -> Image:
|
||||
return self._emoji_hash.image
|
||||
|
||||
@allure.step('Verify: user image contains text')
|
||||
def is_user_image_contains(self, text: str):
|
||||
# To remove all artifacts, the image cropped.
|
||||
crop = driver.UiTypes.ScreenRectangle(
|
||||
15, 15, self._profile_image.image.width - 30, self._profile_image.image.height - 30
|
||||
)
|
||||
return self.profile_image.has_text(text, constants.tesseract.text_on_profile_image, crop=crop)
|
|
@ -0,0 +1,20 @@
|
|||
import allure
|
||||
|
||||
import configs
|
||||
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class SplashScreen(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super(SplashScreen, self).__init__('splashScreen')
|
||||
|
||||
@allure.step('Wait until appears {0}')
|
||||
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
|
||||
assert self.wait_for(lambda: self.exists, timeout_msec), f'Object {self} is not visible'
|
||||
return self
|
||||
|
||||
@allure.step('Wait until hidden {0}')
|
||||
def wait_until_hidden(self, timeout_msec: int = configs.timeouts.APP_LOAD_TIMEOUT_MSEC):
|
||||
super().wait_until_hidden(timeout_msec)
|
|
@ -0,0 +1,67 @@
|
|||
import time
|
||||
|
||||
import allure
|
||||
|
||||
import configs
|
||||
import constants
|
||||
import driver
|
||||
from gui.components.profile_popup import ProfilePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.object import QObject
|
||||
from gui.elements.qt.text_label import TextLabel
|
||||
|
||||
|
||||
class UserCanvas(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super(UserCanvas, self).__init__('o_StatusListView')
|
||||
self._always_active_button = Button('userContextmenu_AlwaysActiveButton')
|
||||
self._inactive_button = Button('userContextmenu_InActiveButton')
|
||||
self._automatic_button = Button('userContextmenu_AutomaticButton')
|
||||
self._view_my_profile_button = Button('userContextMenu_ViewMyProfileAction')
|
||||
self._user_name_text_label = TextLabel('userLabel_StyledText')
|
||||
self._profile_image = QObject('o_StatusIdenticonRing')
|
||||
|
||||
@property
|
||||
@allure.step('Get profile image')
|
||||
def profile_image(self):
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get user name')
|
||||
def user_name(self) -> str:
|
||||
return self._user_name_text_label.text
|
||||
|
||||
@allure.step('Wait until appears {0}')
|
||||
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
|
||||
super(UserCanvas, self).wait_until_appears(timeout_msec)
|
||||
time.sleep(1)
|
||||
return self
|
||||
|
||||
@allure.step('Set user state online')
|
||||
def set_user_state_online(self):
|
||||
self._always_active_button.click()
|
||||
self.wait_until_hidden()
|
||||
|
||||
@allure.step('Set user state offline')
|
||||
def set_user_state_offline(self):
|
||||
self._inactive_button.click()
|
||||
self.wait_until_hidden()
|
||||
|
||||
@allure.step('Set user automatic state')
|
||||
def set_user_automatic_state(self):
|
||||
self._automatic_button.click()
|
||||
self.wait_until_hidden()
|
||||
|
||||
@allure.step('Open Profile popup')
|
||||
def open_profile_popup(self) -> ProfilePopup:
|
||||
self._view_my_profile_button.click()
|
||||
return ProfilePopup().wait_until_appears()
|
||||
|
||||
@allure.step('Verify: User image contains text')
|
||||
def is_user_image_contains(self, text: str):
|
||||
# To remove all artifacts, the image cropped.
|
||||
crop = driver.UiTypes.ScreenRectangle(
|
||||
5, 5, self._profile_image.image.width-10, self._profile_image.image.height-10
|
||||
)
|
||||
return self._profile_image.image.has_text(text, constants.tesseract.text_on_profile_image, crop=crop)
|
|
@ -0,0 +1,21 @@
|
|||
import allure
|
||||
|
||||
from gui.components.base_popup import BasePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.check_box import CheckBox
|
||||
|
||||
|
||||
class WelcomeStatusPopup(BasePopup):
|
||||
|
||||
def __init__(self):
|
||||
self._agree_to_use_checkbox = CheckBox('agreeToUse_StatusCheckBox')
|
||||
self._ready_to_use_checkbox = CheckBox('readyToUse_StatusCheckBox')
|
||||
self._ready_to_use_button = Button('i_m_ready_to_use_Status_Desktop_Beta_StatusButton')
|
||||
super(WelcomeStatusPopup, self).__init__()
|
||||
|
||||
@allure.step('Confirm all')
|
||||
def confirm(self):
|
||||
self._agree_to_use_checkbox.set(True)
|
||||
self._ready_to_use_checkbox.set(True)
|
||||
self._ready_to_use_button.click()
|
||||
self.wait_until_hidden()
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from gui import objects_map
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseObject:
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.symbolic_name = name
|
||||
self.real_name = getattr(objects_map, name)
|
||||
|
||||
def __str__(self):
|
||||
return f'{type(self).__qualname__}({self.symbolic_name})'
|
||||
|
||||
def __repr__(self):
|
||||
return f'{type(self).__qualname__}({self.symbolic_name})'
|
||||
|
||||
@property
|
||||
def object(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@allure.step('Wait until appears {0}')
|
||||
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
|
||||
assert driver.waitFor(lambda: self.is_visible, timeout_msec), f'Object {self} is not visible'
|
||||
return self
|
||||
|
||||
@allure.step('Wait until hidden {0}')
|
||||
def wait_until_hidden(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
|
||||
assert driver.waitFor(lambda: not self.is_visible, timeout_msec), f'Object {self} is not hidden'
|
||||
|
||||
@classmethod
|
||||
def wait_for(cls, condition, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC) -> bool:
|
||||
return driver.waitFor(lambda: condition, timeout_msec)
|
|
@ -0,0 +1,10 @@
|
|||
import allure
|
||||
|
||||
from .object import NativeObject
|
||||
|
||||
|
||||
class Button(NativeObject):
|
||||
|
||||
@allure.step('Click {0}')
|
||||
def click(self):
|
||||
self.object.Press()
|
|
@ -0,0 +1,48 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
import driver
|
||||
from gui.elements.base_object import BaseObject
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NativeObject(BaseObject):
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
|
||||
@property
|
||||
@allure.step('Get object {0}')
|
||||
def object(self):
|
||||
return driver.atomacos.wait_for_object(self.real_name)
|
||||
|
||||
@property
|
||||
@allure.step('Get visible {0}')
|
||||
def is_visible(self):
|
||||
try:
|
||||
return self.object is not None
|
||||
except LookupError as err:
|
||||
_logger.debug(err)
|
||||
return False
|
||||
|
||||
@property
|
||||
@allure.step('Get bounds {0}')
|
||||
def bounds(self):
|
||||
return self.object.AXFrame
|
||||
|
||||
@property
|
||||
@allure.step('Get width {0}')
|
||||
def width(self) -> int:
|
||||
return int(self.object.AXSize.width)
|
||||
|
||||
@property
|
||||
@allure.step('Get height {0}')
|
||||
def height(self) -> int:
|
||||
return int(self.object.AXSize.height)
|
||||
|
||||
@property
|
||||
@allure.step('Get central coordinate {0}')
|
||||
def center(self):
|
||||
return self.bounds.center()
|
|
@ -0,0 +1,14 @@
|
|||
import driver
|
||||
from .object import NativeObject
|
||||
|
||||
|
||||
class TextEdit(NativeObject):
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return str(self.object.AXValue)
|
||||
|
||||
@text.setter
|
||||
def text(self, value: str):
|
||||
self.object.setString('AXValue', value)
|
||||
driver.waitFor(lambda: self.text == value)
|
|
@ -0,0 +1,10 @@
|
|||
import allure
|
||||
|
||||
from .object import NativeObject
|
||||
|
||||
|
||||
class Button(NativeObject):
|
||||
|
||||
@allure.step('Click {0}')
|
||||
def click(self):
|
||||
super().click()
|
|
@ -0,0 +1,42 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
import driver
|
||||
from gui.elements.base_object import BaseObject
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NativeObject(BaseObject):
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
|
||||
@property
|
||||
@allure.step('Get object {0}')
|
||||
def object(self):
|
||||
return driver.waitForObject(self.real_name)
|
||||
|
||||
@property
|
||||
@allure.step('Get visible {0}')
|
||||
def is_visible(self):
|
||||
try:
|
||||
driver.waitForObject(self.real_name, 1)
|
||||
return True
|
||||
except (AttributeError, LookupError, RuntimeError):
|
||||
return False
|
||||
|
||||
@property
|
||||
@allure.step('Get bounds {0}')
|
||||
def bounds(self):
|
||||
return driver.object.globalBounds(self.object)
|
||||
|
||||
@property
|
||||
@allure.step('Get central coordinate {0}')
|
||||
def center(self):
|
||||
return self.bounds.center()
|
||||
|
||||
@allure.step('Click {0}')
|
||||
def click(self):
|
||||
driver.mouseClick(self.object)
|
|
@ -0,0 +1,32 @@
|
|||
import allure
|
||||
|
||||
import configs
|
||||
import constants
|
||||
import driver
|
||||
from .object import NativeObject
|
||||
|
||||
|
||||
class TextEdit(NativeObject):
|
||||
|
||||
@property
|
||||
@allure.step('Get current text {0}')
|
||||
def text(self) -> str:
|
||||
return str(self.object.text)
|
||||
|
||||
@text.setter
|
||||
@allure.step('Type: {1} {0}')
|
||||
def text(self, value: str):
|
||||
self.clear()
|
||||
driver.nativeType(value)
|
||||
assert driver.waitFor(lambda: self.text == value, configs.timeouts.UI_LOAD_TIMEOUT_MSEC), \
|
||||
f'Type text failed, value in field: "{self.text}", expected: {value}'
|
||||
|
||||
@allure.step('Clear {0}')
|
||||
def clear(self):
|
||||
# Set focus
|
||||
driver.nativeMouseClick(int(self.center.x), int(self.center.y), driver.Qt.LeftButton)
|
||||
driver.type(self.object, f'<{constants.commands.SELECT_ALL}>')
|
||||
driver.type(self.object, f'<{constants.commands.BACKSPACE}>')
|
||||
assert driver.waitFor(lambda: not self.text), \
|
||||
f'Clear text field failed, value in field: "{self.text}"'
|
||||
return self
|
|
@ -0,0 +1,21 @@
|
|||
import typing
|
||||
|
||||
import allure
|
||||
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class Button(QObject):
|
||||
|
||||
@allure.step('Click {0}')
|
||||
def click(
|
||||
self,
|
||||
x: typing.Union[int, driver.UiTypes.ScreenPoint] = None,
|
||||
y: typing.Union[int, driver.UiTypes.ScreenPoint] = None,
|
||||
button: driver.MouseButton = None
|
||||
):
|
||||
if None not in (x, y, button):
|
||||
getattr(self.object, 'clicked')()
|
||||
else:
|
||||
super(Button, self).click(x, y, button)
|
|
@ -0,0 +1,15 @@
|
|||
import allure
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class CheckBox(QObject):
|
||||
|
||||
@allure.step("Set {0} value: {1}")
|
||||
def set(self, value: bool, x: int = None, y: int = None):
|
||||
if self.is_checked is not value:
|
||||
self.click(x, y)
|
||||
assert driver.waitFor(
|
||||
lambda: self.is_checked is value, configs.timeouts.UI_LOAD_TIMEOUT_MSEC), 'Value not changed'
|
|
@ -0,0 +1,43 @@
|
|||
import time
|
||||
import typing
|
||||
|
||||
import allure
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class List(QObject):
|
||||
|
||||
@property
|
||||
@allure.step('Get list items {0}')
|
||||
def items(self):
|
||||
return [self.object.itemAtIndex(index) for index in range(self.object.count)]
|
||||
|
||||
@allure.step('Get values of list items {0}')
|
||||
def get_values(self, attr_name: str) -> typing.List[str]:
|
||||
values = []
|
||||
for index in range(self.object.count):
|
||||
value = str(getattr(self.object.itemAtIndex(index), attr_name, ''))
|
||||
if value:
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
@allure.step('Select item {1} in {0}')
|
||||
def select(self, value: str, attr_name: str):
|
||||
driver.mouseClick(self.wait_for_item(value, attr_name))
|
||||
|
||||
@allure.step('Wait for item {1} in {0} with attribute {2}')
|
||||
def wait_for_item(self, value: str, attr_name: str, timeout_sec: int = configs.timeouts.UI_LOAD_TIMEOUT_SEC):
|
||||
started_at = time.monotonic()
|
||||
values = []
|
||||
while True:
|
||||
for index in range(self.object.count):
|
||||
cur_value = str(getattr(self.object.itemAtIndex(index), attr_name, ''))
|
||||
if cur_value == value:
|
||||
return self.object.itemAtIndex(index)
|
||||
values.append(cur_value)
|
||||
time.sleep(1)
|
||||
if time.monotonic() - started_at > timeout_sec:
|
||||
raise RuntimeError(f'value not found in list: {values}')
|
|
@ -0,0 +1,118 @@
|
|||
import logging
|
||||
import time
|
||||
|
||||
import allure
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from gui.elements.base_object import BaseObject
|
||||
from scripts.tools.image import Image
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QObject(BaseObject):
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
self._image = Image(self.real_name)
|
||||
|
||||
def __str__(self):
|
||||
return f'{type(self).__qualname__}({self.symbolic_name})'
|
||||
|
||||
@property
|
||||
@allure.step('Get object {0}')
|
||||
def object(self):
|
||||
return driver.waitForObject(self.real_name, configs.timeouts.UI_LOAD_TIMEOUT_MSEC)
|
||||
|
||||
@property
|
||||
@allure.step('Get object exists {0}')
|
||||
def exists(self) -> bool:
|
||||
return driver.object.exists(self.real_name)
|
||||
|
||||
@property
|
||||
@allure.step('Get bounds {0}')
|
||||
def bounds(self):
|
||||
return driver.object.globalBounds(self.object)
|
||||
|
||||
@property
|
||||
@allure.step('Get "x" coordinate {0}')
|
||||
def x(self) -> int:
|
||||
return self.bounds.x
|
||||
|
||||
@property
|
||||
@allure.step('Get "y" coordinate {0}')
|
||||
def y(self) -> int:
|
||||
return self.bounds.y
|
||||
|
||||
@property
|
||||
@allure.step('Get width {0}')
|
||||
def width(self) -> int:
|
||||
return int(self.bounds.width)
|
||||
|
||||
@property
|
||||
@allure.step('Get height {0}')
|
||||
def height(self) -> int:
|
||||
return int(self.bounds.height)
|
||||
|
||||
@property
|
||||
@allure.step('Get central coordinate {0}')
|
||||
def center(self):
|
||||
return self.bounds.center()
|
||||
|
||||
@property
|
||||
@allure.step('Get enabled {0}')
|
||||
def is_enabled(self) -> bool:
|
||||
return self.object.enabled
|
||||
|
||||
@property
|
||||
@allure.step('Get selected {0}')
|
||||
def is_selected(self) -> bool:
|
||||
return self.object.selected
|
||||
|
||||
@property
|
||||
@allure.step('Get checked {0}')
|
||||
def is_checked(self) -> bool:
|
||||
return self.object.checked
|
||||
|
||||
@property
|
||||
@allure.step('Get visible {0}')
|
||||
def is_visible(self) -> bool:
|
||||
try:
|
||||
return driver.waitForObject(self.real_name, 0).visible
|
||||
except (AttributeError, LookupError, RuntimeError):
|
||||
return False
|
||||
|
||||
@property
|
||||
@allure.step('Get image {0}')
|
||||
def image(self):
|
||||
if self._image.view is None:
|
||||
self._image.update_view()
|
||||
return self._image
|
||||
|
||||
@allure.step('Click {0}')
|
||||
def click(
|
||||
self,
|
||||
x: int = None,
|
||||
y: int = None,
|
||||
button=None
|
||||
):
|
||||
driver.mouseClick(
|
||||
self.object,
|
||||
x or self.width // 2,
|
||||
y or self.height // 2,
|
||||
button or driver.Qt.LeftButton
|
||||
)
|
||||
|
||||
@allure.step('Hover {0}')
|
||||
def hover(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
|
||||
def _hover():
|
||||
try:
|
||||
driver.mouseMove(self.object)
|
||||
return getattr(self.object, 'hovered', True)
|
||||
except RuntimeError as err:
|
||||
_logger.debug(err)
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
assert driver.waitFor(lambda: _hover(), timeout_msec)
|
|
@ -0,0 +1,33 @@
|
|||
import allure
|
||||
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class Slider(QObject):
|
||||
|
||||
@property
|
||||
@allure.step('Get minimal value {0}')
|
||||
def min(self) -> int:
|
||||
return int(getattr(self.object, 'from'))
|
||||
|
||||
@property
|
||||
@allure.step('Get maximal value {0}')
|
||||
def max(self) -> max:
|
||||
return int(getattr(self.object, 'to'))
|
||||
|
||||
@property
|
||||
@allure.step('Get value {0}')
|
||||
def value(self) -> int:
|
||||
return int(self.object.value)
|
||||
|
||||
@value.setter
|
||||
@allure.step('Set value {1} {0}')
|
||||
def value(self, value: int):
|
||||
if value != self.value:
|
||||
if self.min <= value <= self.max:
|
||||
if self.value < value:
|
||||
while self.value < value:
|
||||
self.object.increase()
|
||||
if self.value > value:
|
||||
while self.value > value:
|
||||
self.object.decrease()
|
|
@ -0,0 +1,33 @@
|
|||
import allure
|
||||
|
||||
import configs
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class TextEdit(QObject):
|
||||
|
||||
@property
|
||||
@allure.step('Get current text {0}')
|
||||
def text(self) -> str:
|
||||
return str(self.object.text)
|
||||
|
||||
@text.setter
|
||||
@allure.step('Type text {1} {0}')
|
||||
def text(self, value: str):
|
||||
self.clear()
|
||||
self.type_text(value)
|
||||
assert driver.waitFor(lambda: self.text == value, configs.timeouts.UI_LOAD_TIMEOUT_MSEC), \
|
||||
f'Type text failed, value in field: "{self.text}", expected: {value}'
|
||||
|
||||
@allure.step('Type: {1} in {0}')
|
||||
def type_text(self, value: str):
|
||||
driver.type(self.object, value)
|
||||
return self
|
||||
|
||||
@allure.step('Clear {0}')
|
||||
def clear(self):
|
||||
self.object.clear()
|
||||
assert driver.waitFor(lambda: not self.text), \
|
||||
f'Clear text field failed, value in field: "{self.text}"'
|
||||
return self
|
|
@ -0,0 +1,11 @@
|
|||
import allure
|
||||
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
|
||||
class TextLabel(QObject):
|
||||
|
||||
@property
|
||||
@allure.step('Get text {0}')
|
||||
def text(self) -> str:
|
||||
return str(self.object.text)
|
|
@ -0,0 +1,40 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
import driver
|
||||
from gui.elements.qt.object import QObject
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Window(QObject):
|
||||
|
||||
def prepare(self) -> 'Window':
|
||||
self.maximize()
|
||||
self.on_top_level()
|
||||
return self
|
||||
|
||||
@allure.step("Maximize {0}")
|
||||
def maximize(self):
|
||||
assert driver.toplevel_window.maximize(self.real_name), 'Maximize failed'
|
||||
_logger.info(f'Window {getattr(self.object, "title", "")} is maximized')
|
||||
|
||||
@allure.step("Minimize {0}")
|
||||
def minimize(self):
|
||||
assert driver.toplevel_window.minimize(self.real_name), 'Minimize failed'
|
||||
_logger.info(f'Window {getattr(self.object, "title", "")} is minimized')
|
||||
|
||||
@allure.step("Set focus on {0}")
|
||||
def set_focus(self):
|
||||
assert driver.toplevel_window.set_focus(self.real_name), 'Set focus failed'
|
||||
_logger.info(f'Window {getattr(self.object, "title", "")} in focus')
|
||||
|
||||
@allure.step("Move {0} on top")
|
||||
def on_top_level(self):
|
||||
assert driver.toplevel_window.on_top_level(self.real_name), 'Set on top failed'
|
||||
_logger.info(f'Window {getattr(self.object, "title", "")} moved on top')
|
||||
|
||||
@allure.step("Close {0}")
|
||||
def close(self):
|
||||
driver.toplevel_window.close(self.real_name)
|
|
@ -1,9 +1,46 @@
|
|||
import logging
|
||||
|
||||
import allure
|
||||
|
||||
from gui.components.user_canvas import UserCanvas
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.object import QObject
|
||||
from gui.elements.qt.window import Window
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MainWindow:
|
||||
class LeftPanel(QObject):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
super(LeftPanel, self).__init__('mainWindow_StatusAppNavBar')
|
||||
self._profile_button = Button('mainWindow_ProfileNavBarButton')
|
||||
|
||||
@property
|
||||
@allure.step('Get user badge color')
|
||||
def user_badge_color(self) -> str:
|
||||
return str(self._profile_button.object.badge.color.name)
|
||||
|
||||
@allure.step('Open user canvas')
|
||||
def open_user_canvas(self) -> UserCanvas:
|
||||
self._profile_button.click()
|
||||
return UserCanvas().wait_until_appears()
|
||||
|
||||
@allure.step('Verify: User is online')
|
||||
def user_is_online(self) -> bool:
|
||||
return self.user_badge_color == '#4ebc60'
|
||||
|
||||
@allure.step('Verify: User is offline')
|
||||
def user_is_offline(self):
|
||||
return self.user_badge_color == '#7f8990'
|
||||
|
||||
@allure.step('Verify: User is set to automatic')
|
||||
def user_is_set_to_automatic(self):
|
||||
return self.user_badge_color == '#4ebc60'
|
||||
|
||||
|
||||
class MainWindow(Window):
|
||||
|
||||
def __init__(self):
|
||||
super(MainWindow, self).__init__('statusDesktop_mainWindow')
|
||||
self.left_panel = LeftPanel()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from .component_names import *
|
||||
from .main_window_names import *
|
||||
from .messages_names import *
|
||||
from .main_names import *
|
||||
from .onboarding_names import *
|
||||
from .settings_names import *
|
||||
from .os_names import *
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
from objectmaphelper import *
|
||||
from . main_names import statusDesktop_mainWindow_overlay
|
||||
|
||||
# Before you get started Popup
|
||||
acknowledge_checkbox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName": "acknowledgeCheckBox", "type": "StatusCheckBox", "visible": True}
|
||||
termsOfUseCheckBox_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName":"termsOfUseCheckBox", "type": "StatusCheckBox", "visible": True}
|
||||
getStartedStatusButton_StatusButton = {"container": statusDesktop_mainWindow_overlay, "objectName": "getStartedStatusButton", "type": "StatusButton", "visible": True}
|
||||
|
||||
# Back Up Your Seed Phrase Popup
|
||||
o_PopupItem = {"container": statusDesktop_mainWindow_overlay, "type": "PopupItem", "unnamed": 1, "visible": True}
|
||||
i_have_a_pen_and_paper_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName": "Acknowledgements_havePen", "type": "StatusCheckBox", "visible": True}
|
||||
i_know_where_I_ll_store_it_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName": "Acknowledgements_storeIt", "type": "StatusCheckBox", "visible": True}
|
||||
i_am_ready_to_write_down_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName": "Acknowledgements_writeDown", "type": "StatusCheckBox", "visible": True}
|
||||
not_Now_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "type": "StatusButton", "unnamed": 1, "visible": True}
|
||||
confirm_Seed_Phrase_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "objectName": "BackupSeedModal_nextButton", "type": "StatusButton", "visible": True}
|
||||
backup_seed_phrase_popup_ConfirmSeedPhrasePanel_StatusSeedPhraseInput_placeholder = {"container": statusDesktop_mainWindow_overlay, "objectName": "ConfirmSeedPhrasePanel_StatusSeedPhraseInput_%WORD_NO%", "type": "StatusSeedPhraseInput", "visible": True}
|
||||
reveal_seed_phrase_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "objectName": "ConfirmSeedPhrasePanel_RevealSeedPhraseButton", "type": "StatusButton", "visible": True}
|
||||
blur_GaussianBlur = {"container": statusDesktop_mainWindow_overlay, "id": "blur", "type": "GaussianBlur", "unnamed": 1, "visible": True}
|
||||
confirmSeedPhrasePanel_StatusSeedPhraseInput = {"container": statusDesktop_mainWindow_overlay, "type": "StatusSeedPhraseInput", "visible": True}
|
||||
confirmFirstWord = {"container": statusDesktop_mainWindow_overlay, "objectName": "BackupSeedModal_BackupSeedStepBase_confirmFirstWord", "type": "BackupSeedStepBase", "visible": True}
|
||||
confirmFirstWord_inputText = {"container": confirmFirstWord, "objectName": "BackupSeedStepBase_inputText", "type": "TextEdit", "visible": True}
|
||||
continue_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "objectName": "BackupSeedModal_nextButton", "type": "StatusButton", "visible": True}
|
||||
confirmSecondWord = {"container": statusDesktop_mainWindow_overlay, "objectName": "BackupSeedModal_BackupSeedStepBase_confirmSecondWord", "type": "BackupSeedStepBase", "visible": True}
|
||||
confirmSecondWord_inputText = {"container": confirmSecondWord, "objectName": "BackupSeedStepBase_inputText", "type": "TextEdit", "visible": True}
|
||||
i_acknowledge_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "objectName": "ConfirmStoringSeedPhrasePanel_storeCheck", "type": "StatusCheckBox", "visible": True}
|
||||
completeAndDeleteSeedPhraseButton = {"container": statusDesktop_mainWindow_overlay, "objectName": "BackupSeedModal_completeAndDeleteSeedPhraseButton", "type": "StatusButton", "visible": True}
|
||||
|
||||
# Send Contact Request Popup
|
||||
contactRequest_ChatKey_Input = {"container": statusDesktop_mainWindow_overlay, "objectName": "SendContactRequestModal_ChatKey_Input", "type": "TextEdit"}
|
||||
contactRequest_SayWhoYouAre_Input = {"container": statusDesktop_mainWindow_overlay, "objectName": "SendContactRequestModal_SayWhoYouAre_Input", "type": "TextEdit"}
|
||||
contactRequest_Send_Button = {"container": statusDesktop_mainWindow_overlay, "objectName": "SendContactRequestModal_Send_Button", "type": "StatusButton"}
|
||||
|
||||
# Change Language Popup
|
||||
close_the_app_now_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "type": "StatusButton", "unnamed": 1, "visible": True}
|
||||
|
||||
# User Status Profile Menu
|
||||
o_StatusListView = {"container": statusDesktop_mainWindow_overlay, "type": "StatusListView", "unnamed": 1, "visible": True}
|
||||
userContextmenu_AlwaysActiveButton= {"container": o_StatusListView, "objectName": "userStatusMenuAlwaysOnlineAction", "type": "StatusMenuItem", "visible": True}
|
||||
userContextmenu_InActiveButton= {"container": o_StatusListView, "objectName": "userStatusMenuInactiveAction", "type": "StatusMenuItem", "visible": True}
|
||||
userContextmenu_AutomaticButton= {"container": o_StatusListView, "objectName": "userStatusMenuAutomaticAction", "type": "StatusMenuItem", "visible": True}
|
||||
userContextMenu_ViewMyProfileAction = {"container": o_StatusListView, "objectName": "userStatusViewMyProfileAction", "type": "StatusMenuItem", "visible": True}
|
||||
userLabel_StyledText = {"container": o_StatusListView, "type": "StyledText", "unnamed": 1, "visible": True}
|
||||
o_StatusIdenticonRing = {"container": o_StatusListView, "type": "StatusIdenticonRing", "unnamed": 1, "visible": True}
|
||||
|
||||
# My Profile Popup
|
||||
ProfileHeader_userImage = {"container": statusDesktop_mainWindow_overlay, "objectName": "ProfileDialog_userImage", "type": "UserImage", "visible": True}
|
||||
ProfilePopup_displayName = {"container": statusDesktop_mainWindow_overlay, "objectName": "ProfileDialog_displayName", "type": "StatusBaseText", "visible": True}
|
||||
ProfilePopup_editButton = {"container": statusDesktop_mainWindow_overlay, "objectName": "editProfileButton", "type": "StatusButton", "visible": True}
|
||||
ProfilePopup_SendContactRequestButton = {"container": statusDesktop_mainWindow_overlay, "objectName": "profileDialog_sendContactRequestButton", "type": "StatusButton", "visible": True}
|
||||
profileDialog_userEmojiHash_EmojiHash = {"container": statusDesktop_mainWindow_overlay, "objectName": "ProfileDialog_userEmojiHash", "type": "EmojiHash", "visible": True}
|
||||
edit_TextEdit = {"container": statusDesktop_mainWindow_overlay, "id": "edit", "type": "TextEdit", "unnamed": 1, "visible": True}
|
||||
https_status_app_StatusBaseText = {"container": edit_TextEdit, "type": "StatusBaseText", "unnamed": 1, "visible": True}
|
||||
|
||||
# Welcome Status Popup
|
||||
agreeToUse_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "id": "agreeToUse", "type": "StatusCheckBox", "unnamed": 1, "visible": True}
|
||||
readyToUse_StatusCheckBox = {"checkable": True, "container": statusDesktop_mainWindow_overlay, "id": "readyToUse", "type": "StatusCheckBox", "unnamed": 1, "visible": True}
|
||||
i_m_ready_to_use_Status_Desktop_Beta_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "type": "StatusButton", "unnamed": 1, "visible": True}
|
||||
|
||||
# Profile Picture Popup
|
||||
o_StatusSlider = {"container": statusDesktop_mainWindow_overlay, "type": "StatusSlider", "unnamed": 1, "visible": True}
|
||||
cropSpaceItem_Item = {"container": statusDesktop_mainWindow_overlay, "id": "cropSpaceItem", "type": "Item", "unnamed": 1, "visible": True}
|
||||
make_this_my_profile_picture_StatusButton = {"checkable": False, "container": statusDesktop_mainWindow_overlay, "objectName": "imageCropperAcceptButton", "type": "StatusButton", "visible": True}
|
||||
o_DropShadow = {"container": statusDesktop_mainWindow_overlay, "type": "DropShadow", "unnamed": 1, "visible": True}
|
|
@ -0,0 +1,14 @@
|
|||
statusDesktop_mainWindow = {"name": "mainWindow", "type": "StatusWindow", "visible": True}
|
||||
statusDesktop_mainWindow_overlay = {"container": statusDesktop_mainWindow, "type": "Overlay", "unnamed": 1, "visible": True}
|
||||
splashScreen = {"container": statusDesktop_mainWindow, "objectName": "splashScreen", "type": "DidYouKnowSplashScreen"}
|
||||
|
||||
# Navigation Panel
|
||||
mainWindow_StatusAppNavBar = {"container": statusDesktop_mainWindow, "type": "StatusAppNavBar", "unnamed": 1, "visible": True}
|
||||
messages_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_StatusAppNavBar, "objectName": "Messages-navbar", "type": "StatusNavBarTabButton", "visible": True}
|
||||
communities_Portal_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_StatusAppNavBar, "objectName": "Communities Portal-navbar", "type": "StatusNavBarTabButton", "visible": True}
|
||||
wallet_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_StatusAppNavBar, "objectName": "Wallet-navbar", "type": "StatusNavBarTabButton", "visible": True}
|
||||
settings_navbar_StatusNavBarTabButton = {"checkable": True, "container": mainWindow_StatusAppNavBar, "objectName": "Settings-navbar", "type": "StatusNavBarTabButton", "visible": True}
|
||||
mainWindow_ProfileNavBarButton = {"container": statusDesktop_mainWindow, "objectName": "statusProfileNavBarTabButton", "type": "StatusNavBarTabButton", "visible": True}
|
||||
|
||||
# Banners
|
||||
secureYourSeedPhraseBanner_ModuleWarning = {"container": statusDesktop_mainWindow, "objectName": "secureYourSeedPhraseBanner", "type": "ModuleWarning", "visible": True}
|
|
@ -0,0 +1,56 @@
|
|||
from . main_names import *
|
||||
|
||||
mainWindow_onboardingBackButton_StatusRoundButton = {"container": statusDesktop_mainWindow, "objectName": "onboardingBackButton", "type": "StatusRoundButton", "visible": True}
|
||||
|
||||
# Allow Notification View
|
||||
mainWindow_AllowNotificationsView = {"container": statusDesktop_mainWindow, "type": "AllowNotificationsView", "unnamed": 1, "visible": True}
|
||||
mainWindow_allowNotificationsOnboardingOkButton = {"container": mainWindow_AllowNotificationsView, "objectName": "allowNotificationsOnboardingOkButton", "type": "StatusButton", "visible": True}
|
||||
|
||||
# Welcome View
|
||||
mainWindow_WelcomeView = {"container": statusDesktop_mainWindow, "type": "WelcomeView", "unnamed": 1, "visible": True}
|
||||
mainWindow_I_am_new_to_Status_StatusBaseText = {"container": mainWindow_WelcomeView, "objectName": "welcomeViewIAmNewToStatusButton", "type": "StatusButton"}
|
||||
mainWindow_I_already_use_Status_StatusBaseText = {"container": mainWindow_WelcomeView, "objectName": "welcomeViewIAlreadyUseStatusButton", "type": "StatusFlatButton", "visible": True}
|
||||
|
||||
# Get Keys View
|
||||
mainWindow_KeysMainView = {"container": statusDesktop_mainWindow, "type": "KeysMainView", "unnamed": 1, "visible": True}
|
||||
mainWindow_Generate_new_keys_StatusButton = {"checkable": False, "container": mainWindow_KeysMainView, "objectName": "keysMainViewPrimaryActionButton", "type": "StatusButton", "visible": True}
|
||||
|
||||
# Your Profile View
|
||||
mainWindow_InsertDetailsView = {"container": statusDesktop_mainWindow, "type": "InsertDetailsView", "unnamed": 1, "visible": True}
|
||||
updatePicButton_StatusRoundButton = {"container": mainWindow_InsertDetailsView, "id": "updatePicButton", "type": "StatusRoundButton", "unnamed": 1, "visible": True}
|
||||
mainWindow_CanvasItem = {"container": mainWindow_InsertDetailsView, "type": "CanvasItem", "unnamed": 1, "visible": True}
|
||||
mainWindow_Next_StatusButton = {"container": statusDesktop_mainWindow, "objectName": "onboardingDetailsViewNextButton", "type": "StatusButton", "visible": True, "enabled": True}
|
||||
mainWindow_inputLayout_ColumnLayout = {"container": statusDesktop_mainWindow, "id": "inputLayout", "type": "ColumnLayout", "unnamed": 1, "visible": True}
|
||||
mainWindow_statusBaseInput_StatusBaseInput = {"container": mainWindow_inputLayout_ColumnLayout, "objectName": "onboardingDisplayNameInput", "type": "TextEdit", "visible": True}
|
||||
mainWindow_errorMessage_StatusBaseText = {"container": mainWindow_inputLayout_ColumnLayout, "type": "StatusBaseText", "unnamed": 1, "visible": True}
|
||||
|
||||
# Your emojihash and identicon ring
|
||||
mainWindow_welcomeScreenUserProfileImage_StatusSmartIdenticon = {"container": mainWindow_InsertDetailsView, "objectName": "welcomeScreenUserProfileImage", "type": "StatusSmartIdenticon", "visible": True}
|
||||
mainWindow_insertDetailsViewChatKeyTxt_StyledText = {"container": mainWindow_InsertDetailsView, "objectName": "insertDetailsViewChatKeyTxt", "type": "StyledText", "visible": True}
|
||||
mainWindow_EmojiHash = {"container": mainWindow_InsertDetailsView, "type": "EmojiHash", "unnamed": 1, "visible": True}
|
||||
mainWindow_userImageCopy_StatusSmartIdenticon = {"container": mainWindow_InsertDetailsView, "id": "userImageCopy", "type": "StatusSmartIdenticon", "unnamed": 1, "visible": True}
|
||||
|
||||
|
||||
# Create Password View
|
||||
mainWindow_CreatePasswordView = {"container": statusDesktop_mainWindow, "type": "CreatePasswordView", "unnamed": 1, "visible": True}
|
||||
mainWindow_passwordViewNewPassword = {"container": mainWindow_CreatePasswordView, "echoMode": 2, "objectName": "passwordViewNewPassword", "type": "StatusPasswordInput", "visible": True}
|
||||
mainWindow_passwordViewNewPasswordConfirm = {"container": mainWindow_CreatePasswordView, "echoMode": 2, "objectName": "passwordViewNewPasswordConfirm", "type": "StatusPasswordInput", "visible": True}
|
||||
mainWindow_Create_password_StatusButton = {"checkable": False, "container": mainWindow_CreatePasswordView, "objectName": "onboardingCreatePasswordButton", "type": "StatusButton", "visible": True, "enabled": True}
|
||||
|
||||
# Confirm Password View
|
||||
mainWindow_ConfirmPasswordView = {"container": statusDesktop_mainWindow, "type": "ConfirmPasswordView", "unnamed": 1,"visible": True}
|
||||
mainWindow_confirmAgainPasswordInput = {"container": mainWindow_ConfirmPasswordView, "objectName": "confirmAgainPasswordInput", "type": "StatusPasswordInput", "visible": True}
|
||||
mainWindow_Finalise_Status_Password_Creation_StatusButton = {"checkable": False, "container": mainWindow_ConfirmPasswordView, "objectName": "confirmPswSubmitBtn", "type": "StatusButton", "visible": True}
|
||||
|
||||
# Login View
|
||||
mainWindow_LoginView = {"container": statusDesktop_mainWindow, "type": "LoginView", "unnamed": 1, "visible": True}
|
||||
loginView_submitBtn = {"container": mainWindow_LoginView, "type": "StatusRoundButton", "visible": True}
|
||||
loginView_passwordInput = {"container": mainWindow_LoginView, "objectName": "loginPasswordInput", "type": "StyledTextField"}
|
||||
loginView_currentUserNameLabel = {"container": mainWindow_LoginView, "objectName": "currentUserNameLabel", "type": "StatusBaseText"}
|
||||
loginView_changeAccountBtn = {"container": mainWindow_LoginView, "objectName": "loginChangeAccountButton", "type": "StatusFlatRoundButton"}
|
||||
accountsView_accountListPanel = {"container": statusDesktop_mainWindow, "objectName": "LoginView_AccountsRepeater", "type": "Repeater", "visible": True}
|
||||
|
||||
|
||||
# Touch ID Auth View
|
||||
mainWindow_TouchIDAuthView = {"container": statusDesktop_mainWindow, "type": "TouchIDAuthView", "unnamed": 1, "visible": True}
|
||||
mainWindow_touchIdIPreferToUseMyPasswordText = {"container": statusDesktop_mainWindow, "objectName": "touchIdIPreferToUseMyPasswordText", "type": "StatusBaseText"}
|
|
@ -0,0 +1,25 @@
|
|||
""" MAC """
|
||||
# Open Files Dialog
|
||||
mainWindow = {"AXRole": "AXWindow", "AXMain": True}
|
||||
openFileDialog = {"container": mainWindow, "AXRole": "AXSheet", "AXIdentifier": "open-panel"}
|
||||
openButton = {"container": openFileDialog, "AXRole": "AXButton", "AXIdentifier": "OKButton"}
|
||||
|
||||
# Go To Dialog
|
||||
goToDialog = {"container": openFileDialog, "AXRole": "AXSheet", "AXIdentifier": "GoToWindow"}
|
||||
pathTextField = {"container": goToDialog, "AXRole": "AXTextField", "AXIdentifier": "PathTextField"}
|
||||
|
||||
""" WINDOWS """
|
||||
# Open File Dialog
|
||||
file_Dialog = {"type": "Dialog"}
|
||||
choose_file_Edit = {"container": file_Dialog, "type": "Edit"}
|
||||
choose_Open_Button = {"container": file_Dialog, "text": "Open", "type": "Button"}
|
||||
|
||||
""" LINUX """
|
||||
# Open File Dialog
|
||||
# Select Image Dialog
|
||||
please_choose_an_image_QQuickWindow = {"type": "QQuickWindow", "unnamed": 1, "visible": True}
|
||||
please_choose_an_image_Open_Button = {"container": please_choose_an_image_QQuickWindow, "id": "okButton", "type": "Button", "unnamed": 1, "visible": True}
|
||||
please_choose_an_image_titleBar_ToolBar = {"container": please_choose_an_image_QQuickWindow, "id": "titleBar", "type": "ToolBar", "unnamed": 1, "visible": True}
|
||||
titleBar_textInput_TextInputWithHandles = {"container": please_choose_an_image_QQuickWindow, "echoMode": 0, "id": "textInput", "type": "TextInputWithHandles", "unnamed": 1, "visible": True}
|
||||
view_listView_ListView = {"container": please_choose_an_image_QQuickWindow, "id": "listView", "type": "ListView", "unnamed": 1, "visible": True}
|
||||
rowitem_Text = {"container": view_listView_ListView, "type": "Text", "unnamed": 1, "visible": True}
|
|
@ -0,0 +1,249 @@
|
|||
import logging
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
|
||||
import allure
|
||||
import cv2
|
||||
|
||||
import configs.testpath
|
||||
import constants.tesseract
|
||||
import driver
|
||||
from gui.components.os.open_file_dialogs import OpenFileDialog
|
||||
from gui.components.profile_picture_popup import ProfilePicturePopup
|
||||
from gui.elements.qt.button import Button
|
||||
from gui.elements.qt.object import QObject
|
||||
from gui.elements.qt.text_edit import TextEdit
|
||||
from gui.elements.qt.text_label import TextLabel
|
||||
from scripts.tools.image import Image
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AllowNotificationsView(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super(AllowNotificationsView, self).__init__('mainWindow_AllowNotificationsView')
|
||||
self._allow_button = Button('mainWindow_allowNotificationsOnboardingOkButton')
|
||||
|
||||
@allure.step("Allow Notifications")
|
||||
def allow(self):
|
||||
self._allow_button.click()
|
||||
self.wait_until_hidden()
|
||||
|
||||
|
||||
class WelcomeScreen(QObject):
|
||||
|
||||
def __init__(self):
|
||||
super(WelcomeScreen, self).__init__('mainWindow_WelcomeView')
|
||||
self._new_user_button = Button('mainWindow_I_am_new_to_Status_StatusBaseText')
|
||||
self._existing_user_button = Button('mainWindow_I_already_use_Status_StatusBaseText')
|
||||
|
||||
@allure.step('Open Keys view')
|
||||
def get_keys(self) -> 'KeysView':
|
||||
self._new_user_button.click()
|
||||
time.sleep(1)
|
||||
return KeysView().wait_until_appears()
|
||||
|
||||
|
||||
class OnboardingScreen(QObject):
|
||||
|
||||
def __init__(self, object_name):
|
||||
super(OnboardingScreen, self).__init__(object_name)
|
||||
self._back_button = Button('mainWindow_onboardingBackButton_StatusRoundButton')
|
||||
|
||||
@abstractmethod
|
||||
def back(self):
|
||||
pass
|
||||
|
||||
|
||||
class KeysView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(KeysView, self).__init__('mainWindow_KeysMainView')
|
||||
self._generate_key_button = Button('mainWindow_Generate_new_keys_StatusButton')
|
||||
|
||||
@allure.step('Open Profile view')
|
||||
def generate_new_keys(self) -> 'YourProfileView':
|
||||
self._generate_key_button.click()
|
||||
return YourProfileView().wait_until_appears()
|
||||
|
||||
@allure.step('Go back')
|
||||
def back(self) -> WelcomeScreen:
|
||||
self._back_button.click()
|
||||
return WelcomeScreen().wait_until_appears()
|
||||
|
||||
|
||||
class YourProfileView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(YourProfileView, self).__init__('mainWindow_InsertDetailsView')
|
||||
self._upload_picture_button = Button('updatePicButton_StatusRoundButton')
|
||||
self._profile_image = QObject('mainWindow_CanvasItem')
|
||||
self._display_name_text_field = TextEdit('mainWindow_statusBaseInput_StatusBaseInput')
|
||||
self._erros_text_label = TextLabel('mainWindow_errorMessage_StatusBaseText')
|
||||
self._next_button = Button('mainWindow_Next_StatusButton')
|
||||
|
||||
@property
|
||||
@allure.step('Get profile image')
|
||||
def profile_image(self) -> Image:
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get error messages')
|
||||
def error_message(self) -> str:
|
||||
return self._erros_text_label.text if self._erros_text_label.is_visible else ''
|
||||
|
||||
@allure.step('Set user display name')
|
||||
def set_display_name(self, value: str):
|
||||
self._display_name_text_field.clear().text = value
|
||||
return self
|
||||
|
||||
@allure.step('Set user image')
|
||||
def set_user_image(self, fp: SystemPath) -> ProfilePicturePopup:
|
||||
allure.attach(name='User image', body=fp.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
self._upload_picture_button.hover()
|
||||
self._upload_picture_button.click()
|
||||
file_dialog = OpenFileDialog().wait_until_appears()
|
||||
file_dialog.open_file(fp)
|
||||
return ProfilePicturePopup().wait_until_appears()
|
||||
|
||||
@allure.step('Open Emoji and Icon view')
|
||||
def next(self) -> 'EmojiAndIconView':
|
||||
self._next_button.click()
|
||||
time.sleep(1)
|
||||
return EmojiAndIconView()
|
||||
|
||||
@allure.step('Go back')
|
||||
def back(self):
|
||||
self._back_button.click()
|
||||
return KeysView().wait_until_appears()
|
||||
|
||||
|
||||
class EmojiAndIconView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(EmojiAndIconView, self).__init__('mainWindow_InsertDetailsView')
|
||||
self._profile_image = QObject('mainWindow_welcomeScreenUserProfileImage_StatusSmartIdenticon')
|
||||
self._chat_key_text_label = TextLabel('mainWindow_insertDetailsViewChatKeyTxt_StyledText')
|
||||
self._next_button = Button('mainWindow_Next_StatusButton')
|
||||
self._emoji_hash = QObject('mainWindow_EmojiHash')
|
||||
self._identicon_ring = QObject('mainWindow_userImageCopy_StatusSmartIdenticon')
|
||||
|
||||
@property
|
||||
@allure.step('Get profile image icon')
|
||||
def profile_image(self) -> Image:
|
||||
self._profile_image.image.update_view()
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get profile image icon without identicon ring')
|
||||
def cropped_profile_image(self) -> Image:
|
||||
# Profile image without identicon_ring
|
||||
self._profile_image.image.update_view()
|
||||
self._profile_image.image.crop(
|
||||
driver.UiTypes.ScreenRectangle(
|
||||
20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40
|
||||
))
|
||||
return self._profile_image.image
|
||||
|
||||
@property
|
||||
@allure.step('Get chat key')
|
||||
def chat_key(self) -> str:
|
||||
return self._chat_key_text_label.text.split(':')[1].strip()
|
||||
|
||||
@property
|
||||
@allure.step('Get emoji hash image')
|
||||
def emoji_hash(self) -> Image:
|
||||
return self._emoji_hash.image
|
||||
|
||||
@property
|
||||
@allure.step('Verify: Identicon ring visible')
|
||||
def is_identicon_ring_visible(self):
|
||||
return self._identicon_ring.is_visible
|
||||
|
||||
@allure.step('Open Create password view')
|
||||
def next(self) -> 'CreatePasswordView':
|
||||
self._next_button.click()
|
||||
time.sleep(1)
|
||||
return CreatePasswordView().wait_until_appears()
|
||||
|
||||
@allure.step('Go back')
|
||||
def back(self):
|
||||
self._back_button.click()
|
||||
return YourProfileView().wait_until_appears()
|
||||
|
||||
@allure.step
|
||||
@allure.step('Verify: User image contains text')
|
||||
def is_user_image_contains(self, text: str):
|
||||
crop = driver.UiTypes.ScreenRectangle(
|
||||
20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40
|
||||
)
|
||||
return self.profile_image.has_text(text, constants.tesseract.text_on_profile_image, crop=crop)
|
||||
|
||||
@allure.step
|
||||
@allure.step('Verify: User image background color')
|
||||
def is_user_image_background_white(self):
|
||||
crop = driver.UiTypes.ScreenRectangle(
|
||||
20, 20, self._profile_image.image.width - 40, self._profile_image.image.height - 40
|
||||
)
|
||||
return self.profile_image.has_color(constants.Color.WHITE, crop=crop)
|
||||
|
||||
|
||||
class CreatePasswordView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(CreatePasswordView, self).__init__('mainWindow_CreatePasswordView')
|
||||
self._new_password_text_field = TextEdit('mainWindow_passwordViewNewPassword')
|
||||
self._confirm_password_text_field = TextEdit('mainWindow_passwordViewNewPasswordConfirm')
|
||||
self._create_button = Button('mainWindow_Create_password_StatusButton')
|
||||
|
||||
@property
|
||||
@allure.step('Verify: Create password button enabled')
|
||||
def is_create_password_button_enabled(self) -> bool:
|
||||
# Verification is_enable can not be used
|
||||
# LookupError, because of "Enable: True" in object real name, if button disabled
|
||||
return self._create_button.is_visible
|
||||
|
||||
@allure.step('Set password and open Confirmation password view')
|
||||
def create_password(self, value: str) -> 'ConfirmPasswordView':
|
||||
self._new_password_text_field.clear().text = value
|
||||
self._confirm_password_text_field.clear().text = value
|
||||
self._create_button.click()
|
||||
time.sleep(1)
|
||||
return ConfirmPasswordView().wait_until_appears()
|
||||
|
||||
@allure.step('Go back')
|
||||
def back(self):
|
||||
self._back_button.click()
|
||||
return EmojiAndIconView().wait_until_appears()
|
||||
|
||||
|
||||
class ConfirmPasswordView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(ConfirmPasswordView, self).__init__('mainWindow_ConfirmPasswordView')
|
||||
self._confirm_password_text_field = TextEdit('mainWindow_confirmAgainPasswordInput')
|
||||
self._confirm_button = Button('mainWindow_Finalise_Status_Password_Creation_StatusButton')
|
||||
|
||||
@allure.step('Confirm password')
|
||||
def confirm_password(self, value: str):
|
||||
self._confirm_password_text_field.text = value
|
||||
self._confirm_button.click()
|
||||
|
||||
@allure.step('Go back')
|
||||
def back(self):
|
||||
self._back_button.click()
|
||||
return CreatePasswordView().wait_until_appears()
|
||||
|
||||
|
||||
class TouchIDAuthView(OnboardingScreen):
|
||||
|
||||
def __init__(self):
|
||||
super(TouchIDAuthView, self).__init__('mainWindow_TouchIDAuthView')
|
||||
self._prefer_password_button = Button('mainWindow_touchIdIPreferToUseMyPasswordText')
|
||||
|
||||
@allure.step('Select prefer password')
|
||||
def prefer_password(self):
|
||||
self._prefer_password_button.click()
|
||||
self.wait_until_hidden()
|
|
@ -3,5 +3,7 @@ log_format = %(asctime)s.%(msecs)03d %(levelname)7s %(name)s %(message).5000s
|
|||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
|
||||
addopts = --disable-warnings
|
||||
|
||||
markers =
|
||||
self: framework tests
|
||||
smoke: Smoke tests
|
||||
|
|
|
@ -1 +1,9 @@
|
|||
pytest==7.4.0
|
||||
psutil==5.9.5
|
||||
pillow==10.0.0
|
||||
opencv-python-headless==4.8.0.74
|
||||
numpy~=1.25.1
|
||||
pytesseract==0.3.10
|
||||
atomacos==3.3.0; platform_system == "Darwin"
|
||||
allure-pytest==2.13.2
|
||||
testrail-api==1.12.0
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
import logging
|
||||
import time
|
||||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
import allure
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytesseract
|
||||
from PIL import ImageGrab
|
||||
|
||||
import configs
|
||||
import constants
|
||||
import driver
|
||||
from configs.system import IS_LIN
|
||||
from scripts.tools.ocv import Ocv
|
||||
from scripts.utils.system_path import SystemPath
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Image:
|
||||
|
||||
def __init__(self, object_name: dict):
|
||||
self.object_name = object_name
|
||||
self._view = None
|
||||
|
||||
@property
|
||||
@allure.step('Get image view')
|
||||
def view(self) -> np.ndarray:
|
||||
return self._view
|
||||
|
||||
@property
|
||||
@allure.step('Get image height')
|
||||
def height(self) -> int:
|
||||
return self.view.shape[0]
|
||||
|
||||
@property
|
||||
@allure.step('Get image width')
|
||||
def width(self) -> int:
|
||||
return self.view.shape[1]
|
||||
|
||||
@property
|
||||
@allure.step('Get image is grayscale')
|
||||
def is_grayscale(self) -> bool:
|
||||
return self.view.ndim == 2
|
||||
|
||||
@allure.step('Set image in grayscale')
|
||||
def set_grayscale(self) -> 'Image':
|
||||
if not self.is_grayscale:
|
||||
self._view = cv2.cvtColor(self.view, cv2.COLOR_BGR2GRAY)
|
||||
return self
|
||||
|
||||
@allure.step('Grab image view from object')
|
||||
def update_view(self):
|
||||
_logger.debug(f'Image view was grab from: {self.object_name}')
|
||||
rect = driver.object.globalBounds(driver.waitForObject(self.object_name))
|
||||
img = ImageGrab.grab(
|
||||
bbox=(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height),
|
||||
xdisplay=":0" if IS_LIN else None
|
||||
)
|
||||
self._view = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
|
||||
|
||||
@allure.step('Save image')
|
||||
def save(self, path: SystemPath, force: bool = False):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists() and not force:
|
||||
raise FileExistsError(path)
|
||||
cv2.imwrite(str(path), self.view)
|
||||
|
||||
@allure.step('Compare images')
|
||||
def compare(
|
||||
self, expected: np.ndarray, threshold: float = 0.99) -> bool:
|
||||
correlation = Ocv.compare_images(self.view, expected)
|
||||
result = correlation >= threshold
|
||||
_logger.info(f'Images equals on: {abs(round(correlation, 4) * 100)}%')
|
||||
|
||||
if result:
|
||||
_logger.info(f'Screenshot comparison passed')
|
||||
else:
|
||||
configs.testpath.TEST_ARTIFACTS.mkdir(parents=True, exist_ok=True)
|
||||
diff = Ocv.draw_contours(self.view, expected)
|
||||
|
||||
actual_fp = configs.testpath.TEST_ARTIFACTS / f'actual_image.png'
|
||||
expected_fp = configs.testpath.TEST_ARTIFACTS / f'expected_image.png'
|
||||
diff_fp = configs.testpath.TEST_ARTIFACTS / f'diff_image.png'
|
||||
|
||||
self.save(actual_fp, force=True)
|
||||
cv2.imwrite(str(expected_fp), expected)
|
||||
cv2.imwrite(str(diff_fp), diff)
|
||||
|
||||
allure.attach(name='actual', body=actual_fp.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
allure.attach(name='expected', body=expected_fp.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
allure.attach(name='diff', body=diff_fp.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
|
||||
_logger.info(
|
||||
f"Screenshot comparison failed.\n"
|
||||
f"Actual, Diff and Expected screenshots are saved:\n"
|
||||
f"{configs.testpath.TEST_ARTIFACTS.relative_to(configs.testpath.ROOT)}.")
|
||||
return result
|
||||
|
||||
@allure.step('Crop image')
|
||||
def crop(self, rect: driver.UiTypes.ScreenRectangle):
|
||||
assert rect.x + rect.width < self.width
|
||||
assert rect.y + rect.height < self.height
|
||||
self._view = self.view[rect.y: (rect.y + rect.height), rect.x: (rect.x + rect.width)]
|
||||
|
||||
@allure.step('Parse text on image')
|
||||
def to_string(self, custom_config: str):
|
||||
text: str = pytesseract.image_to_string(self.view, config=custom_config)
|
||||
_logger.debug(f'Text on image: {text}')
|
||||
return text
|
||||
|
||||
@allure.step('Verify: Image contains text: {1}')
|
||||
def has_text(self, text: str, criteria: str, crop: driver.UiTypes.ScreenRectangle = None) -> bool:
|
||||
self.update_view()
|
||||
if crop:
|
||||
self.crop(crop)
|
||||
|
||||
# Search text on image converted in gray color
|
||||
self.set_grayscale()
|
||||
fp_gray = configs.testpath.TEST_ARTIFACTS / f'search_region_in_gray_color.png'
|
||||
self.save(fp_gray, force=True)
|
||||
if text.lower() in self.to_string(criteria).lower():
|
||||
allure.attach(name='search_region', body=fp_gray.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
return True
|
||||
|
||||
# Search text on image with inverted color
|
||||
self._view = cv2.bitwise_not(self.view)
|
||||
fp_invert = configs.testpath.TEST_ARTIFACTS / f'search_region_in_inverted_color.png'
|
||||
self.save(fp_invert, force=True)
|
||||
if text.lower() in self.to_string(criteria).lower():
|
||||
allure.attach(name='search_region', body=fp_invert.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
return True
|
||||
return False
|
||||
|
||||
@allure.step('Search color on image')
|
||||
def has_color(self, color: constants.Color, denoise: int = 10, crop: driver.UiTypes.ScreenRectangle = None) -> bool:
|
||||
self.update_view()
|
||||
if crop:
|
||||
self.crop(crop)
|
||||
|
||||
initial_view = configs.testpath.TEST_ARTIFACTS / f'{color.name}.png'
|
||||
self.save(initial_view)
|
||||
allure.attach(name='search_region', body=initial_view.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
|
||||
contours = self._get_color_contours(color, denoise, apply=True)
|
||||
|
||||
mask_view = configs.testpath.TEST_ARTIFACTS / f'{color.name}_mask.png'
|
||||
self.save(mask_view)
|
||||
allure.attach(name='contours', body=mask_view.read_bytes(), attachment_type=allure.attachment_type.PNG)
|
||||
|
||||
self._view = None
|
||||
return len(contours) >= 1
|
||||
|
||||
@allure.step('Apply contours with found color on image')
|
||||
def _get_color_contours(
|
||||
self,
|
||||
color: constants.Color,
|
||||
denoise: int = 10,
|
||||
apply: bool = False
|
||||
) -> typing.List[driver.UiTypes.ScreenRectangle]:
|
||||
if not self.is_grayscale:
|
||||
view = cv2.cvtColor(self.view, cv2.COLOR_BGR2HSV)
|
||||
else:
|
||||
view = self.view
|
||||
boundaries = constants.boundaries[color]
|
||||
|
||||
if color == constants.Color.RED:
|
||||
mask = None
|
||||
for bond in boundaries:
|
||||
lower_range = np.array(bond[0])
|
||||
upper_range = np.array(bond[1])
|
||||
_mask = cv2.inRange(view, lower_range, upper_range)
|
||||
mask = _mask if mask is None else mask + _mask
|
||||
else:
|
||||
lower_range = np.array(boundaries[0])
|
||||
upper_range = np.array(boundaries[1])
|
||||
mask = cv2.inRange(view, lower_range, upper_range)
|
||||
|
||||
contours = []
|
||||
_contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
_contours = _contours[0] if len(_contours) == 2 else _contours[1]
|
||||
for _contour in _contours:
|
||||
x, y, w, h = cv2.boundingRect(_contour)
|
||||
# To remove small artifacts, less than denoise variable value
|
||||
if w * h < denoise:
|
||||
continue
|
||||
contours.append(driver.UiTypes.ScreenRectangle(x, y, w, h))
|
||||
|
||||
if apply:
|
||||
self._view = cv2.bitwise_and(self.view, self.view, mask=mask)
|
||||
for contour in contours:
|
||||
cv2.rectangle(
|
||||
self.view,
|
||||
(contour.x, contour.y),
|
||||
(contour.x + contour.width, contour.y + contour.height),
|
||||
(36, 255, 12), 2)
|
||||
|
||||
return contours
|
||||
|
||||
|
||||
@allure.step('Compare images')
|
||||
def compare(actual: Image,
|
||||
expected: typing.Union[str, SystemPath, Image],
|
||||
threshold: float = 0.99,
|
||||
timout_sec: int = 1
|
||||
):
|
||||
if isinstance(expected, str):
|
||||
expected_fp = configs.testpath.TEST_VP / configs.system.OS_ID / expected
|
||||
if not expected_fp.exists():
|
||||
expected_fp = configs.testpath.TEST_VP / expected
|
||||
expected = expected_fp
|
||||
if isinstance(expected, SystemPath):
|
||||
assert expected.exists(), f'File: {expected} not found'
|
||||
expected = cv2.imread(str(expected))
|
||||
else:
|
||||
expected = expected.view
|
||||
start = datetime.now()
|
||||
while not actual.compare(expected, threshold):
|
||||
time.sleep(1)
|
||||
assert (datetime.now() - start).seconds < timout_sec, 'Comparison failed'
|
||||
_logger.info(f'Screenshot comparison passed')
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Ocv:
|
||||
|
||||
@classmethod
|
||||
def compare_images(cls, lhd: np.ndarray, rhd: np.ndarray) -> float:
|
||||
res = cv2.matchTemplate(lhd, rhd, cv2.TM_CCOEFF_NORMED)
|
||||
_, correlation, _, _ = cv2.minMaxLoc(res)
|
||||
return correlation
|
||||
|
||||
@classmethod
|
||||
def draw_contours(cls, lhd: np.ndarray, rhd: np.ndarray) -> np.ndarray:
|
||||
view = rhd.copy()
|
||||
|
||||
lhd = cv2.cvtColor(lhd, cv2.COLOR_BGRA2GRAY)
|
||||
_, thresh = cv2.threshold(lhd, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
|
||||
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||
cv2.drawContours(view, contours, -1, (0, 0, 255), 1)
|
||||
|
||||
rhd = cv2.cvtColor(rhd, cv2.COLOR_BGRA2GRAY)
|
||||
_, thresh = cv2.threshold(rhd, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
|
||||
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||
cv2.drawContours(view, contours, -1, (0, 255, 0), 1)
|
||||
|
||||
return view
|
|
@ -0,0 +1,114 @@
|
|||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
|
||||
import allure
|
||||
import psutil
|
||||
|
||||
import configs
|
||||
from configs.system import IS_WIN
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
process_info = namedtuple('RunInfo', ['pid', 'name', 'create_time'])
|
||||
|
||||
|
||||
@allure.step('Find process by name')
|
||||
def find_process_by_name(process_name: str):
|
||||
processes = []
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
if process_name.lower().split('.')[0] == proc.name().lower().split('.')[0]:
|
||||
processes.append(process_info(
|
||||
proc.pid,
|
||||
proc.name(),
|
||||
datetime.fromtimestamp(proc.create_time()).strftime("%H:%M:%S.%f"))
|
||||
)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
return processes
|
||||
|
||||
|
||||
@allure.step('Kill process by name')
|
||||
def kill_process_by_name(process_name: str, verify: bool = True, timeout_sec: int = 10):
|
||||
_logger.info(f'Closing process: {process_name}')
|
||||
processes = find_process_by_name(process_name)
|
||||
for process in processes:
|
||||
try:
|
||||
os.kill(process.pid, signal.SIGILL if IS_WIN else signal.SIGKILL)
|
||||
except PermissionError as err:
|
||||
_logger.info(f'Close "{process}" error: {err}')
|
||||
if verify and processes:
|
||||
wait_for_close(process_name, timeout_sec)
|
||||
|
||||
|
||||
@allure.step('Wait for process start')
|
||||
def wait_for_started(process_name: str, timeout_sec: int = configs.timeouts.PROCESS_TIMEOUT_SEC):
|
||||
started_at = time.monotonic()
|
||||
while True:
|
||||
process = find_process_by_name(process_name)
|
||||
if process:
|
||||
_logger.info(f'Process started: {process_name}, start time: {process[0].create_time}')
|
||||
return process[0]
|
||||
time.sleep(1)
|
||||
_logger.debug(f'Waiting time: {int(time.monotonic() - started_at)} seconds')
|
||||
assert time.monotonic() - started_at < timeout_sec, f'Start process error: {process_name}'
|
||||
|
||||
|
||||
@allure.step('Wait for process close')
|
||||
def wait_for_close(process_name: str, timeout_sec: int = configs.timeouts.PROCESS_TIMEOUT_SEC):
|
||||
started_at = time.monotonic()
|
||||
while True:
|
||||
if not find_process_by_name(process_name):
|
||||
break
|
||||
time.sleep(1)
|
||||
assert time.monotonic() - started_at < timeout_sec, f'Close process error: {process_name}'
|
||||
_logger.info(f'Process closed: {process_name}')
|
||||
|
||||
|
||||
@allure.step('System execute command')
|
||||
def execute(
|
||||
command: list,
|
||||
shell=True,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
check=False
|
||||
):
|
||||
def _is_process_exists(_process) -> bool:
|
||||
return _process.poll() is None
|
||||
|
||||
def _wait_for_execution(_process):
|
||||
while _is_process_exists(_process):
|
||||
time.sleep(1)
|
||||
|
||||
def _get_output(_process):
|
||||
_wait_for_execution(_process)
|
||||
return _process.communicate()
|
||||
|
||||
command = " ".join(str(atr) for atr in command)
|
||||
_logger.info(f'Execute: {command}')
|
||||
process = subprocess.Popen(command, shell=shell, stderr=stderr, stdout=stdout)
|
||||
if check and process.returncode != 0:
|
||||
stdout, stderr = _get_output(process)
|
||||
raise RuntimeError(stderr)
|
||||
return process.pid
|
||||
|
||||
|
||||
@allure.step('System run command')
|
||||
def run(
|
||||
command: list,
|
||||
shell=True,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
timeout_sec=configs.timeouts.PROCESS_TIMEOUT_SEC,
|
||||
check=True
|
||||
):
|
||||
command = " ".join(str(atr) for atr in command)
|
||||
_logger.info(f'Execute: {command}')
|
||||
process = subprocess.run(command, shell=shell, stderr=stderr, stdout=stdout, timeout=timeout_sec)
|
||||
if check and process.returncode != 0:
|
||||
raise subprocess.CalledProcessError(process.returncode, command, process.stdout, process.stderr)
|
|
@ -3,6 +3,8 @@ import os
|
|||
import pathlib
|
||||
import shutil
|
||||
|
||||
import allure
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -10,5 +12,10 @@ class SystemPath(pathlib.Path):
|
|||
_accessor = pathlib._normal_accessor # noqa
|
||||
_flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour # noqa
|
||||
|
||||
@allure.step('Delete path')
|
||||
def rmtree(self, ignore_errors=False):
|
||||
shutil.rmtree(self, ignore_errors=ignore_errors)
|
||||
|
||||
@allure.step('Copy path')
|
||||
def copy_to(self, destination: 'SystemPath'):
|
||||
shutil.copytree(self, destination, dirs_exist_ok=True)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
from datetime import datetime
|
||||
|
||||
import allure
|
||||
import pytest
|
||||
|
||||
import configs
|
||||
from driver.aut import AUT
|
||||
from gui.main_window import MainWindow
|
||||
from scripts.utils import system_path
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def aut() -> AUT:
|
||||
if not configs.APP_DIR.exists():
|
||||
pytest.exit(f"Application not found: {configs.APP_DIR}")
|
||||
_aut = AUT()
|
||||
yield _aut
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_data(request) -> system_path.SystemPath:
|
||||
user_data = configs.testpath.STATUS_DATA / f'app_{datetime.now():%H%M%S_%f}' / 'data'
|
||||
if hasattr(request, 'param'):
|
||||
system_path.SystemPath(request.param).copy_to(user_data)
|
||||
yield user_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_window(aut: AUT, user_data):
|
||||
aut.launch(f'-d={user_data.parent}')
|
||||
yield MainWindow().wait_until_appears().prepare()
|
||||
aut.detach().stop()
|
|
@ -13,6 +13,13 @@ _logger = logging.getLogger(__name__)
|
|||
def generate_test_data(request):
|
||||
test_path, test_name, test_params = generate_test_info(request.node)
|
||||
configs.testpath.TEST = configs.testpath.RUN / test_path / test_name
|
||||
node_dir = configs.testpath.TEST / test_params
|
||||
configs.testpath.TEST_ARTIFACTS = node_dir / 'artifacts'
|
||||
configs.testpath.TEST_VP = configs.testpath.VP / test_path / test_name
|
||||
_logger.info(
|
||||
f'\nArtifacts directory:\t{configs.testpath.TEST_ARTIFACTS.relative_to(configs.testpath.ROOT)}'
|
||||
f'\nVerification points directory:\t{configs.testpath.TEST_VP.relative_to(configs.testpath.ROOT)}'
|
||||
)
|
||||
_logger.info(f'Start test: {test_name}')
|
||||
|
||||
|
||||
|
@ -25,7 +32,7 @@ def generate_test_info(node):
|
|||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def run_dir():
|
||||
def prepare_test_directory():
|
||||
keep_results = 5
|
||||
run_name_pattern = 'run_????????_??????'
|
||||
runs = list(sorted(configs.testpath.RESULTS.glob(run_name_pattern)))
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import pytest
|
||||
|
||||
from driver.server import SquishServer
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def start_squish_server():
|
||||
squish_server = SquishServer()
|
||||
squish_server.stop()
|
||||
attempt = 3
|
||||
while True:
|
||||
try:
|
||||
squish_server.start()
|
||||
break
|
||||
except AssertionError as err:
|
||||
attempt -= 1
|
||||
if not attempt:
|
||||
pytest.exit(err)
|
||||
yield squish_server
|
||||
squish_server.stop()
|
|
@ -0,0 +1,125 @@
|
|||
import logging
|
||||
import typing
|
||||
|
||||
import pytest
|
||||
from testrail_api import TestRailAPI
|
||||
|
||||
import configs
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
testrail_api = None
|
||||
|
||||
PASS = 1
|
||||
FAIL = 5
|
||||
RETEST = 4
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def init_testrail_api(request):
|
||||
global testrail_api
|
||||
if configs.testrail.TESTRAIL_RUN_ID:
|
||||
_logger.info('TestRail API initializing')
|
||||
testrail_api = TestRailAPI(
|
||||
configs.testrail.TESTRAIL_URL,
|
||||
configs.testrail.TESTRAIL_USER,
|
||||
configs.testrail.TESTRAIL_PWD
|
||||
)
|
||||
test_case_ids = get_test_ids_in_session(request)
|
||||
for test_case_id in test_case_ids:
|
||||
if is_test_case_in_run(test_case_id):
|
||||
_update_result(test_case_id, RETEST)
|
||||
_logger.info(f'Test: "{test_case_id}" marked as "Retest"')
|
||||
else:
|
||||
_logger.info(f'Report result for test case: {test_case_id} skipped, not in test run')
|
||||
else:
|
||||
_logger.info('TestRail report skipped')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def check_result(request):
|
||||
yield
|
||||
if configs.testrail.TESTRAIL_RUN_ID:
|
||||
item = request.node
|
||||
test_case_ids = _find_test_case_id_markers(request)
|
||||
for test_case_id in test_case_ids:
|
||||
if is_test_case_in_run(test_case_id):
|
||||
current_test_status = _get_test_case_status(test_case_id)
|
||||
if item.rep_call.failed:
|
||||
if current_test_status != FAIL:
|
||||
_update_result(test_case_id, FAIL)
|
||||
_update_comment(test_case_id, f"{request.node.name} FAILED")
|
||||
else:
|
||||
if current_test_status != FAIL:
|
||||
_update_result(test_case_id, PASS)
|
||||
_update_comment(test_case_id, f"{request.node.name} SUCCESS")
|
||||
|
||||
|
||||
def _update_result(test_case_id: int, result: int):
|
||||
testrail_api.results.add_result_for_case(
|
||||
run_id=configs.testrail.TESTRAIL_RUN_ID,
|
||||
case_id=test_case_id,
|
||||
status_id=result,
|
||||
)
|
||||
|
||||
|
||||
def _update_comment(test_case_id: int, comment: str):
|
||||
testrail_api.results.add_result_for_case(
|
||||
run_id=configs.testrail.TESTRAIL_RUN_ID,
|
||||
case_id=test_case_id,
|
||||
comment=comment
|
||||
)
|
||||
|
||||
|
||||
def _find_test_case_id_markers(request) -> typing.List[int]:
|
||||
for marker in request.node.own_markers:
|
||||
if marker.name == 'case':
|
||||
test_case_ids = marker.args
|
||||
return test_case_ids
|
||||
return []
|
||||
|
||||
|
||||
def _get_test_case_status(test_case_id: int) -> int:
|
||||
test_case_results = testrail_api.results.get_results_for_case(configs.testrail.TESTRAIL_RUN_ID, test_case_id)
|
||||
try:
|
||||
result = 0
|
||||
while True:
|
||||
last_test_case_status = test_case_results['results'][result]['status_id']
|
||||
if last_test_case_status is None:
|
||||
result += 1
|
||||
else:
|
||||
return last_test_case_status
|
||||
except:
|
||||
return RETEST
|
||||
|
||||
|
||||
def is_test_case_in_run(test_case_id: int) -> bool:
|
||||
try:
|
||||
testrail_api.results.get_results_for_case(configs.testrail.TESTRAIL_RUN_ID, test_case_id)
|
||||
except Exception as err:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _get_test_cases():
|
||||
results = []
|
||||
limit = 250
|
||||
chunk = 0
|
||||
while True:
|
||||
tests = testrail_api.tests.get_tests(configs.testrail.TESTRAIL_RUN_ID, offset=chunk)['tests']
|
||||
results.extend(tests)
|
||||
if len(tests) == limit:
|
||||
chunk += limit
|
||||
else:
|
||||
return results
|
||||
|
||||
|
||||
def get_test_ids_in_session(request):
|
||||
tests = request.session.items
|
||||
ids = []
|
||||
for test in tests:
|
||||
for marker in getattr(test, 'own_markers', []):
|
||||
if getattr(marker, 'name', '') == 'case':
|
||||
ids.extend(list(marker.args))
|
||||
return set(ids)
|
|
@ -0,0 +1,95 @@
|
|||
import logging
|
||||
import allure
|
||||
import pytest
|
||||
from allure import step
|
||||
|
||||
import configs.timeouts
|
||||
import driver
|
||||
from gui.components.before_started_popup import BeforeStartedPopUp
|
||||
from gui.components.profile_picture_popup import shift_image
|
||||
from gui.components.splash_screen import SplashScreen
|
||||
from gui.components.welcome_status_popup import WelcomeStatusPopup
|
||||
from gui.screens.onboarding import AllowNotificationsView, WelcomeScreen, TouchIDAuthView
|
||||
from scripts.tools import image
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
pytestmark = allure.suite("Onboarding")
|
||||
|
||||
|
||||
@allure.testcase('https://ethstatus.testrail.net/index.php?/cases/view/703421', 'Generate new keys')
|
||||
@pytest.mark.case(703421)
|
||||
@pytest.mark.parametrize('user_name, password, user_image', [
|
||||
pytest.param('Test-User _1', '*P@ssw0rd*', None),
|
||||
pytest.param('_1Test-User', '*P@ssw0rd*', 'tv_signal.jpeg', marks=pytest.mark.smoke),
|
||||
pytest.param('Test-User', '*P@ssw0rd*', 'tv_signal.png'),
|
||||
])
|
||||
def test_generate_new_keys(main_window, user_name, password, user_image: str):
|
||||
with step('Open Generate new keys view'):
|
||||
if configs.system.IS_MAC:
|
||||
AllowNotificationsView().wait_until_appears().allow()
|
||||
BeforeStartedPopUp().get_started()
|
||||
wellcome_screen = WelcomeScreen().wait_until_appears()
|
||||
keys_screen = wellcome_screen.get_keys()
|
||||
|
||||
with step(f'Setup profile with name: {user_name} and image: {user_image}'):
|
||||
profile_view = keys_screen.generate_new_keys()
|
||||
profile_view.set_display_name(user_name)
|
||||
if user_image is not None:
|
||||
profile_picture_popup = profile_view.set_user_image(configs.testpath.TEST_FILES / user_image)
|
||||
profile_picture_popup.make_profile_picture(zoom=5, shift=shift_image(0, 200, 200, 0))
|
||||
assert not profile_view.error_message
|
||||
|
||||
with step('Open Profile details view'):
|
||||
details_view = profile_view.next()
|
||||
|
||||
with step('Verify Profile details'):
|
||||
if user_image is None:
|
||||
assert not details_view.is_user_image_background_white()
|
||||
assert driver.waitFor(
|
||||
lambda: details_view.is_user_image_contains(user_name[:2]),
|
||||
configs.timeouts.UI_LOAD_TIMEOUT_MSEC
|
||||
)
|
||||
else:
|
||||
image.compare(
|
||||
details_view.cropped_profile_image,
|
||||
configs.testpath.TEST_VP / f'user_image_onboarding.png',
|
||||
)
|
||||
|
||||
chat_key = details_view.chat_key
|
||||
emoji_hash = details_view.emoji_hash
|
||||
assert details_view.is_identicon_ring_visible
|
||||
|
||||
with step('Finalize onboarding and prepare main screen'):
|
||||
create_password_view = details_view.next()
|
||||
assert not create_password_view.is_create_password_button_enabled
|
||||
confirm_password_view = create_password_view.create_password(password)
|
||||
confirm_password_view.confirm_password(password)
|
||||
if configs.system.IS_MAC:
|
||||
TouchIDAuthView().wait_until_appears().prefer_password()
|
||||
SplashScreen().wait_until_appears().wait_until_hidden()
|
||||
WelcomeStatusPopup().confirm()
|
||||
|
||||
with step('Open User Canvas and verify profile'):
|
||||
user_canvas = main_window.left_panel.open_user_canvas()
|
||||
assert user_canvas.user_name == user_name
|
||||
if user_image is None:
|
||||
assert driver.waitFor(
|
||||
lambda: user_canvas.is_user_image_contains(user_name[:2]),
|
||||
configs.timeouts.UI_LOAD_TIMEOUT_MSEC
|
||||
)
|
||||
|
||||
with step('Open Profile popup and verify profile'):
|
||||
profile_popup = user_canvas.open_profile_popup()
|
||||
assert profile_popup.user_name == user_name
|
||||
assert profile_popup.chat_key == chat_key
|
||||
assert profile_popup.emoji_hash.compare(emoji_hash.view)
|
||||
if user_image is None:
|
||||
assert driver.waitFor(
|
||||
lambda: profile_popup.is_user_image_contains(user_name[:2]),
|
||||
configs.timeouts.UI_LOAD_TIMEOUT_MSEC
|
||||
)
|
||||
else:
|
||||
image.compare(
|
||||
profile_popup.cropped_profile_image,
|
||||
'user_image_profile.png',
|
||||
)
|
|
@ -1,13 +1,12 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
import allure
|
||||
|
||||
import driver
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
pytestmark = allure.suite("Self")
|
||||
|
||||
|
||||
@pytest.mark.self
|
||||
def test_import_squish():
|
||||
_logger.info(str(driver.__dict__))
|
||||
driver.snooze(1)
|
||||
def test_start_aut(main_window):
|
||||
driver.context.detach()
|
||||
|
|
Loading…
Reference in New Issue