chore(ui-test): First-approach
Add basic layered architecture and basic test suite (status login).
|
@ -0,0 +1,18 @@
|
||||||
|
# status-desktop-ui-test
|
||||||
|
UI test application for **Status Desktop**
|
||||||
|
|
||||||
|
* Test automation project that uses [Squish](https://www.froglogic.com/squish/) as a testing tool with [BDD](https://www.froglogic.com/squish/features/bdd-behavior-driven-development-testing/).
|
||||||
|
* Information about its architecture can be found in [wiki](https://hackmd.io/@status-desktop/B1MlJV5nd/%2Fm9D4p_y7ShOm3ooD7GAT0A).
|
||||||
|
|
||||||
|
![Screenshot 2022-02-25 at 10 22 45](https://user-images.githubusercontent.com/97019400/155689587-e933bbfa-519c-4f73-90a7-c019c0bb163f.png)
|
||||||
|
|
||||||
|
|
||||||
|
## Preparing the environment to develop and run tests
|
||||||
|
1) Install [Squish](https://doc.froglogic.com/squish/latest/) and run its IDE.
|
||||||
|
2) `File / Open Test Suite` and browse to `testSuites` directory.
|
||||||
|
3) Once the suite is open, click `Test Suite Settings` button to configure the **AUT** (Application Under test).
|
||||||
|
* Select `AUT tab` and browse until the corresponding `status-desktop\bin\nim_status_client` binary is set.
|
||||||
|
* Uncheck `Automatically start the AUT` option (if it is already checked).
|
||||||
|
* Save changes.
|
||||||
|
|
||||||
|
Now you should be able to create new suites, test cases and run the existing ones just only by clicking `Run` buttons!!
|
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 56"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#5dc754;}</style></defs><g id="Ebene_2" data-name="Ebene 2"><g id="Ebene_1-2" data-name="Ebene 1"><path d="M70.9,15a12.41,12.41,0,0,0-2.7-.3c-2.3,0-3.3.8-3.8,2.9L64,19.7h6.3l-1.2,6H62.8l-3.7,19H52l3.7-19h-4l1.2-6h4l.5-2.7c1.1-5.7,4.4-8.3,10.8-8.3a17.59,17.59,0,0,1,4.6.6Z"/><path d="M91.4,19.4l-1.8,6a5.5,5.5,0,0,0-2.2-.2,8,8,0,0,0-7,4.2L77.5,44.7H70.3l4.8-25h6.4l-.1,3.1A9.47,9.47,0,0,1,89,19C89.7,19.1,90.5,19.2,91.4,19.4Z"/><path d="M90.9,32.2c1.6-8,7.7-13.1,15-13.1s11.4,5.2,9.8,13.1-7.6,13.1-15,13.1S89.3,40.1,90.9,32.2Zm17.5,0c.9-4.5-.7-7-3.8-7s-5.6,2.5-6.5,7,.7,7,3.8,7S107.5,36.7,108.4,32.2Z"/><path d="M134.8,42.7a9.71,9.71,0,0,1-6.7,2.6c-6.1,0-10.1-4.8-8.5-13.1s7.6-13.1,13.7-13.1c2.8,0,5,1.2,5.9,2.9l.8-2.3h6.5l-4.8,24.5c-1.5,7.5-7.2,11.8-15,11.8-4.4,0-8.2-1.8-9.9-5l6.2-3.4a5.25,5.25,0,0,0,4.9,2.8,6.28,6.28,0,0,0,6.4-5.3Zm-4.1-3.5a7,7,0,0,0,5.2-2.5l1.8-9.1a4.55,4.55,0,0,0-4.3-2.5c-3.2,0-5.7,2.4-6.6,7S127.5,39.2,130.7,39.2Z"/><path d="M148.2,44.7l7-35.9h7.2l-7,35.9Z"/><path d="M163.1,32.2c1.6-8,7.6-13.1,15-13.1s11.4,5.2,9.9,13.1-7.6,13.1-15,13.1S161.6,40.1,163.1,32.2Zm17.6,0c.9-4.5-.7-7-3.8-7s-5.6,2.5-6.5,7,.7,7,3.8,7S179.8,36.7,180.7,32.2Z"/><path d="M207,42.7a9.71,9.71,0,0,1-6.7,2.6c-6.1,0-10.1-4.8-8.5-13.1s7.6-13.1,13.7-13.1c2.7,0,5,1.2,5.9,2.9l.8-2.3h6.5L214,44.2c-1.5,7.5-7.1,11.8-14.9,11.8-4.4,0-8.2-1.8-9.9-5l6.2-3.4a5.25,5.25,0,0,0,4.9,2.8,6.05,6.05,0,0,0,6.4-5.3Zm-4-3.5a7.38,7.38,0,0,0,5.2-2.5l1.8-9.1a4.55,4.55,0,0,0-4.3-2.5c-3.2,0-5.8,2.4-6.7,7S199.8,39.2,203,39.2Z"/><path d="M220.6,44.7l4.9-25h7.2l-4.9,25ZM231.2,8.4a3.36,3.36,0,0,1,3.5,3.2,3.4,3.4,0,0,1-.1,1,5.32,5.32,0,0,1-5,4.2,3.46,3.46,0,0,1-3.6-3.3,2.77,2.77,0,0,1,.1-.9A5.35,5.35,0,0,1,231.2,8.4Z"/><path d="M250.3,19.1c5.5,0,9.1,3,9.7,7.8l-7.2,1.6a3.35,3.35,0,0,0-3.4-3.3h-.3c-3.1,0-5.6,2.6-6.4,7s.6,7,3.7,7a5.63,5.63,0,0,0,5-3.3l6.6,1.6a14.23,14.23,0,0,1-12.7,7.9c-7.3,0-11.3-5.2-9.8-13.1S243,19.1,250.3,19.1Z"/><rect class="cls-1" x="25.5" y="7.6" width="5.7" height="13.1" transform="translate(-0.01 28.28) rotate(-53)"/><path class="cls-2" d="M37.7,28.9l6.9-9.1-3.3-2.5-4.9,6.5-1.3-1L40,16.3,23,3.5,18.2,10,16.9,9l4.9-6.5L18.5,0,11.6,9.1l4.6,3.4L9.8,21,5.9,18.1,0,26l3.9,2.9,3.9-5.2,17,12.8-3.9,5.2,3.9,3,5.9-7.8L26.8,34l6.4-8.5ZM25.8,14.8l-3.3-2.5L25,9l3.3,2.5Zm5.9,4.4-3.3-2.5,2.5-3.3,3.3,2.5Z"/><rect x="24.7" y="10.09" width="2.4" height="2.4" transform="translate(1.29 25.18) rotate(-53)"/><rect x="30.61" y="14.49" width="2.4" height="2.4" transform="translate(0.13 31.65) rotate(-52.99)"/></g></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="252"><path fill="#BABCBE" d="M223.99 65a.01.01 0 0 1 .01.01v144.98a.01.01 0 0 1-.01.01H28.01a.01.01 0 0 1-.01-.01V65.01a.01.01 0 0 1 .01-.01h195.98z"/><path fill="#F2F2F2" d="M40 141h82v57H40zM129 141h81v57h-81zM40 74h82v60H40zM129 74h81v60h-81z"/><path fill="#333" d="M28 42h196v22H28z"/><path stroke="#333" stroke-width="3" stroke-miterlimit="10" d="M157 85.5h-12M176 96.5h-11M171 107.5h-26M155 118.5h-10"/><path stroke="#4CA34B" stroke-width="5.566" stroke-miterlimit="10" d="M53.254 120.958l22.116-15.686"/><circle fill="none" stroke="#4CA34B" stroke-width="5.566" stroke-miterlimit="10" cx="89.423" cy="98.538" r="15.087"/><circle fill="#4CA34B" cx="155.333" cy="169.933" r="8.271"/><path fill="#4CA34B" d="M183 161.713l15.388 8.883L183 179.479zM168.298 192h-2.964l8.895-44h2.966z"/><path stroke="#848689" stroke-width="3" stroke-miterlimit="10" d="M111 162.5H72M111 180.5H72"/><path fill="none" stroke="#4CA34B" stroke-width="4.287" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M51.996 159.113l6.848 7.303 4.506-13.098"/><path fill="#4CA34B" stroke="#333" stroke-width="4.271" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M52.043 185.148l10.458-10.22M61.908 185.148l-9.272-10.22"/><circle fill="#FFF" cx="41.214" cy="52.164" r="5.639"/><circle fill="#FFF" cx="58.132" cy="52.164" r="5.639"/><circle fill="#FFF" cx="75.049" cy="52.164" r="5.639"/><path fill="none" stroke="#848689" stroke-width="2" stroke-miterlimit="10" d="M40 135h82M129 135h81M40 198h82M129 198h81M28 65h196"/><path fill="#BABCBE" stroke="#848689" stroke-width="3" stroke-miterlimit="10" d="M171 85.5h-10"/><path stroke="#848689" stroke-width="3" stroke-miterlimit="10" d="M189 96.5h-10"/><path stroke="#333" stroke-width="3" stroke-miterlimit="10" d="M161 96.5h-16M194 107.5h-20"/><path stroke="#848689" stroke-width="3" stroke-miterlimit="10" d="M168 118.5h-10"/><path stroke="#333" stroke-width="3" stroke-miterlimit="10" d="M182 118.5h-10"/></svg>
|
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 140 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file StatusAccount.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief It defines a basic status account object.
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
#It defines a basic status account object.
|
||||||
|
class StatusAccount():
|
||||||
|
__name = None
|
||||||
|
__password = None
|
||||||
|
|
||||||
|
def __init__(self, name, password = None):
|
||||||
|
self.__name = name
|
||||||
|
self.__password = password
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def get_password(self):
|
||||||
|
return self.__password
|
|
@ -0,0 +1,76 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file SquishDriver.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief It contains generic Status view components definitions and Squish driver API.
|
||||||
|
# *****************************************************************************/
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# IMPORTANT: It is necessary to import manually the Squish drivers module by module.
|
||||||
|
# More info in: https://kb.froglogic.com/display/KB/Article+-+Using+Squish+functions+in+your+own+Python+modules+or+packages
|
||||||
|
import squish
|
||||||
|
import object
|
||||||
|
import names
|
||||||
|
|
||||||
|
# The default maximum timeout to find ui object
|
||||||
|
_MAX_WAIT_OBJ_TIMEOUT = 5000 #[milliseconds]
|
||||||
|
|
||||||
|
# Waits for the given object is loaded, visible and enabled.
|
||||||
|
# It returns a tuple: True in case it is found. Otherwise, false. And the object itself.
|
||||||
|
def is_loaded_visible_and_enabled(objName, timeout = _MAX_WAIT_OBJ_TIMEOUT):
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = squish.waitForObject(getattr(names, objName), timeout)
|
||||||
|
return True, obj
|
||||||
|
except LookupError:
|
||||||
|
return False, obj
|
||||||
|
|
||||||
|
# Waits for the given object is loaded and might be not visible and/or not enabled:
|
||||||
|
# It returns a tuple: True in case it is found. Otherwise, false. And the object itself.
|
||||||
|
def is_loaded(objName):
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = squish.findObject(getattr(names, objName))
|
||||||
|
return True, obj
|
||||||
|
except LookupError:
|
||||||
|
return False, obj
|
||||||
|
|
||||||
|
# It checks if the given object is visible and enabled.
|
||||||
|
def is_visible_and_enabled(obj):
|
||||||
|
return obj.visible and obj.enabled
|
||||||
|
|
||||||
|
# Given a specific object, get a specific child.
|
||||||
|
def get_child(obj, child_index = None):
|
||||||
|
if None == child_index:
|
||||||
|
return object.children(obj)
|
||||||
|
else:
|
||||||
|
return object.children(obj)[child_index]
|
||||||
|
|
||||||
|
# It executes the click action into the given object:
|
||||||
|
def click_obj(obj):
|
||||||
|
try:
|
||||||
|
squish.mouseClick(obj, squish.Qt.LeftButton)
|
||||||
|
return True
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# It executes the click action into object with given object name:
|
||||||
|
def click_obj_by_name(objName):
|
||||||
|
try:
|
||||||
|
obj = squish.waitForObject(getattr(names, objName))
|
||||||
|
squish.mouseClick(obj, squish.Qt.LeftButton)
|
||||||
|
return True
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# It types the specified text into the given object (as if the user had used the keyboard):
|
||||||
|
def type(objName, text):
|
||||||
|
try:
|
||||||
|
obj = squish.findObject(getattr(names, objName))
|
||||||
|
squish.type(obj, text)
|
||||||
|
return True
|
||||||
|
except LookupError:
|
||||||
|
return False
|
|
@ -0,0 +1,127 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file StatusLoginProcess.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief It defines the status login process.
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
from processes.StatusProcess import StatusProcess
|
||||||
|
from screens.StatusLoginScreen import StatusLoginScreen
|
||||||
|
from screens.StatusLoginScreen import PswPlaceholderTextType
|
||||||
|
|
||||||
|
# It defines the status login process.
|
||||||
|
class StatusLoginProcess(StatusProcess):
|
||||||
|
__login_screen = None
|
||||||
|
__account = None
|
||||||
|
__byKeycard = False
|
||||||
|
__isBiometrics = False
|
||||||
|
__step1_result = False
|
||||||
|
__step2_result = False
|
||||||
|
__step3_result = False
|
||||||
|
|
||||||
|
def __init__(self, account, isBiometrics=False):
|
||||||
|
self.__account = account
|
||||||
|
self.__byKeycard = (account.get_password() == None)
|
||||||
|
self.__isBiometrics = isBiometrics
|
||||||
|
|
||||||
|
# It is used to check if the process can be run
|
||||||
|
def can_execute_process(self):
|
||||||
|
# Create current screen and verify if it is correctly loaded
|
||||||
|
self.__login_screen = StatusLoginScreen()
|
||||||
|
return self.__login_screen.is_loaded()
|
||||||
|
|
||||||
|
# It is used to execute the status login process steps.
|
||||||
|
def execute_process(self, verify_success = True):
|
||||||
|
self.__verify_success = verify_success
|
||||||
|
|
||||||
|
if(self.__account.get_password()):
|
||||||
|
self.__execute_password_steps()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("TODO: __execute_keycard_steps")
|
||||||
|
|
||||||
|
# It is used to obtain the status login process output result.
|
||||||
|
def get_process_result(self):
|
||||||
|
result = False
|
||||||
|
result_description = None
|
||||||
|
|
||||||
|
# Login by password:
|
||||||
|
if(self.__account.get_password()):
|
||||||
|
if not self.__step1_result:
|
||||||
|
result_description = "Not possible to select the given account."
|
||||||
|
|
||||||
|
elif not self.__step2_result:
|
||||||
|
result_description = "Not possible to introduce given password and submit."
|
||||||
|
|
||||||
|
elif not self.__step3_result and self.__verify_success:
|
||||||
|
result_description = "Expected connection to Status Desktop failed."
|
||||||
|
|
||||||
|
elif not self.__step3_result and not self.__verify_success:
|
||||||
|
result_description = "Expected error message not shown."
|
||||||
|
|
||||||
|
else:
|
||||||
|
# All the steps have been correctly executed.
|
||||||
|
result = True
|
||||||
|
result_description = "Process steps succeeded!"
|
||||||
|
|
||||||
|
# Login by keycard:
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("TODO: __execute_keycard_steps")
|
||||||
|
|
||||||
|
return result, result_description
|
||||||
|
|
||||||
|
# Step 1 - The user selects an account
|
||||||
|
# Step 2 - The user enters a password
|
||||||
|
# Step 3 - Verify login success / failure
|
||||||
|
def __execute_password_steps(self):
|
||||||
|
|
||||||
|
# Step 1:
|
||||||
|
self.__step1_result = self.__step_user_selects_account(self.__account.get_name())
|
||||||
|
|
||||||
|
# Step 2:
|
||||||
|
self.__step2_result = self.__step_user_enters_password(self.__account.get_password())
|
||||||
|
|
||||||
|
# Step 3:
|
||||||
|
if self.__verify_success:
|
||||||
|
self.__step3_result = self.__verify_login_success()
|
||||||
|
else:
|
||||||
|
self.__step3_result = self.__verify_login_failure()
|
||||||
|
|
||||||
|
# It navigates through login screen to select the given account:
|
||||||
|
def __step_user_selects_account(self, accountName):
|
||||||
|
result = False
|
||||||
|
if self.__login_screen and self.__login_screen.is_loaded():
|
||||||
|
if self.__login_screen.open_accounts_selector_popup():
|
||||||
|
accounts_popup = self.__login_screen.get_accounts_selector_popup()
|
||||||
|
if accounts_popup.is_loaded():
|
||||||
|
result = accounts_popup.select_account(accountName)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# It navigates through password input and submits the introduced one:
|
||||||
|
def __step_user_enters_password(self, password):
|
||||||
|
res1 = False
|
||||||
|
res2 = False
|
||||||
|
if self.__login_screen and self.__login_screen.is_loaded():
|
||||||
|
res1 = self.__login_screen.introduce_password(password)
|
||||||
|
res2 = self.__login_screen.submit_password()
|
||||||
|
|
||||||
|
return res1 & res2
|
||||||
|
|
||||||
|
# It inspects login screen and decides if login has been succeed:
|
||||||
|
def __verify_login_success(self):
|
||||||
|
res1 = False
|
||||||
|
res2 = False
|
||||||
|
if self.__login_screen and self.__login_screen.is_loaded():
|
||||||
|
res1 = self.__login_screen.get_password_placeholder_text() == PswPlaceholderTextType.CONNECTING.value
|
||||||
|
res2 = self.__login_screen.get_error_message_text() == ""
|
||||||
|
return res1 & res2
|
||||||
|
|
||||||
|
# It inspects login screen and decides if it displays the expected failure information:
|
||||||
|
def __verify_login_failure(self):
|
||||||
|
result = False
|
||||||
|
if self.__login_screen and self.__login_screen.is_loaded():
|
||||||
|
result = self.__login_screen.get_error_message_text() == self.__login_screen.get_expected_error_message_text()
|
||||||
|
return result
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file StatusProcess.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief Base template class to define testing status processes.
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
class StatusProcess:
|
||||||
|
__context = None
|
||||||
|
# Variable used to determine if it is needed to verify the process success or the process failure behavior
|
||||||
|
__verify_success = True
|
||||||
|
|
||||||
|
def __init__(self, context):
|
||||||
|
self.__context = context
|
||||||
|
|
||||||
|
# It is used to check if the process can be run
|
||||||
|
#@abstractmethod
|
||||||
|
def can_execute_process(self):
|
||||||
|
print("TODO: Invoke needed screen/s constructors")
|
||||||
|
# ***
|
||||||
|
# Below, the code to create the necessary status screen to execute the process.
|
||||||
|
# ***
|
||||||
|
|
||||||
|
# It is used to execute the specific status process steps.
|
||||||
|
#@abstractmethod
|
||||||
|
def execute_process(self, verify_success = True):
|
||||||
|
self.__verify_success = verify_success
|
||||||
|
print("TODO: Invoke navigations")
|
||||||
|
# ***
|
||||||
|
# Below, the code to invoke the necessary status screen's navigations.
|
||||||
|
# ***
|
||||||
|
|
||||||
|
# It is used to obtain the status process output result.
|
||||||
|
#@abstractmethod
|
||||||
|
def get_process_result(self):
|
||||||
|
result = False
|
||||||
|
result_description = None
|
||||||
|
|
||||||
|
print("TODO: Validate process steps")
|
||||||
|
# ***
|
||||||
|
# Below, the code to validate status process steps.
|
||||||
|
# ***
|
||||||
|
|
||||||
|
return result, result_description
|
|
@ -0,0 +1,47 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file AccountsPopup.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief It defines the status accounts popup behavior and properties.
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
from drivers.SquishDriver import *
|
||||||
|
|
||||||
|
# It defines the identifier for each Account View component:
|
||||||
|
class SAccountsComponents(Enum):
|
||||||
|
ACCOUNTS_POPUP = "accountsView_accountListPanel"
|
||||||
|
|
||||||
|
#It defines the status accounts popup behavior and properties.
|
||||||
|
class StatusAccountsScreen():
|
||||||
|
__is_loaded = False
|
||||||
|
__accountsList = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
[self.__is_loaded, self.__accountsList] = is_loaded_visible_and_enabled(SAccountsComponents.ACCOUNTS_POPUP.value)
|
||||||
|
|
||||||
|
def is_loaded(self):
|
||||||
|
return self.__is_loaded
|
||||||
|
|
||||||
|
def find_account(self, account):
|
||||||
|
[found, account_obj] = self.__find_account(account)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def select_account(self, account):
|
||||||
|
[found, account_obj] = self.__find_account(account)
|
||||||
|
if found:
|
||||||
|
return click_obj(account_obj)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def __find_account(self, account):
|
||||||
|
found = False
|
||||||
|
account_obj = None
|
||||||
|
for index in range(self.__accountsList.count):
|
||||||
|
a = self.__accountsList.itemAtIndex(index)
|
||||||
|
if(a.username == account):
|
||||||
|
account_obj = a
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
return found, account_obj
|
|
@ -0,0 +1,86 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file StatusLoginScreen.py
|
||||||
|
# *
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief It defines the status login screen behavior and properties.
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from screens.StatusAccountsScreen import StatusAccountsScreen
|
||||||
|
from drivers.SquishDriver import *
|
||||||
|
|
||||||
|
# It defines the identifier for each Login View component:
|
||||||
|
class SLoginComponents(Enum):
|
||||||
|
MAIN_VIEW = "loginView_main"
|
||||||
|
PASSWORD_INPUT = "loginView_passwordInput"
|
||||||
|
SUBMIT_BTN = "loginView_submitBtn"
|
||||||
|
CHANGE_ACCOUNT_BTN = "loginView_changeAccountBtn"
|
||||||
|
ERR_MSG_LABEL = "loginView_errMsgLabel"
|
||||||
|
|
||||||
|
# It defines expected password placeholder text.
|
||||||
|
class PswPlaceholderTextType(Enum):
|
||||||
|
NONE = None
|
||||||
|
CONNECTING = "Connecting..."
|
||||||
|
PASSWORD = "Enter password"
|
||||||
|
|
||||||
|
# It defines the status login screen behavior and properties.
|
||||||
|
class StatusLoginScreen():
|
||||||
|
__is_loaded = False
|
||||||
|
__login_view_obj = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
[self.__is_loaded, self.__login_view_obj] = is_loaded_visible_and_enabled(SLoginComponents.MAIN_VIEW.value)
|
||||||
|
|
||||||
|
def is_loaded(self):
|
||||||
|
return self.__is_loaded
|
||||||
|
|
||||||
|
def introduce_password(self, password):
|
||||||
|
result = False
|
||||||
|
if click_obj_by_name(SLoginComponents.PASSWORD_INPUT.value) and type(SLoginComponents.PASSWORD_INPUT.value, password):
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
def submit_password(self):
|
||||||
|
return click_obj_by_name(SLoginComponents.SUBMIT_BTN.value)
|
||||||
|
|
||||||
|
def open_accounts_selector_popup(self):
|
||||||
|
return click_obj_by_name(SLoginComponents.CHANGE_ACCOUNT_BTN.value)
|
||||||
|
|
||||||
|
def get_accounts_selector_popup(self):
|
||||||
|
return StatusAccountsScreen()
|
||||||
|
|
||||||
|
def get_password_placeholder_text(self):
|
||||||
|
result = ""
|
||||||
|
[loaded, obj] = is_loaded(SLoginComponents.PASSWORD_INPUT.value)
|
||||||
|
if loaded:
|
||||||
|
result = obj.placeholderText
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_error_message_text(self):
|
||||||
|
result = ""
|
||||||
|
[loaded, obj] = is_loaded_visible_and_enabled(SLoginComponents.ERR_MSG_LABEL.value)
|
||||||
|
if loaded:
|
||||||
|
result = obj.text
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_expected_error_message_text(self):#, language):
|
||||||
|
# NOTE: It could introduce language checkers.
|
||||||
|
return "Login failed. Please re-enter your password and try again."
|
||||||
|
|
||||||
|
# NOT IMPLEMENTED STUFF:
|
||||||
|
def get_expected_placeholder_text(self, pswPlaceholderTextType):#, language):
|
||||||
|
# NOTE: It could introduce language checkers.
|
||||||
|
raise NotImplementedError("TODO: get_expected_placeholder_text method")
|
||||||
|
|
||||||
|
def open_generate_new_keys_popup(self):
|
||||||
|
raise NotImplementedError("TODO: open_generate_new_keys_popup method")
|
||||||
|
|
||||||
|
def get_current_account_name(self):
|
||||||
|
raise NotImplementedError("TODO: get_current_account_name method")
|
||||||
|
|
||||||
|
def get_current_identicon(self):
|
||||||
|
raise NotImplementedError("TODO: get_current_identicon method")
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<testconfig version="1.0">
|
||||||
|
<information>
|
||||||
|
<summary/>
|
||||||
|
<description/>
|
||||||
|
</information>
|
||||||
|
<testsettings/>
|
||||||
|
<passwords/>
|
||||||
|
</testconfig>
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# This file contains hook functions to run as the .feature file is executed.
|
||||||
|
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../../../src/"))
|
||||||
|
|
||||||
|
_statusDektopAppName = "nim_status_client"
|
||||||
|
_appClosureTimeout = 2 #[seconds]
|
||||||
|
|
||||||
|
@OnScenarioStart
|
||||||
|
def hook(context):
|
||||||
|
startApplication(_statusDektopAppName)
|
||||||
|
context.userData = {}
|
||||||
|
|
||||||
|
@OnScenarioEnd
|
||||||
|
def hook(context):
|
||||||
|
currentApplicationContext().detach()
|
||||||
|
snooze(_appClosureTimeout)
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# encoding: UTF-8
|
||||||
|
|
||||||
|
from objectmaphelper import *
|
||||||
|
|
||||||
|
statusDesktop_mainWindow = {"name": "mainWindow", "type": "StatusWindow", "visible": True}
|
||||||
|
mainWindow_dropRectangle_Rectangle = {"container": statusDesktop_mainWindow, "id": "dropRectangle", "type": "Rectangle", "unnamed": 1, "visible": True}
|
||||||
|
loginView_passwordInput = {"container": statusDesktop_mainWindow, "echoMode": 2, "id": "inputValue", "passwordCharacter": "•", "type": "StyledTextField", "unnamed": 1, "visible": True}
|
||||||
|
loginView_changeAccountBtn = {"container": statusDesktop_mainWindow, "id": "changeAccountBtn", "type": "Rectangle", "unnamed": 1, "visible": True}
|
||||||
|
loginView_submitBtn = {"container": statusDesktop_mainWindow, "type": "StatusRoundButton", "visible": True}
|
||||||
|
loginView_main = {"container": statusDesktop_mainWindow, "type": "LoginView", "visible": True}
|
||||||
|
loginView_errMsgLabel = {"container": statusDesktop_mainWindow, "id": "errMsg", "type": "StyledText", "visible": True}
|
||||||
|
statusDesktop_mainWindow_overlay = {"container": statusDesktop_mainWindow, "type": "Overlay", "unnamed": 1, "visible": True}
|
||||||
|
accountsView_accountListPanel = {"container": statusDesktop_mainWindow_overlay, "type": "AccountListPanel", "visible": True}
|
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file steps.py
|
||||||
|
# *
|
||||||
|
# * \test Status Desktop - Login
|
||||||
|
# * \date February 2022
|
||||||
|
# * \brief This file contains snippets of script code to be executed as the .feature
|
||||||
|
# * file is processed.
|
||||||
|
# * The decorators Given/When/Then/Step can be used to associate a script snippet
|
||||||
|
# * with a pattern which is matched against the steps being executed.
|
||||||
|
# *****************************************************************************
|
||||||
|
from data.StatusAccount import StatusAccount
|
||||||
|
from processes.StatusLoginProcess import StatusLoginProcess
|
||||||
|
|
||||||
|
@Given("A Status Desktop |any| and |word| with |word| as a preference language")
|
||||||
|
def step(context,account,password,languageType):
|
||||||
|
|
||||||
|
# Create new data domain:
|
||||||
|
accountObj = StatusAccount(account, password)
|
||||||
|
|
||||||
|
# Create new process:
|
||||||
|
process = StatusLoginProcess(accountObj)
|
||||||
|
|
||||||
|
# Set needed context properties:
|
||||||
|
context.userData['process'] = process
|
||||||
|
context.userData['account'] = accountObj
|
||||||
|
context.userData['languageType'] = languageType
|
||||||
|
|
||||||
|
# Verify process can be executed:
|
||||||
|
test.verify(process.can_execute_process(), "Not possible to start login process. Check if expected Login Screen is available.")
|
||||||
|
|
||||||
|
@When("the user tries to login with valid credentials")
|
||||||
|
def step(context):
|
||||||
|
loginProcess = context.userData['process']
|
||||||
|
|
||||||
|
# Check valid process behavior:
|
||||||
|
loginProcess.execute_process(True)
|
||||||
|
|
||||||
|
@When("the user tries to login with invalid credentials")
|
||||||
|
def step(context):
|
||||||
|
loginProcess = context.userData['process']
|
||||||
|
|
||||||
|
# Check invalid process behavior:
|
||||||
|
loginProcess.execute_process(False)
|
||||||
|
|
||||||
|
@Then("the user is able to login to Status Desktop application")
|
||||||
|
def step(context):
|
||||||
|
get_process_result(context)
|
||||||
|
|
||||||
|
@Then("the user is NOT able to login to Status Desktop application")
|
||||||
|
def step(context):
|
||||||
|
get_process_result(context)
|
||||||
|
|
||||||
|
# Common:
|
||||||
|
def get_process_result(context):
|
||||||
|
loginProcess = context.userData['process']
|
||||||
|
result, description = loginProcess.get_process_result()
|
||||||
|
test.verify(result, description)
|
|
@ -0,0 +1,9 @@
|
||||||
|
AUT=nim_status_client
|
||||||
|
ENVVARS=envvars
|
||||||
|
HOOK_SUB_PROCESSES=false
|
||||||
|
IMPLICITAUTSTART=0
|
||||||
|
LANGUAGE=Python
|
||||||
|
OBJECTMAPSTYLE=script
|
||||||
|
TEST_CASES=tst_statusLoginPassword
|
||||||
|
VERSION=3
|
||||||
|
WRAPPERS=Qt
|
|
@ -0,0 +1,39 @@
|
||||||
|
#******************************************************************************
|
||||||
|
# Status.im
|
||||||
|
#*****************************************************************************/
|
||||||
|
#/**
|
||||||
|
# * \file test.feature
|
||||||
|
# *
|
||||||
|
# * \test Status Desktop - Login
|
||||||
|
# * \date February 2022
|
||||||
|
# **
|
||||||
|
# *****************************************************************************/
|
||||||
|
|
||||||
|
Feature: Status Desktop login
|
||||||
|
|
||||||
|
As a user I want to login into the Status Desktop application.
|
||||||
|
|
||||||
|
The following scenarios cover login by using a password.
|
||||||
|
|
||||||
|
Scenario Outline: User tries to login with a valid password
|
||||||
|
|
||||||
|
Given A Status Desktop <account> and <password> with <languageType> as a preference language
|
||||||
|
When the user tries to login with valid credentials
|
||||||
|
Then the user is able to login to Status Desktop application
|
||||||
|
Examples:
|
||||||
|
| account | password | languageType |
|
||||||
|
| Athletic Prime Springtail | Test_1234 | english |
|
||||||
|
| Nervous Pesky Serpent | Test_1234 | english |
|
||||||
|
| Granular Diligent Gorilla | Test_1234 | english |
|
||||||
|
|
||||||
|
|
||||||
|
Scenario Outline: User tries to login with an invalid password
|
||||||
|
|
||||||
|
Given A Status Desktop <account> and <password> with <languageType> as a preference language
|
||||||
|
When the user tries to login with invalid credentials
|
||||||
|
Then the user is NOT able to login to Status Desktop application
|
||||||
|
Examples:
|
||||||
|
| account | password | languageType |
|
||||||
|
| Athletic Prime Springtail | Invalid34 | english |
|
||||||
|
| Granular Diligent Gorilla | Testpwd | english |
|
||||||
|
| Nervous Pesky Serpent | WrongPSW | english |
|
|
@ -0,0 +1,8 @@
|
||||||
|
source(findFile('scripts', 'python/bdd.py'))
|
||||||
|
|
||||||
|
setupHooks('../shared/scripts/bdd_hooks.py')
|
||||||
|
collectStepDefinitions('./steps', '../shared/steps')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
testSettings.throwOnFailure = True
|
||||||
|
runFeatureFile('test.feature')
|