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)