From 98beef138f849729c947ee485f146a2a4a1dfa98 Mon Sep 17 00:00:00 2001 From: Anton Danchenko Date: Thu, 5 Oct 2017 22:41:17 +0300 Subject: [PATCH] added 'send transaction initiated from the DApp' and 'send transaction with invalid password' tests, basetestcase reworked for smooth local runs added 'send transaction initiated from the DApp' and 'send transaction with invalid password' tests, basetestcase reworked for smooth local runs --- test/appium/tests/basetestcase.py | 114 ++++++++++++++----- test/appium/tests/conftest.py | 4 + test/appium/tests/preconditions.py | 3 + test/appium/tests/test_sanity.py | 70 +++++++++--- test/appium/views/base_element.py | 10 +- test/appium/views/base_view.py | 28 +++-- test/appium/views/chats.py | 20 ++-- test/appium/views/contacts.py | 24 ++++ test/appium/views/home.py | 4 +- test/appium/views/web_views/auction_house.py | 43 +++++++ test/appium/views/web_views/base_web_view.py | 30 +++++ 11 files changed, 285 insertions(+), 65 deletions(-) create mode 100644 test/appium/views/contacts.py create mode 100644 test/appium/views/web_views/auction_house.py create mode 100644 test/appium/views/web_views/base_web_view.py diff --git a/test/appium/tests/basetestcase.py b/test/appium/tests/basetestcase.py index 44cd69e448..ad91b4060e 100644 --- a/test/appium/tests/basetestcase.py +++ b/test/appium/tests/basetestcase.py @@ -6,7 +6,10 @@ from appium import webdriver from abc import ABCMeta, \ abstractmethod import hmac +import re +import subprocess from hashlib import md5 +from selenium.common.exceptions import WebDriverException class AbstractTestCase: @@ -26,21 +29,8 @@ class AbstractTestCase: return 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % (self.sauce_username, self.sauce_access_key) @property - def capabilities_sauce_lab(self): - - desired_caps = dict() - desired_caps['platformName'] = 'Android' - desired_caps['appiumVersion'] = '1.6.5' - desired_caps['platformVersion'] = '6.0' - desired_caps['deviceName'] = 'Android GoogleAPI Emulator' - desired_caps['app'] = pytest.config.getoption('apk') - desired_caps['browserName'] = '' - desired_caps['deviceOrientation'] = "portrait" - desired_caps['name'] = tests_data.name - desired_caps['build'] = pytest.config.getoption('build') - desired_caps['idleTimeout'] = 1000 - desired_caps['commandTimeout'] = 600 - return desired_caps + def executor_local(self): + return 'http://localhost:4723/wd/hub' def get_public_url(self, driver): token = hmac.new(bytes(self.sauce_username + ":" + self.sauce_access_key, 'latin-1'), @@ -53,18 +43,42 @@ class AbstractTestCase: pytest.config.getoption('build'))) print(self.get_public_url(driver)) + def add_local_devices_to_capabilities(self): + updated_capabilities = list() + raw_out = re.split(r'[\r\\n]+', str(subprocess.check_output(['adb', 'devices'])).rstrip()) + for line in raw_out[1:]: + serial = re.findall(r"([\d.\d:]*\d+)", line) + if serial: + capabilities = self.capabilities_local + capabilities['udid'] = serial[0] + updated_capabilities.append(capabilities) + return updated_capabilities + @property - def executor_local(self): - return 'http://localhost:4723/wd/hub' + def capabilities_sauce_lab(self): + desired_caps = dict() + desired_caps['app'] = pytest.config.getoption('apk') + desired_caps['build'] = pytest.config.getoption('build') + desired_caps['name'] = tests_data.name + desired_caps['platformName'] = 'Android' + desired_caps['appiumVersion'] = '1.7.1' + desired_caps['platformVersion'] = '6.0' + desired_caps['deviceName'] = 'Android GoogleAPI Emulator' + desired_caps['deviceOrientation'] = "portrait" + desired_caps['commandTimeout'] = 600 + desired_caps['idleTimeout'] = 1000 + return desired_caps @property def capabilities_local(self): desired_caps = dict() + desired_caps['app'] = pytest.config.getoption('apk') desired_caps['deviceName'] = 'nexus_5' desired_caps['platformName'] = 'Android' - desired_caps['appiumVersion'] = '1.6.5' + desired_caps['appiumVersion'] = '1.7.1' desired_caps['platformVersion'] = '6.0' - desired_caps['app'] = pytest.config.getoption('apk') + desired_caps['commandTimeout'] = 600 + desired_caps['idleTimeout'] = 1000 return desired_caps @abstractmethod @@ -75,40 +89,82 @@ class AbstractTestCase: def teardown_method(self, method): raise NotImplementedError('Should be overridden from a child class') + @property + def environment(self): + return pytest.config.getoption('env') -class SingleDeviceTestCase(AbstractTestCase): + +class LocalMultiplyDeviceTestCase(AbstractTestCase): def setup_method(self, method): - self.driver = webdriver.Remote(self.executor_sauce_lab, - self.capabilities_sauce_lab) - self.driver.implicitly_wait(20) + capabilities = self.add_local_devices_to_capabilities() + self.driver_1 = webdriver.Remote(self.executor_local, capabilities[0]) + self.driver_2 = webdriver.Remote(self.executor_local, capabilities[1]) + for driver in self.driver_1, self.driver_2: + driver.implicitly_wait(10) def teardown_method(self, method): - self.print_sauce_lab_info(self.driver) - self.driver.quit() + for driver in self.driver_1, self.driver_2: + try: + driver.quit() + except WebDriverException: + pass -class MultiplyDeviceTestCase(AbstractTestCase): +class SauceMultiplyDeviceTestCase(AbstractTestCase): @classmethod def setup_class(cls): cls.loop = asyncio.get_event_loop() def setup_method(self, method): - self.driver_1, \ self.driver_2 = self.loop.run_until_complete(start_threads(2, webdriver.Remote, self.executor_sauce_lab, self.capabilities_sauce_lab)) for driver in self.driver_1, self.driver_2: - driver.implicitly_wait(20) + driver.implicitly_wait(10) def teardown_method(self, method): for driver in self.driver_1, self.driver_2: self.print_sauce_lab_info(driver) - driver.quit() + try: + driver.quit() + except WebDriverException: + pass @classmethod def teardown_class(cls): cls.loop.close() + + +class SingleDeviceTestCase(AbstractTestCase): + + def setup_method(self, method): + + capabilities = {'local': {'executor': self.executor_local, + 'capabilities': self.capabilities_local}, + 'sauce': {'executor': self.executor_sauce_lab, + 'capabilities': self.capabilities_sauce_lab}} + + self.driver = webdriver.Remote(capabilities[self.environment]['executor'], + capabilities[self.environment]['capabilities']) + self.driver.implicitly_wait(10) + + def teardown_method(self, method): + if self.environment == 'sauce': + self.print_sauce_lab_info(self.driver) + try: + self.driver.quit() + except WebDriverException: + pass + + +environments = {'local': LocalMultiplyDeviceTestCase, + 'sauce': SauceMultiplyDeviceTestCase} + + +class MultiplyDeviceTestCase(environments[pytest.config.getoption('env')]): + + pass diff --git a/test/appium/tests/conftest.py b/test/appium/tests/conftest.py index c79f288fb9..b9e01fe46d 100644 --- a/test/appium/tests/conftest.py +++ b/test/appium/tests/conftest.py @@ -31,6 +31,10 @@ def pytest_addoption(parser): action='store', default='sauce-storage:' + latest_apk, help='Please provide url or local path to apk') + parser.addoption('--env', + action='store', + default='sauce', + help='Please specify environment: local/sauce') def pytest_runtest_setup(item): diff --git a/test/appium/tests/preconditions.py b/test/appium/tests/preconditions.py index 83d6fab8df..08e5007d6f 100644 --- a/test/appium/tests/preconditions.py +++ b/test/appium/tests/preconditions.py @@ -19,6 +19,9 @@ def recover_access(chats, passphrase, password, username): login.password_input.send_keys(password) login.confirm_recover_access.click() recovered_user = login.element_by_text(username, 'button') + login.confirm() recovered_user.click() login.password_input.send_keys(password) login.sign_in_button.click() + login.find_full_text('Chats', 60) + diff --git a/test/appium/tests/test_sanity.py b/test/appium/tests/test_sanity.py index 2fc0dd3368..ecc492b517 100644 --- a/test/appium/tests/test_sanity.py +++ b/test/appium/tests/test_sanity.py @@ -1,4 +1,5 @@ import pytest +import time from tests.basetestcase import SingleDeviceTestCase from views.home import HomeView from tests.preconditions import set_password_as_new_user, recover_access @@ -6,7 +7,7 @@ from tests import basic_user, transaction_users @pytest.mark.sanity -class TestSanity(SingleDeviceTestCase): +class TestAccess(SingleDeviceTestCase): def test_recover_access(self): home = HomeView(self.driver) @@ -23,7 +24,7 @@ class TestSanity(SingleDeviceTestCase): recovered_user.click() login.password_input.send_keys(basic_user['password']) login.sign_in_button.click() - home.find_full_text('Chats', 10) + home.find_full_text('Chats', 60) @pytest.mark.parametrize("verification", ["invalid", "valid"]) def test_sign_in(self, verification): @@ -62,9 +63,10 @@ class TestSanity(SingleDeviceTestCase): home.confirm() home.find_full_text(verifications[verification]["outcome"]) - @pytest.mark.parametrize("test, recipient, sender", [('group_chat', 'A_USER', 'B_USER'), + @pytest.mark.parametrize("test, recipient, sender", [('wrong_password', 'A_USER', 'B_USER'), + ('group_chat', 'A_USER', 'B_USER'), ('one_to_one_chat', 'B_USER', 'A_USER')], - ids=['group_chat', 'one_to_one_chat']) + ids=['group_chat', 'one_to_one_chat', 'wrong_password']) def test_send_transaction(self, test, recipient, sender): home = HomeView(self.driver) set_password_as_new_user(home) @@ -102,18 +104,52 @@ class TestSanity(SingleDeviceTestCase): chats.send_funds_button.click() chats.first_recipient_button.click() - chats.send_int_as_keyevent(0) - chats.send_dot_as_keyevent() - chats.send_int_as_keyevent(1) + chats.send_as_keyevent('0,1') chats.send_message_button.click() - chats.confirm_transaction_button.wait_for_element(60) - chats.confirm_transaction_button.click() - chats.password_input.send_keys(transaction_users[sender]['password']) - chats.confirm_button.click() - chats.got_it_button.click() + chats.sign_transaction_button.wait_for_element(20) + chats.sign_transaction_button.click() - chats.find_full_text('0.1') - chats.find_full_text('Sent', 60) - if test == 'group_chat': - chats.find_full_text('to ' + transaction_users[recipient]['username'], 60) - chats.verify_balance_is_updated(initial_balance_recipient, recipient_address) + if test == 'wrong_password': + chats.enter_password_input.send_keys('invalid') + chats.sign_transaction_button.click() + chats.find_full_text('Wrong password', 20) + + else: + chats.enter_password_input.send_keys(transaction_users[recipient]['password']) + chats.sign_transaction_button.click() + chats.find_full_text('0.1') + chats.find_full_text('Sent', 60) + if test == 'group_chat': + chats.find_full_text('to ' + transaction_users[recipient]['username'], 60) + chats.verify_balance_is_updated(initial_balance_recipient, recipient_address) + + def test_send_transaction_from_daap(self): + home = HomeView(self.driver) + set_password_as_new_user(home) + chats = home.get_chats() + + address = transaction_users['B_USER']['address'] + initial_balance = chats.get_balance(address) + recover_access(chats, + transaction_users['B_USER']['passphrase'], + transaction_users['B_USER']['password'], + transaction_users['B_USER']['username']) + if chats.get_balance(address) < 1000000000000000000: + chats.get_donate(address) + + contacts = chats.contacts_button.click() + auction_house = contacts.auction_house_button.click() + + auction_house.toggle_navigation_button.click() + auction_house.new_auction_button.click() + auction_house.name_to_reserve_input.click() + auction_name = time.strftime('%Y-%m-%d-%H-%M') + auction_house.send_as_keyevent(auction_name) + auction_house.register_name_button.click() + + chats.sign_transaction_button.wait_for_element(20) + chats.sign_transaction_button.click() + chats.enter_password_input.send_keys(transaction_users['B_USER']['password']) + chats.sign_transaction_button.click() + auction_house.find_full_text('You are the proud owner of the name: ' + auction_name, 120) + chats.verify_balance_is_updated(initial_balance, address) diff --git a/test/appium/views/base_element.py b/test/appium/views/base_element.py index 69f072cf31..e4bd40e49e 100644 --- a/test/appium/views/base_element.py +++ b/test/appium/views/base_element.py @@ -40,18 +40,22 @@ class BaseElement(object): logging.info('Looking for %s' % self.name) return self.driver.find_element(self.locator.by, self.locator.value) + def find_elements(self): + logging.info('Looking for %s' % self.name) + return self.driver.find_elements(self.locator.by, self.locator.value) + def wait_for_element(self, seconds=10): return WebDriverWait(self.driver, seconds)\ .until(expected_conditions.presence_of_element_located((self.locator.by, self.locator.value))) def scroll_to_element(self): + action = TouchAction(self.driver) for _ in range(5): try: self.find_element() break except NoSuchElementException: logging.info('Scrolling to %s' % self.name) - action = TouchAction(self.driver) action.press(x=0, y=1000).move_to(x=200, y=-1000).release().perform() def is_element_present(self, sec=5): @@ -75,6 +79,10 @@ class BaseEditBox(BaseElement): self.find_element().send_keys(value) logging.info('Type %s to %s' % (value, self.name)) + def set_value(self, value): + self.find_element().set_value(value) + logging.info('Set %s to %s' % (value, self.name)) + class BaseText(BaseElement): diff --git a/test/appium/views/base_view.py b/test/appium/views/base_view.py index 3c5dc80b4d..6a2e951916 100644 --- a/test/appium/views/base_view.py +++ b/test/appium/views/base_view.py @@ -11,6 +11,12 @@ class BackButton(BaseButton): super(BackButton, self).__init__(driver) self.locator = self.Locator.xpath_selector("//*[@content-desc='toolbar-back-button']") + def click(self): + self.wait_for_element(30) + self.find_element().click() + logging.info('Tap on %s' % self.name) + return self.navigate() + class ContactsButton(BaseButton): @@ -18,6 +24,10 @@ class ContactsButton(BaseButton): super(ContactsButton, self).__init__(driver) self.locator = self.Locator.xpath_selector("//*[@text='Contacts']") + def navigate(self): + from views.contacts import ContactsViewObject + return ContactsViewObject(self.driver) + class BaseViewObject(object): @@ -27,23 +37,25 @@ class BaseViewObject(object): self.contacts_button = ContactsButton(self.driver) def confirm(self): + logging.info("Tap 'Confirm' on native keyboard") self.driver.keyevent(66) - def send_int_as_keyevent(self, integer): - keys = {0: 7, 1: 8, 2: 9, 3: 10, 4: 11, - 5: 12, 6: 13, 7: 14, 8: 15, 9: 16} - time.sleep(2) - self.driver.keyevent(keys[integer]) - - def send_dot_as_keyevent(self): - self.driver.keyevent(55) + def send_as_keyevent(self, string): + keys = {'0': 7, '1': 8, '2': 9, '3': 10, '4': 11, '5': 12, '6': 13, '7': 14, '8': 15, '9': 16, + ',': 55, '-': 69} + for i in string: + logging.info("Tap '%s' on native keyboard" % i) + time.sleep(1) + self.driver.keyevent(keys[i]) def find_full_text(self, text, wait_time=60): + logging.info("Looking for full text: '%s'" % text) element = BaseElement(self.driver) element.locator = element.Locator.xpath_selector('//*[@text="' + text + '"]') return element.wait_for_element(wait_time) def find_text_part(self, text, wait_time=60): + logging.info("Looking for a text part: '%s'" % text) element = BaseElement(self.driver) element.locator = element.Locator.xpath_selector('//*[contains(@text, "' + text + '")]') return element.wait_for_element(wait_time) diff --git a/test/appium/views/chats.py b/test/appium/views/chats.py index a07d652d72..7dfcda6cfa 100644 --- a/test/appium/views/chats.py +++ b/test/appium/views/chats.py @@ -148,7 +148,6 @@ class SendMessageButton(BaseButton): self.locator = self.Locator.accessibility_id("send-message-button") def click(self): - time.sleep(10) self.find_element().click() logging.info('Tap on %s' % self.name) @@ -171,14 +170,14 @@ class SendFundsButton(BaseButton): def __init__(self, driver): super(SendFundsButton.FirstRecipient, self).__init__(driver) - self.locator = self.Locator.xpath_selector('//android.view.ViewGroup[4]//' - 'android.widget.ImageView[@content-desc="chat-icon"]') + self.locator = self.Locator.xpath_selector("//*[@text='Choose recipient']/.." + "//android.widget.ImageView[@content-desc='chat-icon']") - class ConfirmTransactionButton(BaseButton): + class SignTransactionButton(BaseButton): def __init__(self, driver): - super(SendFundsButton.ConfirmTransactionButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//*[@text='CONFIRM TRANSACTION']") + super(SendFundsButton.SignTransactionButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//*[@text='SIGN TRANSACTION']") class PasswordInput(BaseEditBox): @@ -186,6 +185,12 @@ class SendFundsButton(BaseButton): super(SendFundsButton.PasswordInput, self).__init__(driver) self.locator = self.Locator.xpath_selector("//*[@text='Password']") + class EnterPasswordInput(BaseEditBox): + + def __init__(self, driver): + super(SendFundsButton.EnterPasswordInput, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//android.widget.EditText[@NAF='true']") + class ConfirmButton(BaseButton): def __init__(self, driver): @@ -233,9 +238,10 @@ class ChatsViewObject(BaseViewObject): self.send_funds_button = SendFundsButton(self.driver) self.first_recipient_button = SendFundsButton.FirstRecipient(self.driver) - self.confirm_transaction_button = SendFundsButton.ConfirmTransactionButton(self.driver) + self.sign_transaction_button = SendFundsButton.SignTransactionButton(self.driver) self.confirm_button = SendFundsButton.ConfirmButton(self.driver) self.password_input = SendFundsButton.PasswordInput(self.driver) + self.enter_password_input = SendFundsButton.EnterPasswordInput(self.driver) self.got_it_button = SendFundsButton.GotItButton(self.driver) self.new_contact_button = NewContactButton(self.driver) diff --git a/test/appium/views/contacts.py b/test/appium/views/contacts.py new file mode 100644 index 0000000000..e6cb54f3df --- /dev/null +++ b/test/appium/views/contacts.py @@ -0,0 +1,24 @@ +from views.base_element import BaseElement, BaseButton, BaseEditBox, BaseText +import logging +import time +import pytest + + +class AuctionHouseButton(BaseButton): + + def __init__(self, driver): + super(AuctionHouseButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "(//android.widget.TextView[@text='Auction House'])[1]") + + def navigate(self): + from views.web_views.auction_house import AuctionHouseWebView + return AuctionHouseWebView(self.driver) + + +class ContactsViewObject(object): + + def __init__(self, driver): + self.driver = driver + + self.auction_house_button = AuctionHouseButton(self.driver) diff --git a/test/appium/views/home.py b/test/appium/views/home.py index 20f3b2f731..51d8d8b56b 100644 --- a/test/appium/views/home.py +++ b/test/appium/views/home.py @@ -32,7 +32,7 @@ class RequestPasswordIcon(BaseButton): self.locator = self.Locator.xpath_selector("//*[@content-desc='request-password']") def click(self): - self.wait_for_element(60) + self.wait_for_element(10) self.find_element().click() logging.info('Tap on %s' % self.name) return self.navigate() @@ -44,12 +44,10 @@ class HomeView(BaseViewObject): super(HomeView, self).__init__(driver) self.continue_button_apk = ContinueButtonAPK(driver) self.ok_button_apk = OkButtonAPK(driver) - for i in self.ok_button_apk, self.continue_button_apk: try: i.click() except (NoSuchElementException, TimeoutException): pass - self.chat_request_input = ChatRequestInput(driver) self.request_password_icon = RequestPasswordIcon(driver) diff --git a/test/appium/views/web_views/auction_house.py b/test/appium/views/web_views/auction_house.py new file mode 100644 index 0000000000..ae3d98e0e5 --- /dev/null +++ b/test/appium/views/web_views/auction_house.py @@ -0,0 +1,43 @@ +from views.web_views.base_web_view import * + + +class ToggleNavigationButton(BaseButton): + + def __init__(self, driver): + super(ToggleNavigationButton, self).__init__(driver) + self.locator = self.Locator.accessibility_id('Toggle navigation ') + + class NewAuctionButton(BaseButton): + def __init__(self, driver): + super(ToggleNavigationButton.NewAuctionButton, self).__init__(driver) + self.locator = self.Locator.accessibility_id('New Auction') + + +class ReserveAssetName(BaseElement): + + class NameToReserveInput(BaseEditBox, BaseButton): + + def __init__(self, driver): + super(ReserveAssetName.NameToReserveInput, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + '(//android.widget.EditText[@content-desc="eg MyFamousWallet.eth"])[1]') + + class RegisterNameButton(BaseButton): + + def __init__(self, driver): + super(ReserveAssetName.RegisterNameButton, self).__init__(driver) + self.locator = self.Locator.accessibility_id('Register Name') + + +class AuctionHouseWebView(BaseWebViewObject): + + def __init__(self, driver): + super(AuctionHouseWebView, self).__init__(driver) + self.driver = driver + self.wait_for_page_loaded() + + self.toggle_navigation_button = ToggleNavigationButton(self.driver) + self.new_auction_button = ToggleNavigationButton.NewAuctionButton(self.driver) + + self.name_to_reserve_input = ReserveAssetName.NameToReserveInput(self.driver) + self.register_name_button = ReserveAssetName.RegisterNameButton(self.driver) diff --git a/test/appium/views/web_views/base_web_view.py b/test/appium/views/web_views/base_web_view.py new file mode 100644 index 0000000000..7925b7304f --- /dev/null +++ b/test/appium/views/web_views/base_web_view.py @@ -0,0 +1,30 @@ +from views.base_view import * + + +class ProgressBarIcon(BaseElement): + + def __init__(self, driver): + super(ProgressBarIcon, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//android.widget.ProgressBar") + + +class BaseWebViewObject(BaseViewObject): + + def __init__(self, driver): + super(BaseWebViewObject, self).__init__(driver) + self.driver = driver + + self.progress_bar_icon = ProgressBarIcon(self.driver) + + def wait_for_page_loaded(self, wait_time=20): + counter = 0 + while self.progress_bar_icon.is_element_present(5): + time.sleep(1) + counter += 1 + if counter > wait_time: + pytest.fail("Page is not loaded during %s seconds" % wait_time) + + def find_full_text(self, text, wait_time=60): + element = BaseElement(self.driver) + element.locator = element.Locator.xpath_selector('//*[@content-desc="' + text + '"]') + return element.wait_for_element(wait_time)