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:
Vladimir Druzhinin 2023-08-04 20:27:03 +02:00 committed by GitHub
parent e828d2c1de
commit 5959897498
81 changed files with 2544 additions and 192 deletions

157
README.md
View File

@ -1,153 +1,4 @@
# Status desktop ui-tests
# Setup:
Skip any of the steps, if sure that you have the correct version of the required tool.
## All Platforms
### 1. Install Qt 5.15
https://doc.qt.io/qt-6/get-and-install-qt.html
### 2. Setup Squish License Server
https://hackmd.io/@status-desktop/HkbWpk2e5
### 3. Install PyCharm
Download and install:
https://www.jetbrains.com/pycharm/download/other.html
Please, select any build depending on OS, but NOT an Apple Silicon (dmg)
How to: https://www.jetbrains.com/help/pycharm/installation-guide.html
## Windows
### 4. Install Squish
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230301-1424-qt515x-win64-msvc142.exe
### 5. Install Python
Download and install for all users: https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe
### 6. Install Requirements
```
YOUR_PYTHON_PATH/pip3.exe install -r ./requirements.txt
```
### 7. Setup Environment Variables
Add in system environment variables:
```
SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
PYTHONPATH=%SQUISH_DIR%/lib;%SQUISH_DIR%/lib/python;%PYTHONPATH%
```
RESTART PC
### 8. Verify environment variables
```
echo %SQUISH_DIR%
echo %PYTHONPATH%
```
### 9. Setup Python for Squish
Download 'PythonChanger.py' in %SQUISH_DIR%:
https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/PythonChanger.py
```
YOUR_PYTHON_PATH/python3.10 SQUISH_DIR/PythonChanger.py --revert
YOUR_PYTHON_PATH/python3.10 SQUISH_DIR/PythonChanger.py
```
- Replace "YOUR PYTHON PATH" on to Python3.10 file location path
- Replace "SQUISH DIR" on to the Squish root folder path
### 10 Test:
Executing tests located in 'test_self.py' file
```
pytest ./tests/test_self.py
```
Executing test 'test_import_squish' from 'test_self.py' file
```
pytest ./tests/test_self.py::test_import_squish
```
Executing all tests with 'import_squish' in test name
```
pytest -k import_squish
```
Executing all tests with tag 'self'
```
pytest -m self
```
## Linux
### 4. Install Squish
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230222-1555-qt515x-linux64.run
### 5. Install Python
```bash
sudo apt-get install software-properties-common
```
```bash
sudo add-apt-repository ppa:deadsnakes/ppa
```
```bash
sudo apt-get update
```
```bash
sudo apt-get install python3.10
```
```bash
sudo apt install python3-pip
```
### 6. Install Requirements
```bash
sudo pip3 install -r ./requirements.txt
```
### 7. Setup Environment Variables
```bash
gedit ~/.profile
```
```
export SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
export PYTHONPATH=$SQUISH_DIR/lib:$SQUISH_DIR/lib/python:$PYTHONPATH
export LD_LIBRARY_PATH=$SQUISH_DIR/lib:$SQUISH_DIR/python3/lib:$LD_LIBRARY_PATH
```
RESTART PC
## Mac
### 4. Install Squish
https://status-misc.ams3.digitaloceanspaces.com/squish/squish-7.1-20230328-1608-qt515x-macaarch64.dmg
### 5. Install Python
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
```bash
brew update --auto-update
brew install wget
brew install python@3.10
```
### 6. Install Requirements
```bash
sudo pip3 install -r ./requirements.txt
```
### 7. Setup Environment Variables
```bash
touch ~/.zprofile
open ~/.zprofile
```
```
export SQUISH_DIR=PATH_TO_THE_SQUISH_ROOT_FOLDER
export PYTHONPATH=$SQUISH_DIR/lib:$SQUISH_DIR/lib/python:$PYTHONPATH
export LD_LIBRARY_PATH=$SQUISH_DIR/lib:$LD_LIBRARY_PATH
```
RESTART PC
## Linux or MAC:
### 8. Verify environment variables
```bash
echo $USERNAME
echo $PYTHONPATH
echo $LD_LIBRARY_PATH
```
### 9. Setup Python for Squish
https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/
```bash
brew install wget
wget -O $SQUISH_DIR/PythonChanger.py https://kb.froglogic.com/squish/howto/changing-python-installation-used-squish-binary-packages/PythonChanger.py
python3.10 $SQUISH_DIR/PythonChanger.py --revert
python3.10 $SQUISH_DIR/PythonChanger.py
```
### 10 Test:
```bash
echo "Executing tests located in 'test_self.py' file"
pytest ./tests/test_self.py
echo "Executing test 'test_import_squish' from 'test_self.py' file"
pytest ./tests/test_self.py::test_import_squish
echo "Executing all tests with 'import_squish' in test name"
pytest -k import_squish
echo "Executing all tests with tag 'self'"
pytest -m self
```
For more info, read: https://docs.pytest.org/en/latest/getting-started.html
# Status desktop ui-tests
Setup:
https://www.notion.so/Setup-Environment-e5d88399027042a0992e85fd9b0e5167?pvs=4

View File

@ -1,14 +1,21 @@
import logging
from . import testpath, timeouts
_logger = logging.getLogger(__name__)
try:
from ._local import *
except ImportError:
exit(
'Config file: "_local.py" not found in "./configs".\n'
'Please use template "_.local.py.default" to create file or execute command: \n'
rf'cp {testpath.ROOT}/configs/_local.py.default {testpath.ROOT}/configs/_local.py'
)
import logging
from scripts.utils.system_path import SystemPath
from . import testpath, timeouts, testrail, system
_logger = logging.getLogger(__name__)
try:
from ._local import *
except ImportError:
exit(
'Config file: "_local.py" not found in "./configs".\n'
'Please use template "_.local.py.default" to create file or execute command: \n'
rf'cp {testpath.ROOT}/configs/_local.py.default {testpath.ROOT}/configs/_local.py'
)
if APP_DIR is None:
exit('Please add "APP_DIR" in ./configs/_local.py')
if system.IS_WIN and 'bin' not in APP_DIR:
exit('Please use launcher from "bin" folder in "APP_DIR"')
APP_DIR = SystemPath(APP_DIR)

View File

@ -1,7 +1,5 @@
import os
from scripts.utils.system_path import SystemPath
LOCAL_RUN = False
APP_DIR = SystemPath(os.getenv('APP_DIR')
ATTACH_MODE = False
APP_DIR = os.getenv('APP_DIR')

View File

@ -1,3 +1,3 @@
LOCAL_RUN = True
ATTACH_MODE = True
APP_DIR = None

7
configs/system.py Normal file
View File

@ -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'

View File

@ -1,15 +1,27 @@
import os
from datetime import datetime
import typing
from scripts.utils.system_path import SystemPath
ROOT: SystemPath = SystemPath(__file__).resolve().parent.parent
# Runtime initialisation
TEST: typing.Optional[SystemPath] = None
TEST_VP: typing.Optional[SystemPath] = None
TEST_ARTIFACTS: typing.Optional[SystemPath] = None
# Test Directories
RUN_ID = os.getenv('RUN_DIR', f'run_{datetime.now():%d%m%Y_%H%M%S}')
TEMP: SystemPath = ROOT / 'tmp'
RESULTS: SystemPath = TEMP / 'results'
RUN: SystemPath = RESULTS / RUN_ID
VP: SystemPath = ROOT / 'ext' / 'vp'
TEST_FILES: SystemPath = ROOT / 'ext' / 'test_files'
# Driver Directories
SQUISH_DIR = os.getenv('RUN_DIR')
SQUISH_DIR = SystemPath(os.getenv('SQUISH_DIR'))
# Status Application
STATUS_DATA: SystemPath = RUN / 'status'

6
configs/testrail.py Normal file
View File

@ -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)

View File

@ -1,3 +1,6 @@
# Timoeuts before raising errors
UI_LOAD_TIMEOUT_MSEC = 5000
UI_LOAD_TIMEOUT_SEC = 5
UI_LOAD_TIMEOUT_MSEC = UI_LOAD_TIMEOUT_SEC * 1000
PROCESS_TIMEOUT_SEC = 10
APP_LOAD_TIMEOUT_MSEC = 60000

View File

@ -1,17 +1,63 @@
import logging
from datetime import datetime
import allure
import pytest
from PIL import ImageGrab
import configs
from driver.aut import AUT
from scripts.utils.system_path import SystemPath
from tests.fixtures.path import generate_test_info
_logger = logging.getLogger(__name__)
pytest_plugins = [
'tests.fixtures.aut',
'tests.fixtures.path',
'tests.fixtures.squish',
'tests.fixtures.testrail',
]
@pytest.fixture(scope='session', autouse=True)
def setup_session_scope(
run_dir,
init_testrail_api,
prepare_test_directory,
start_squish_server,
):
yield
@pytest.fixture(autouse=True)
def setup_function_scope(
generate_test_data,
check_result
):
yield
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, 'rep_' + rep.when, rep)
def pytest_exception_interact(node):
"""Handles test on fail."""
pass
try:
test_path, test_name, test_params = generate_test_info(node)
node_dir: SystemPath = configs.testpath.RUN / test_path / test_name / test_params
node_dir.mkdir(parents=True, exist_ok=True)
screenshot = node_dir / 'screenshot.png'
if screenshot.exists():
screenshot = node_dir / f'screenshot_{datetime.now():%H%M%S}.png'
ImageGrab.grab().save(screenshot)
allure.attach(
name='Screenshot on fail',
body=screenshot.read_bytes(),
attachment_type=allure.attachment_type.PNG)
AUT().stop()
except Exception as ex:
_logger.debug(ex)

View File

@ -0,0 +1,4 @@
from . import commands
from .colors import *
from .tesseract import *
from .user import *

49
constants/colors.py Normal file
View File

@ -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]
]
}

13
constants/commands.py Normal file
View File

@ -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'

30
constants/tesseract.py Normal file
View File

@ -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'

7
constants/user.py Normal file
View File

@ -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')

View File

@ -1,16 +1,27 @@
import squishtest # noqa
import configs
from . import server, context, objects_access, toplevel_window, aut, atomacos, mouse
imports = {module.__name__: module for module in [
# import any modules from driver folder
atomacos,
aut,
context,
objects_access,
mouse,
server,
toplevel_window
]}
def __getattr__(name):
if name in imports:
return imports[name]
return getattr(squishtest, name)
try:
return getattr(squishtest, name)
except AttributeError:
raise ImportError(f'Module "driver" has no attribute "{name}"')
squishtest.testSettings.waitForObjectTimeout = configs.timeouts.UI_LOAD_TIMEOUT_MSEC

45
driver/atomacos.py Normal file
View File

@ -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}')

78
driver/aut.py Normal file
View File

@ -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

31
driver/context.py Normal file
View File

@ -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')

44
driver/mouse.py Executable file
View File

@ -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)

12
driver/objects_access.py Normal file
View File

@ -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)

51
driver/server.py Normal file
View File

@ -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)])

52
driver/toplevel_window.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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()

View File

View File

View File

@ -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()

View File

View File

@ -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()

View File

@ -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

View File

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -1 +0,0 @@

View File

@ -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)

View File

View File

View File

@ -0,0 +1,10 @@
import allure
from .object import NativeObject
class Button(NativeObject):
@allure.step('Click {0}')
def click(self):
self.object.Press()

View File

@ -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()

View File

@ -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)

View File

View File

@ -0,0 +1,10 @@
import allure
from .object import NativeObject
class Button(NativeObject):
@allure.step('Click {0}')
def click(self):
super().click()

View File

@ -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)

View File

@ -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

View File

21
gui/elements/qt/button.py Normal file
View File

@ -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)

View File

@ -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'

43
gui/elements/qt/list.py Normal file
View File

@ -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}')

118
gui/elements/qt/object.py Normal file
View File

@ -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)

33
gui/elements/qt/slider.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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)

40
gui/elements/qt/window.py Normal file
View File

@ -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)

View File

@ -1,9 +1,46 @@
import logging
import allure
from gui.components.user_canvas import UserCanvas
from gui.elements.qt.button import Button
from gui.elements.qt.object import QObject
from gui.elements.qt.window import Window
_logger = logging.getLogger(__name__)
class MainWindow:
class LeftPanel(QObject):
def __init__(self):
pass
super(LeftPanel, self).__init__('mainWindow_StatusAppNavBar')
self._profile_button = Button('mainWindow_ProfileNavBarButton')
@property
@allure.step('Get user badge color')
def user_badge_color(self) -> str:
return str(self._profile_button.object.badge.color.name)
@allure.step('Open user canvas')
def open_user_canvas(self) -> UserCanvas:
self._profile_button.click()
return UserCanvas().wait_until_appears()
@allure.step('Verify: User is online')
def user_is_online(self) -> bool:
return self.user_badge_color == '#4ebc60'
@allure.step('Verify: User is offline')
def user_is_offline(self):
return self.user_badge_color == '#7f8990'
@allure.step('Verify: User is set to automatic')
def user_is_set_to_automatic(self):
return self.user_badge_color == '#4ebc60'
class MainWindow(Window):
def __init__(self):
super(MainWindow, self).__init__('statusDesktop_mainWindow')
self.left_panel = LeftPanel()

View File

@ -1,5 +1,4 @@
from .component_names import *
from .main_window_names import *
from .messages_names import *
from .main_names import *
from .onboarding_names import *
from .settings_names import *
from .os_names import *

View File

@ -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}

View File

@ -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}

View File

@ -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"}

View File

@ -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}

249
gui/screens/onboarding.py Executable file
View File

@ -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()

View File

@ -3,5 +3,7 @@ log_format = %(asctime)s.%(msecs)03d %(levelname)7s %(name)s %(message).5000s
log_cli = true
log_cli_level = INFO
addopts = --disable-warnings
markers =
self: framework tests
smoke: Smoke tests

View File

@ -1 +1,9 @@
pytest==7.4.0
psutil==5.9.5
pillow==10.0.0
opencv-python-headless==4.8.0.74
numpy~=1.25.1
pytesseract==0.3.10
atomacos==3.3.0; platform_system == "Darwin"
allure-pytest==2.13.2
testrail-api==1.12.0

224
scripts/tools/image.py Executable file
View File

@ -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')

27
scripts/tools/ocv.py Executable file
View File

@ -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

View File

@ -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)

View File

@ -3,6 +3,8 @@ import os
import pathlib
import shutil
import allure
_logger = logging.getLogger(__name__)
@ -10,5 +12,10 @@ class SystemPath(pathlib.Path):
_accessor = pathlib._normal_accessor # noqa
_flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour # noqa
@allure.step('Delete path')
def rmtree(self, ignore_errors=False):
shutil.rmtree(self, ignore_errors=ignore_errors)
@allure.step('Copy path')
def copy_to(self, destination: 'SystemPath'):
shutil.copytree(self, destination, dirs_exist_ok=True)

32
tests/fixtures/aut.py vendored Normal file
View File

@ -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()

View File

@ -13,6 +13,13 @@ _logger = logging.getLogger(__name__)
def generate_test_data(request):
test_path, test_name, test_params = generate_test_info(request.node)
configs.testpath.TEST = configs.testpath.RUN / test_path / test_name
node_dir = configs.testpath.TEST / test_params
configs.testpath.TEST_ARTIFACTS = node_dir / 'artifacts'
configs.testpath.TEST_VP = configs.testpath.VP / test_path / test_name
_logger.info(
f'\nArtifacts directory:\t{configs.testpath.TEST_ARTIFACTS.relative_to(configs.testpath.ROOT)}'
f'\nVerification points directory:\t{configs.testpath.TEST_VP.relative_to(configs.testpath.ROOT)}'
)
_logger.info(f'Start test: {test_name}')
@ -25,7 +32,7 @@ def generate_test_info(node):
@pytest.fixture(scope='session')
def run_dir():
def prepare_test_directory():
keep_results = 5
run_name_pattern = 'run_????????_??????'
runs = list(sorted(configs.testpath.RESULTS.glob(run_name_pattern)))

20
tests/fixtures/squish.py vendored Normal file
View File

@ -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()

125
tests/fixtures/testrail.py vendored Normal file
View File

@ -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)

95
tests/test_onboarding.py Executable file
View File

@ -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',
)

View File

@ -1,13 +1,12 @@
import logging
import pytest
import allure
import driver
_logger = logging.getLogger(__name__)
pytestmark = allure.suite("Self")
@pytest.mark.self
def test_import_squish():
_logger.info(str(driver.__dict__))
driver.snooze(1)
def test_start_aut(main_window):
driver.context.detach()