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
|
# ui-tests
|
||||||
/test/ui-pytest/configs/_local.py
|
/test/ui-pytest/configs/_local.py
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
test/ui-pytest/squish_server.ini
|
||||||
|
|
|
@ -1,153 +1,4 @@
|
||||||
# Status desktop ui-tests
|
# Status desktop ui-tests
|
||||||
|
|
||||||
# Setup:
|
Setup:
|
||||||
Skip any of the steps, if sure that you have the correct version of the required tool.
|
https://www.notion.so/Setup-Environment-e5d88399027042a0992e85fd9b0e5167?pvs=4
|
||||||
## 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
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from . import testpath, timeouts
|
from scripts.utils.system_path import SystemPath
|
||||||
|
from . import testpath, timeouts, testrail, system
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -12,3 +13,9 @@ except ImportError:
|
||||||
'Please use template "_.local.py.default" to create file or execute command: \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'
|
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
|
import os
|
||||||
|
|
||||||
from scripts.utils.system_path import SystemPath
|
|
||||||
|
|
||||||
LOCAL_RUN = False
|
LOCAL_RUN = False
|
||||||
|
ATTACH_MODE = False
|
||||||
APP_DIR = SystemPath(os.getenv('APP_DIR')
|
APP_DIR = os.getenv('APP_DIR')
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
LOCAL_RUN = True
|
LOCAL_RUN = True
|
||||||
|
ATTACH_MODE = True
|
||||||
APP_DIR = None
|
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
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import typing
|
||||||
|
|
||||||
from scripts.utils.system_path import SystemPath
|
from scripts.utils.system_path import SystemPath
|
||||||
|
|
||||||
ROOT: SystemPath = SystemPath(__file__).resolve().parent.parent
|
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
|
# Test Directories
|
||||||
RUN_ID = os.getenv('RUN_DIR', f'run_{datetime.now():%d%m%Y_%H%M%S}')
|
RUN_ID = os.getenv('RUN_DIR', f'run_{datetime.now():%d%m%Y_%H%M%S}')
|
||||||
TEMP: SystemPath = ROOT / 'tmp'
|
TEMP: SystemPath = ROOT / 'tmp'
|
||||||
RESULTS: SystemPath = TEMP / 'results'
|
RESULTS: SystemPath = TEMP / 'results'
|
||||||
RUN: SystemPath = RESULTS / RUN_ID
|
RUN: SystemPath = RESULTS / RUN_ID
|
||||||
|
VP: SystemPath = ROOT / 'ext' / 'vp'
|
||||||
|
TEST_FILES: SystemPath = ROOT / 'ext' / 'test_files'
|
||||||
|
|
||||||
# Driver Directories
|
# 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
|
# 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
|
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 = [
|
pytest_plugins = [
|
||||||
|
'tests.fixtures.aut',
|
||||||
'tests.fixtures.path',
|
'tests.fixtures.path',
|
||||||
|
'tests.fixtures.squish',
|
||||||
|
'tests.fixtures.testrail',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session', autouse=True)
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
def setup_session_scope(
|
def setup_session_scope(
|
||||||
run_dir,
|
init_testrail_api,
|
||||||
|
prepare_test_directory,
|
||||||
|
start_squish_server,
|
||||||
):
|
):
|
||||||
yield
|
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):
|
def pytest_exception_interact(node):
|
||||||
"""Handles test on fail."""
|
try:
|
||||||
pass
|
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 squishtest # noqa
|
||||||
|
|
||||||
import configs
|
import configs
|
||||||
|
from . import server, context, objects_access, toplevel_window, aut, atomacos, mouse
|
||||||
|
|
||||||
imports = {module.__name__: module for module in [
|
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):
|
def __getattr__(name):
|
||||||
if name in imports:
|
if name in imports:
|
||||||
return imports[name]
|
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
|
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 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__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MainWindow:
|
class LeftPanel(QObject):
|
||||||
|
|
||||||
def __init__(self):
|
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 .component_names import *
|
||||||
from .main_window_names import *
|
from .main_names import *
|
||||||
from .messages_names import *
|
|
||||||
from .onboarding_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 = true
|
||||||
log_cli_level = INFO
|
log_cli_level = INFO
|
||||||
|
|
||||||
|
addopts = --disable-warnings
|
||||||
|
|
||||||
markers =
|
markers =
|
||||||
self: framework tests
|
smoke: Smoke tests
|
||||||
|
|
|
@ -1 +1,9 @@
|
||||||
pytest==7.4.0
|
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 pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
import allure
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,5 +12,10 @@ class SystemPath(pathlib.Path):
|
||||||
_accessor = pathlib._normal_accessor # noqa
|
_accessor = pathlib._normal_accessor # noqa
|
||||||
_flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour # noqa
|
_flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour # noqa
|
||||||
|
|
||||||
|
@allure.step('Delete path')
|
||||||
def rmtree(self, ignore_errors=False):
|
def rmtree(self, ignore_errors=False):
|
||||||
shutil.rmtree(self, ignore_errors=ignore_errors)
|
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):
|
def generate_test_data(request):
|
||||||
test_path, test_name, test_params = generate_test_info(request.node)
|
test_path, test_name, test_params = generate_test_info(request.node)
|
||||||
configs.testpath.TEST = configs.testpath.RUN / test_path / test_name
|
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}')
|
_logger.info(f'Start test: {test_name}')
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +32,7 @@ def generate_test_info(node):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def run_dir():
|
def prepare_test_directory():
|
||||||
keep_results = 5
|
keep_results = 5
|
||||||
run_name_pattern = 'run_????????_??????'
|
run_name_pattern = 'run_????????_??????'
|
||||||
runs = list(sorted(configs.testpath.RESULTS.glob(run_name_pattern)))
|
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 logging
|
||||||
|
|
||||||
import pytest
|
import allure
|
||||||
|
|
||||||
import driver
|
import driver
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
pytestmark = allure.suite("Self")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.self
|
def test_start_aut(main_window):
|
||||||
def test_import_squish():
|
driver.context.detach()
|
||||||
_logger.info(str(driver.__dict__))
|
|
||||||
driver.snooze(1)
|
|
||||||
|
|
Loading…
Reference in New Issue