Merge pull request #101 from status-im/develop

Develop
This commit is contained in:
Vladimir Druzhinin 2023-09-11 17:15:14 +02:00 committed by GitHub
commit 8ebc0143d4
142 changed files with 5119 additions and 0 deletions

View File

@ -1 +1,3 @@
# desktop-qa-automation
https://www.notion.so/Setup-Environment-e5d88399027042a0992e85fd9b0e5167?pvs=4

View File

@ -0,0 +1,25 @@
import logging
from scripts.utils.system_path import SystemPath
from . import testpath, timeouts, testrail, squish, 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)
# Application will be stuck after the first test execution if set to False
# We need to investigate more time on it.
ATTACH_MODE = True

View File

@ -0,0 +1,7 @@
import logging
import os
LOG_LEVEL = logging.DEBUG
DEV_BUILD = False
APP_DIR = os.getenv('APP_DIR')

View File

@ -0,0 +1,6 @@
import logging
LOG_LEVEL = logging.DEBUG
DEV_BUILD = False
APP_DIR = None

View File

@ -0,0 +1 @@
AUT_PORT = 61500

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

@ -0,0 +1,27 @@
import os
import typing
from datetime import datetime
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'
TEST_USER_DATA: SystemPath = ROOT / 'ext' / 'user_data'
# Driver Directories
SQUISH_DIR = SystemPath(os.getenv('SQUISH_DIR'))
# Status Application
STATUS_DATA: SystemPath = RUN / 'status'

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

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

66
test/e2e/conftest.py Normal file
View File

@ -0,0 +1,66 @@
import logging
from datetime import datetime
import allure
import pytest
from PIL import ImageGrab
import configs
import driver
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(
init_testrail_api,
prepare_test_directory,
start_squish_server,
):
yield
@pytest.fixture(autouse=True)
def setup_function_scope(
caplog,
generate_test_data,
check_result
):
caplog.set_level(configs.LOG_LEVEL)
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):
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)
driver.context.detach()
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 *

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

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'

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'

View File

@ -0,0 +1,25 @@
from collections import namedtuple
import configs
UserAccount = namedtuple('User', ['name', 'password', 'seed_phrase'])
user_account_default = UserAccount('squisher', '*P@ssw0rd*', [
'rail', 'witness', 'era', 'asthma', 'empty', 'cheap', 'shed', 'pond', 'skate', 'amount', 'invite', 'year'
])
user_account_one = UserAccount('tester123', 'TesTEr16843/!@00', [])
user_account_two = UserAccount('Athletic', 'TesTEr16843/!@00', [])
user_account_three = UserAccount('Nervous', 'TesTEr16843/!@00', [])
default_community_params = {
'name': 'Name',
'description': 'Description',
'logo': {'fp': configs.testpath.TEST_FILES / 'tv_signal.png', 'zoom': None, 'shift': None},
'banner': {'fp': configs.testpath.TEST_FILES / 'banner.png', 'zoom': None, 'shift': None},
'intro': 'Intro',
'outro': 'Outro'
}
UserCommunityInfo = namedtuple('CommunityInfo', ['name', 'description', 'members', 'image'])
UserChannel = namedtuple('Channel', ['name', 'image', 'selected'])
account_list_item = namedtuple('AccountListItem', ['name', 'color', 'emoji'])

View File

@ -0,0 +1,9 @@
from enum import Enum
class DerivationPath(Enum):
CUSTOM = 'Custom'
ETHEREUM = 'Ethereum'
ETHEREUM_ROPSTEN = 'Ethereum Testnet (Ropsten)'
ETHEREUM_LEDGER = 'Ethereum (Ledger)'
ETHEREUM_LEDGER_LIVE = 'Ethereum (Ledger Live/KeepKey)'

28
test/e2e/driver/__init__.py Executable file
View File

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

View File

@ -0,0 +1,63 @@
import time
from copy import deepcopy
import configs.timeouts
from scripts.utils import local_system
if configs.system.IS_MAC:
from atomacos._a11y import _running_apps_with_bundle_id
import atomacos
BUNDLE_ID = 'im.Status.NimStatusClient'
# https://pypi.org/project/atomacos/
def attach_atomac(timeout_sec: int = configs.timeouts.UI_LOAD_TIMEOUT_SEC):
def from_bundle_id(bundle_id):
"""
Get the top level element for the application with the specified
bundle ID, such as com.vmware.fusion.
"""
apps = _running_apps_with_bundle_id(bundle_id)
if not apps:
raise ValueError(
"Specified bundle ID not found in " "running apps: %s" % bundle_id
)
return atomacos.NativeUIElement.from_pid(apps[-1].processIdentifier())
if configs.DEV_BUILD:
pid = local_system.find_process_by_port(configs.squish.AUT_PORT)
atomator = atomacos.getAppRefByPid(pid)
else:
atomator = from_bundle_id(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}')

75
test/e2e/driver/aut.py Normal file
View File

@ -0,0 +1,75 @@
import allure
import squish
import configs
import driver
from configs.system import 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 = configs.squish.AUT_PORT
):
super(AUT, self).__init__()
self.path = app_path
self.host = host
self.port = int(port)
self.ctx = None
self.pid = None
self.aut_id = self.path.name if IS_LIN else self.path.stem
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')
def stop(self):
local_system.kill_process(self.pid)
@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)
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()
self.pid = self.ctx.pid
assert squish.waitFor(lambda: self.ctx.isRunning, configs.timeouts.PROCESS_TIMEOUT_SEC)
return self

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
test/e2e/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)

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)

52
test/e2e/driver/server.py Normal file
View File

@ -0,0 +1,52 @@
import logging
import typing
from subprocess import CalledProcessError
import configs.testpath
from scripts.utils import local_system
_PROCESS_NAME = '_squishserver'
_logger = logging.getLogger(__name__)
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
self.pid = None
def start(self):
cmd = [
f'"{self.path}"',
'--configfile', str(self.config),
f'--host={self.host}',
f'--port={self.port}',
]
self.pid = local_system.execute(cmd)
def stop(self):
if self.pid is not None:
local_system.kill_process(self.pid)
self.pid = None
# 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)])

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: 57 KiB

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

0
test/e2e/gui/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,25 @@
import allure
from gui.elements.qt.button import Button
from gui.elements.qt.text_edit import TextEdit
from gui.elements.qt.object import QObject
class AuthenticatePopup(QObject):
def __init__(self):
super(AuthenticatePopup, self).__init__('contextMenu_PopupItem')
self._password_text_edit = TextEdit('sharedPopup_Password_Input')
self._primary_button = Button('sharedPopup_Primary_Button')
@allure.step('Authenticate action with password')
def authenticate(self, password: str, attempt: int = 2):
self._password_text_edit.text = password
self._primary_button.click()
try:
self._primary_button.wait_until_hidden()
except AssertionError as err:
if attempt:
self.authenticate(password, attempt - 1)
else:
raise err

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,29 @@
import allure
import configs
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
from gui.elements.qt.text_edit import TextEdit
class ColorSelectPopup(BasePopup):
def __init__(self):
super().__init__()
self._hex_color_text_edit = TextEdit('communitySettings_ColorPanel_HexColor_Input')
self._save_button = Button('communitySettings_SaveColor_Button')
@allure.step('Wait until appears {0}')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._hex_color_text_edit.wait_until_appears()
return self
@allure.step('Wait until hidden {0}')
def wait_until_hidden(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._hex_color_text_edit.wait_until_hidden()
@allure.step('Select color {1}')
def select_color(self, value: str):
self._hex_color_text_edit.text = value
self._save_button.click()
self.wait_until_hidden()

View File

@ -0,0 +1,44 @@
import configs
from gui.components.base_popup import BasePopup
from gui.components.emoji_popup import EmojiPopup
from gui.elements.qt.button import Button
from gui.elements.qt.text_edit import TextEdit
class ChannelPopup(BasePopup):
def __init__(self):
super(ChannelPopup, self).__init__()
self._name_text_edit = TextEdit('createOrEditCommunityChannelNameInput_TextEdit')
self._description_text_edit = TextEdit('createOrEditCommunityChannelDescriptionInput_TextEdit')
self._save_create_button = Button('createOrEditCommunityChannelBtn_StatusButton')
self._emoji_button = Button('createOrEditCommunityChannel_EmojiButton')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._name_text_edit.wait_until_appears(timeout_msec)
return self
class NewChannelPopup(ChannelPopup):
def create(self, name: str, description: str, emoji: str = None):
self._name_text_edit.text = name
self._description_text_edit.text = description
if emoji is not None:
self._emoji_button.click()
EmojiPopup().wait_until_appears().select(emoji)
self._save_create_button.click()
self.wait_until_hidden()
class EditChannelPopup(ChannelPopup):
def edit(self, name: str, description: str = None, emoji: str = None):
self._name_text_edit.text = name
if description is not None:
self._description_text_edit.text = description
if emoji is not None:
self._emoji_button.click()
EmojiPopup().wait_until_appears().select(emoji)
self._save_create_button.click()
self.wait_until_hidden()

View File

@ -0,0 +1,147 @@
import typing
import allure
from gui.components.base_popup import BasePopup
from gui.components.color_select_popup import ColorSelectPopup
from gui.components.community.tags_select_popup import TagsSelectPopup
from gui.components.os.open_file_dialogs import OpenFileDialog
from gui.components.picture_edit_popup import PictureEditPopup
from gui.elements.qt.button import Button
from gui.elements.qt.check_box import CheckBox
from gui.elements.qt.scroll import Scroll
from gui.elements.qt.text_edit import TextEdit
from gui.screens.community import CommunityScreen
class CreateCommunitiesBanner(BasePopup):
def __init__(self):
super().__init__()
self._crete_community_button = Button('create_new_StatusButton')
def open_create_community_popup(self) -> 'CreateCommunityPopup':
self._crete_community_button.click()
return CreateCommunityPopup().wait_until_appears()
class CreateCommunityPopup(BasePopup):
def __init__(self):
super().__init__()
self._scroll = Scroll('o_Flickable')
self._name_text_edit = TextEdit('createCommunityNameInput_TextEdit')
self._description_text_edit = TextEdit('createCommunityDescriptionInput_TextEdit')
self._add_logo_button = Button('addButton_StatusRoundButton2')
self._add_banner_button = Button('addButton_StatusRoundButton')
self._select_color_button = Button('StatusPickerButton')
self._choose_tag_button = Button('choose_tags_StatusPickerButton')
self._archive_support_checkbox = CheckBox('archiveSupportToggle_StatusCheckBox')
self._request_to_join_checkbox = CheckBox('requestToJoinToggle_StatusCheckBox')
self._pin_messages_checkbox = CheckBox('pinMessagesToggle_StatusCheckBox')
self._next_button = Button('createCommunityNextBtn_StatusButton')
self._intro_text_edit = TextEdit('createCommunityIntroMessageInput_TextEdit')
self._outro_text_edit = TextEdit('createCommunityOutroMessageInput_TextEdit')
self._create_community_button = Button('createCommunityFinalBtn_StatusButton')
@property
@allure.step('Get community name')
def name(self) -> str:
return self._name_text_edit.text
@name.setter
@allure.step('Set community name')
def name(self, value: str):
self._name_text_edit.text = value
@property
@allure.step('Get community description')
def description(self) -> str:
return self._description_text_edit.text
@description.setter
@allure.step('Set community name')
def description(self, value: str):
self._description_text_edit.text = value
@property
@allure.step('Get community logo')
def logo(self):
return NotImplementedError
@logo.setter
@allure.step('Set community logo')
def logo(self, kwargs: dict):
self._add_logo_button.click()
OpenFileDialog().wait_until_appears().open_file(kwargs['fp'])
PictureEditPopup().wait_until_appears().make_picture(kwargs.get('zoom', None), kwargs.get('shift', None))
@property
@allure.step('Get community banner')
def banner(self):
raise NotImplementedError
@banner.setter
@allure.step('Set community banner')
def banner(self, kwargs: dict):
self._add_banner_button.click()
OpenFileDialog().wait_until_appears().open_file(kwargs['fp'])
PictureEditPopup().wait_until_appears().make_picture(kwargs.get('zoom', None), kwargs.get('shift', None))
@property
@allure.step('Get community color')
def color(self):
raise NotImplementedError
@color.setter
@allure.step('Set community color')
def color(self, value: str):
self._scroll.vertical_scroll_to(self._select_color_button)
self._select_color_button.click()
ColorSelectPopup().wait_until_appears().select_color(value)
@property
@allure.step('Get community tags')
def tags(self):
raise NotImplementedError
@tags.setter
@allure.step('Set community tags')
def tags(self, values: typing.List[str]):
self._scroll.vertical_scroll_to(self._choose_tag_button)
self._choose_tag_button.click()
TagsSelectPopup().wait_until_appears().select_tags(values)
@property
@allure.step('Get community intro')
def intro(self) -> str:
return self._intro_text_edit.text
@intro.setter
@allure.step('Set community intro')
def intro(self, value: str):
self._intro_text_edit.text = value
@property
@allure.step('Get community outro')
def outro(self) -> str:
return self._outro_text_edit.text
@outro.setter
@allure.step('Set community outro')
def outro(self, value: str):
self._outro_text_edit.text = value
@allure.step('Open intro/outro form')
def open_next_form(self):
self._next_button.click()
@allure.step('Create community')
def create(self, kwargs):
for key in list(kwargs):
if key in ['intro', 'outro'] and self._next_button.is_visible:
self._next_button.click()
setattr(self, key, kwargs.get(key))
self._create_community_button.click()
self.wait_until_hidden()
return CommunityScreen().wait_until_appears()

View File

@ -0,0 +1,48 @@
import time
import typing
import allure
import configs
import driver
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
from gui.elements.qt.object import QObject
class TagsSelectPopup(BasePopup):
def __init__(self):
super().__init__()
self._tag_template = QObject('o_StatusCommunityTag')
self._save_button = Button('confirm_Community_Tags_StatusButton')
@allure.step('Wait until appears {0}')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._tag_template.wait_until_appears()
return self
@allure.step('Select tags')
def select_tags(self, values: typing.List[str]):
tags = []
checked = []
unchecked = []
for obj in driver.findAllObjects(self._tag_template.real_name):
name = str(obj.name)
tags.append(name)
if name in values:
if not obj.removable:
driver.mouseClick(obj)
checked.append(name)
time.sleep(1)
values.remove(name)
else:
# Selected and should be unselected
if obj.removable:
driver.mouseClick(obj)
time.sleep(1)
unchecked.append(name)
if values:
raise LookupError(
f'Tags: {values} not found in {tags}. Checked tags: {checked}, Unchecked tags: {unchecked}')
self._save_button.click()

View File

@ -0,0 +1,15 @@
import allure
from gui.elements.qt.object import QObject
class ContextMenu(QObject):
def __init__(self):
super(ContextMenu, self).__init__('contextMenu_PopupItem')
self._menu_item = QObject('contextMenuItem')
@allure.step('Select in context menu')
def select(self, value: str):
self._menu_item.real_name['text'] = value
self._menu_item.click()

View File

@ -0,0 +1,16 @@
import allure
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
class DeletePopup(BasePopup):
def __init__(self):
super().__init__()
self._delete_button = Button('delete_StatusButton')
@allure.step("Delete entity")
def delete(self):
self._delete_button.click()
self.wait_until_hidden()

View File

@ -0,0 +1,25 @@
import allure
import configs
from .base_popup import BasePopup
from ..elements.qt.object import QObject
from ..elements.qt.text_edit import TextEdit
class EmojiPopup(BasePopup):
def __init__(self):
super(EmojiPopup, self).__init__()
self._search_text_edit = TextEdit('mainWallet_AddEditAccountPopup_AccountEmojiSearchBox')
self._emoji_item = QObject('mainWallet_AddEditAccountPopup_AccountEmoji')
@allure.step('Wait until appears {0}')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._search_text_edit.wait_until_appears(timeout_msec)
return self
@allure.step('Select emoji')
def select(self, name: str):
self._search_text_edit.text = name
self._emoji_item.real_name['objectName'] = 'statusEmoji_' + name
self._emoji_item.click()
self._search_text_edit.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

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

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

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

@ -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,51 @@
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 PictureEditPopup(BasePopup):
def __init__(self):
super(PictureEditPopup, self).__init__()
self._zoom_slider = Slider('o_StatusSlider')
self._view = QObject('cropSpaceItem_Item')
self._make_picture_button = Button('make_picture_StatusButton')
self._slider_handler = QObject('o_DropShadow')
@allure.step('Make picture')
def make_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_picture_button.click()

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,16 @@
import allure
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
class SigningPhrasePopup(BasePopup):
def __init__(self):
super(SigningPhrasePopup, self).__init__()
self._ok_got_it_button = Button('signPhrase_Ok_Button')
@allure.step('Confirm signing phrase in popup')
def confirm_phrase(self):
self._ok_got_it_button.click()
SigningPhrasePopup().wait_until_hidden()

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,103 @@
import allure
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
from gui.elements.qt.check_box import CheckBox
from gui.elements.qt.object import QObject
from gui.elements.qt.text_edit import TextEdit
from gui.elements.qt.text_label import TextLabel
class AddSavedAddressPopup(BasePopup):
def __init__(self):
super(AddSavedAddressPopup, self).__init__()
self._name_text_edit = TextEdit('mainWallet_Saved_Addreses_Popup_Name_Input')
self._save_add_address_button = Button('mainWallet_Saved_Addreses_Popup_Address_Add_Button')
self._add_networks_selector = QObject('mainWallet_Saved_Addreses_Popup_Add_Network_Selector_Tag')
self._add_networks_button = Button('mainWallet_Saved_Addreses_Popup_Add_Network_Button')
self._ethereum_mainnet_checkbox = CheckBox(
'mainWallet_Saved_Addresses_Popup_Add_Network_Selector_Mainnet_checkbox')
self._optimism_mainnet_checkbox = CheckBox(
'mainWallet_Saved_Addresses_Popup_Add_Network_Selector_Optimism_checkbox')
self._arbitrum_mainnet_checkbox = CheckBox(
'mainWallet_Saved_Addresses_Popup_Add_Network_Selector_Arbitrum_checkbox')
self._ethereum_mainnet_network_tag = QObject(
'mainWallet_Saved_Addresses_Popup_Network_Selector_Mainnet_network_tag')
self._optimism_mainnet_network_tag = QObject(
'mainWallet_Saved_Addresses_Popup_Network_Selector_Optimism_network_tag')
self._arbitrum_mainnet_network_tag = QObject(
'mainWallet_Saved_Addresses_Popup_Network_Selector_Arbitrum_network_tag')
@allure.step('Set ethereum mainnet network checkbox')
def set_ethereum_mainnet_network(self, value: bool):
self._ethereum_mainnet_checkbox.set(value)
return self
@allure.step('Set optimism mainnet network checkbox')
def set_optimism_mainnet_network(self, value: bool):
self._optimism_mainnet_checkbox.set(value)
return self
@allure.step('Set arbitrum mainnet network checkbox')
def set_arbitrum_mainnet_network(self, value: bool):
self._arbitrum_mainnet_checkbox.set(value)
return self
@allure.step('Verify that network selector enabled')
def verify_network_selector_enabled(self):
assert self._add_networks_selector.is_visible, f'Network selector is not enabled'
@allure.step('Verify that etherium mainnet network present')
def verify_ethereum_mainnet_network_tag_present(self):
assert self._ethereum_mainnet_network_tag.is_visible, f'Ethereum Mainnet network tag is not present'
@allure.step('Verify that etherium mainnet network present')
def verify_otimism_mainnet_network_tag_present(self):
assert self._optimism_mainnet_network_tag.is_visible, f'Optimism Mainnet network tag is not present'
@allure.step('Verify that arbitrum mainnet network present')
def verify_arbitrum_mainnet_network_tag_present(self):
assert self._arbitrum_mainnet_network_tag.is_visible, f'Arbitrum Mainnet network tag is not present'
class AddressPopup(AddSavedAddressPopup):
def __init__(self):
super(AddressPopup, self).__init__()
self._address_text_edit = TextEdit('mainWallet_Saved_Addreses_Popup_Address_Input_Edit')
@allure.step('Add saved address')
def add_saved_address(self, name: str, address: str):
self._name_text_edit.text = name
self._address_text_edit.clear(verify=False)
self._address_text_edit.type_text(address)
if address.startswith("0x"):
self.verify_network_selector_enabled()
self._add_networks_selector.click(1, 1)
self.set_ethereum_mainnet_network(True)
self.set_optimism_mainnet_network(True)
self.set_arbitrum_mainnet_network(True)
self._save_add_address_button.click() # click it twice to close the network selector pop up
self.verify_ethereum_mainnet_network_tag_present()
self.verify_otimism_mainnet_network_tag_present()
self.verify_arbitrum_mainnet_network_tag_present(),
self._save_add_address_button.click()
self.wait_until_hidden()
class EditSavedAddressPopup(AddSavedAddressPopup):
def __init__(self):
super(EditSavedAddressPopup, self).__init__()
self._address_text_label = TextLabel('mainWallet_Saved_Addreses_Popup_Address_Input_Edit')
@allure.step('Edit saved address')
def edit_saved_address(self, new_name: str, address: str):
self._name_text_edit.text = new_name
if address.startswith("0x"):
self._add_networks_button.click()
self.set_ethereum_mainnet_network(False)
self.set_optimism_mainnet_network(False)
self.set_arbitrum_mainnet_network(False)
self._save_add_address_button.click()
self._save_add_address_button.click()
self.wait_until_hidden()

View File

@ -0,0 +1,16 @@
import allure
from gui.elements.qt.button import Button
from gui.elements.qt.object import QObject
class ConfirmationPopup(QObject):
def __init__(self):
super(ConfirmationPopup, self).__init__('contextMenu_PopupItem')
self._confirm_button = Button('confirmButton')
@allure.step('Confirm action')
def confirm(self):
self._confirm_button.click()
self.wait_until_hidden()

View File

@ -0,0 +1,30 @@
import allure
import configs
from gui.components.base_popup import BasePopup
from gui.elements.qt.button import Button
from gui.elements.qt.check_box import CheckBox
class RemoveWalletAccountPopup(BasePopup):
def __init__(self):
super(RemoveWalletAccountPopup, self).__init__()
self._confirm_button = Button('mainWallet_Remove_Account_Popup_ConfirmButton')
self._cancel_button = Button('mainWallet_Remove_Account_Popup_CancelButton')
self._have_pen_paper_checkbox = CheckBox('mainWallet_Remove_Account_Popup_HavePenPaperCheckBox')
@allure.step('Wait until appears {0}')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
self._cancel_button.wait_until_appears(timeout_msec)
return self
@allure.step('Confirm removing account')
def confirm(self):
self._confirm_button.click()
self._confirm_button.wait_until_hidden()
@allure.step('Agree and confirm removing account')
def agree_and_confirm(self):
self._have_pen_paper_checkbox.wait_until_appears().set(True)
self.confirm()

View File

@ -0,0 +1,157 @@
import allure
import configs
import constants.wallet
import driver
from gui.components.authenticate_popup import AuthenticatePopup
from gui.components.base_popup import BasePopup
from gui.components.emoji_popup import EmojiPopup
from gui.elements.qt.button import Button
from gui.elements.qt.check_box import CheckBox
from gui.elements.qt.text_edit import TextEdit
from gui.elements.qt.scroll import Scroll
from gui.elements.qt.object import QObject
GENERATED_PAGES_LIMIT = 20
class AccountPopup(BasePopup):
def __init__(self):
super(AccountPopup, self).__init__()
self._scroll = Scroll('scrollView_StatusScrollView')
self._name_text_edit = TextEdit('mainWallet_AddEditAccountPopup_AccountName')
self._emoji_button = Button('mainWallet_AddEditAccountPopup_AccountEmojiPopupButton')
self._color_radiobutton = QObject('color_StatusColorRadioButton')
# origin
self._origin_combobox = QObject('mainWallet_AddEditAccountPopup_SelectedOrigin')
self._watch_only_account_origin_item = QObject("mainWallet_AddEditAccountPopup_OriginOptionWatchOnlyAcc")
self._new_master_key_origin_item = QObject('mainWallet_AddEditAccountPopup_OriginOptionNewMasterKey')
self._existing_origin_item = QObject('addAccountPopup_OriginOption_StatusListItem')
# derivation
self._address_text_edit = TextEdit('mainWallet_AddEditAccountPopup_AccountWatchOnlyAddress')
self._add_account_button = Button('mainWallet_AddEditAccountPopup_PrimaryButton')
self._edit_derivation_path_button = Button('mainWallet_AddEditAccountPopup_EditDerivationPathButton')
self._derivation_path_combobox_button = Button('mainWallet_AddEditAccountPopup_PreDefinedDerivationPathsButton')
self._derivation_path_list_item = QObject('mainWallet_AddEditAccountPopup_derivationPath')
self._reset_derivation_path_button = Button('mainWallet_AddEditAccountPopup_ResetDerivationPathButton')
self._derivation_path_text_edit = TextEdit('mainWallet_AddEditAccountPopup_DerivationPathInput')
self._address_combobox_button = Button('mainWallet_AddEditAccountPopup_GeneratedAddressComponent')
self._non_eth_checkbox = CheckBox('mainWallet_AddEditAccountPopup_NonEthDerivationPathCheckBox')
@allure.step('Set name for account')
def set_name(self, value: str):
self._name_text_edit.text = value
return self
@allure.step('Set color for account')
def set_color(self, value: str):
if 'radioButtonColor' in self._color_radiobutton.real_name.keys():
del self._color_radiobutton.real_name['radioButtonColor']
colors = [str(item.radioButtonColor) for item in driver.findAllObjects(self._color_radiobutton.real_name)]
assert value in colors, f'Color {value} not found in {colors}'
self._color_radiobutton.real_name['radioButtonColor'] = value
self._color_radiobutton.click()
return self
@allure.step('Set emoji for account')
def set_emoji(self, value: str):
self._emoji_button.click()
EmojiPopup().wait_until_appears().select(value)
return self
@allure.step('Set eth address for account added from context menu')
def set_eth_address(self, value: str):
self._address_text_edit.text = value
return self
@allure.step('Set eth address for account added from plus button')
def set_origin_eth_address(self, value: str):
self._origin_combobox.click()
self._watch_only_account_origin_item.click()
self._address_text_edit.text = value
return self
@allure.step('Set private key for account')
def set_origin_private_key(self, value: str):
self._origin_combobox.click()
self._new_master_key_origin_item.click()
AddNewAccountPopup().wait_until_appears().import_private_key(value)
return self
@allure.step('Set derivation path for account')
def set_derivation_path(self, value: str, index: int, password: str):
self._edit_derivation_path_button.hover().click()
AuthenticatePopup().wait_until_appears().authenticate(password)
if value in [_.value for _ in constants.wallet.DerivationPath]:
self._derivation_path_combobox_button.click()
self._derivation_path_list_item.real_name['title'] = value
self._derivation_path_list_item.click()
del self._derivation_path_list_item.real_name['title']
self._address_combobox_button.click()
GeneratedAddressesList().wait_until_appears().select(index)
if value != constants.wallet.DerivationPath.ETHEREUM.value:
self._scroll.vertical_down_to(self._non_eth_checkbox)
self._non_eth_checkbox.set(True)
else:
self._derivation_path_text_edit.type_text(str(index))
return self
@allure.step('Save added account')
def save(self):
self._add_account_button.wait_until_appears().click()
return self
class AddNewAccountPopup(BasePopup):
def __init__(self):
super(AddNewAccountPopup, self).__init__()
self._import_private_key_button = Button('mainWallet_AddEditAccountPopup_MasterKey_ImportPrivateKeyOption')
self._private_key_text_edit = TextEdit('mainWallet_AddEditAccountPopup_PrivateKey')
self._private_key_name_text_edit = TextEdit('mainWallet_AddEditAccountPopup_PrivateKeyName')
self._continue_button = Button('mainWallet_AddEditAccountPopup_PrimaryButton')
@allure.step('Import private key')
def import_private_key(self, private_key: str) -> str:
self._import_private_key_button.click()
self._private_key_text_edit.text = private_key
self._private_key_name_text_edit.text = private_key[:5]
self._continue_button.click()
return private_key[:5]
class GeneratedAddressesList(QObject):
def __init__(self):
super(GeneratedAddressesList, self).__init__('statusDesktop_mainWindow_overlay_popup2')
self._address_list_item = QObject('addAccountPopup_GeneratedAddress')
self._paginator_page = QObject('page_StatusBaseButton')
@property
@allure.step('Load generated addresses list')
def is_paginator_load(self) -> bool:
try:
return str(driver.findAllObjects(self._paginator_page.real_name)[0].text) == '1'
except IndexError:
return False
@allure.step('Wait until appears {0}')
def wait_until_appears(self, timeout_msec: int = configs.timeouts.UI_LOAD_TIMEOUT_MSEC):
if 'text' in self._paginator_page.real_name:
del self._paginator_page.real_name['text']
assert driver.waitFor(lambda: self.is_paginator_load, timeout_msec), 'Generated address list not load'
return self
@allure.step('Select address in list')
def select(self, index: int):
self._address_list_item.real_name['objectName'] = f'AddAccountPopup-GeneratedAddress-{index}'
selected_page_number = 1
while selected_page_number != GENERATED_PAGES_LIMIT:
if self._address_list_item.is_visible:
self._address_list_item.click()
self._paginator_page.wait_until_hidden()
break
else:
selected_page_number += 1
self._paginator_page.real_name['text'] = selected_page_number
self._paginator_page.click()

View File

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, ValueError) 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

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'

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

Some files were not shown because too many files have changed in this diff Show More