From e69d9980713d37a5891e02010462021e76d65a81 Mon Sep 17 00:00:00 2001 From: Anton Danchenko Date: Mon, 28 Aug 2017 13:02:20 +0300 Subject: [PATCH] e2e-tests, android: added infrastructure for running e2e tests, added test send/receive in chat, added request password tests, updated gitignore to exclude python related files. --- .gitignore | 4 + test/appium/AppiumTests.iml | 78 ------------- test/appium/Jenkinsfile | 8 ++ test/appium/pom.xml | 56 --------- test/appium/pytest.ini | 3 + test/appium/requirements.txt | 22 ++++ test/appium/src/test/java/app/StatusApp.java | 17 --- .../src/test/java/screens/AbstractScreen.java | 32 ------ .../src/test/java/screens/ChatScreen.java | 75 ------------ .../src/test/java/tests/AbstractTest.java | 62 ---------- .../src/test/java/tests/InitialRunTest.java | 28 ----- .../java/utility/AppiumDriverBuilder.java | 58 ---------- test/appium/tests/__init__.py | 20 ++++ test/appium/tests/basetestcase.py | 107 ++++++++++++++++++ test/appium/tests/conftest.py | 12 ++ test/appium/tests/preconditions.py | 10 ++ test/appium/tests/test_chats.py | 33 ++++++ test/appium/tests/test_sanity.py | 28 +++++ test/appium/views/__init__.py | 0 test/appium/views/base_element.py | 75 ++++++++++++ test/appium/views/base_view.py | 27 +++++ test/appium/views/chats.py | 89 +++++++++++++++ test/appium/views/home.py | 39 +++++++ test/appium/views/profile.py | 25 ++++ 24 files changed, 502 insertions(+), 406 deletions(-) delete mode 100644 test/appium/AppiumTests.iml create mode 100644 test/appium/Jenkinsfile delete mode 100644 test/appium/pom.xml create mode 100644 test/appium/pytest.ini create mode 100644 test/appium/requirements.txt delete mode 100644 test/appium/src/test/java/app/StatusApp.java delete mode 100644 test/appium/src/test/java/screens/AbstractScreen.java delete mode 100644 test/appium/src/test/java/screens/ChatScreen.java delete mode 100644 test/appium/src/test/java/tests/AbstractTest.java delete mode 100644 test/appium/src/test/java/tests/InitialRunTest.java delete mode 100644 test/appium/src/test/java/utility/AppiumDriverBuilder.java create mode 100644 test/appium/tests/__init__.py create mode 100644 test/appium/tests/basetestcase.py create mode 100644 test/appium/tests/conftest.py create mode 100644 test/appium/tests/preconditions.py create mode 100644 test/appium/tests/test_chats.py create mode 100644 test/appium/tests/test_sanity.py create mode 100644 test/appium/views/__init__.py create mode 100644 test/appium/views/base_element.py create mode 100644 test/appium/views/base_view.py create mode 100644 test/appium/views/chats.py create mode 100644 test/appium/views/home.py create mode 100644 test/appium/views/profile.py diff --git a/.gitignore b/.gitignore index cc3ade1664..4c2015b620 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,7 @@ status-dev-cli #ios ios/Pods ios/StatusIm.xcworkspace + +#python +*.pyc +*.cache diff --git a/test/appium/AppiumTests.iml b/test/appium/AppiumTests.iml deleted file mode 100644 index 4d44f624cf..0000000000 --- a/test/appium/AppiumTests.iml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/appium/Jenkinsfile b/test/appium/Jenkinsfile new file mode 100644 index 0000000000..f864bf45a9 --- /dev/null +++ b/test/appium/Jenkinsfile @@ -0,0 +1,8 @@ +node {sauce('6269510b-13f3-4019-b156-c2c835f3a408') { + try {sh '/usr/local/bin/python3 -m pytest -m sanity -n5 --apk apkUrl --build ${JOB_NAME}__${BUILD_NUMBER}' + } + finally { + saucePublisher() + junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: '*.xml' } + } +} diff --git a/test/appium/pom.xml b/test/appium/pom.xml deleted file mode 100644 index 2680f725e2..0000000000 --- a/test/appium/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - 4.0.0 - - im.status - AppiumTests - 1.0-SNAPSHOT - - - - io.appium - java-client - 5.0.0-BETA7 - - - org.seleniumhq.selenium - selenium-java - 3.3.1 - - - junit - junit - 4.12 - - - - com.saucelabs - sauce_junit - 2.1.23 - - - - - - - maven-compiler-plugin - 3.6.1 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.20 - - methods - 4 - - - - - \ No newline at end of file diff --git a/test/appium/pytest.ini b/test/appium/pytest.ini new file mode 100644 index 0000000000..f6ee4716ba --- /dev/null +++ b/test/appium/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +norecursedirs = .git views +addopts = -s -v --junitxml=result.xml --tb=short diff --git a/test/appium/requirements.txt b/test/appium/requirements.txt new file mode 100644 index 0000000000..3579f46760 --- /dev/null +++ b/test/appium/requirements.txt @@ -0,0 +1,22 @@ +aiohttp==2.2.3 +allpairspy==2.3.0 +apipkg==1.4 +Appium-Python-Client==0.24 +async-timeout==1.2.1 +asyncio==3.4.3 +certifi==2017.7.27.1 +chardet==3.0.4 +execnet==1.4.1 +idna==2.5 +lxml==3.8.0 +multidict==3.1.3 +namedlist==1.7 +py==1.4.34 +pytest==3.1.3 +pytest-xdist==1.18.2 +requests==2.18.3 +sauceclient==1.0.0 +selenium==2.53.6 +six==1.10.0 +urllib3==1.22 +yarl==0.12.0 diff --git a/test/appium/src/test/java/app/StatusApp.java b/test/appium/src/test/java/app/StatusApp.java deleted file mode 100644 index 2586ac952d..0000000000 --- a/test/appium/src/test/java/app/StatusApp.java +++ /dev/null @@ -1,17 +0,0 @@ -package app; - -import io.appium.java_client.AppiumDriver; -import screens.ChatScreen; - - -public class StatusApp { - - private final AppiumDriver driver; - - public StatusApp(AppiumDriver driver) { - this.driver = driver; - } - - public ChatScreen ChatScreen() { return new ChatScreen(driver); } - -} diff --git a/test/appium/src/test/java/screens/AbstractScreen.java b/test/appium/src/test/java/screens/AbstractScreen.java deleted file mode 100644 index 1c853b0d24..0000000000 --- a/test/appium/src/test/java/screens/AbstractScreen.java +++ /dev/null @@ -1,32 +0,0 @@ -package screens; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileElement; -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import org.openqa.selenium.By; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -public class AbstractScreen { - - protected final AppiumDriver driver; - protected WebDriverWait wait; - - - public AbstractScreen(AppiumDriver driver) { - this.driver = driver; - wait = new WebDriverWait(driver,45); - PageFactory.initElements(new AppiumFieldDecorator(driver), this); - } - - public MobileElement findElementWithTimeout(By by, int timeOutInSeconds) { - return (MobileElement)(new WebDriverWait(driver, timeOutInSeconds)).until(ExpectedConditions.presenceOfElementLocated(by)); - } - - protected void takeScreenShot(){ - driver.getScreenshotAs(OutputType.BASE64); - } -} diff --git a/test/appium/src/test/java/screens/ChatScreen.java b/test/appium/src/test/java/screens/ChatScreen.java deleted file mode 100644 index fd57729729..0000000000 --- a/test/appium/src/test/java/screens/ChatScreen.java +++ /dev/null @@ -1,75 +0,0 @@ -package screens; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.pagefactory.AndroidFindBy; -import org.openqa.selenium.By; -import org.openqa.selenium.InvalidSelectorException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.springframework.util.Assert; - - -public class ChatScreen extends AbstractScreen { - - public ChatScreen(AppiumDriver driver){ - super(driver); - } - - - @AndroidFindBy(id="android:id/button1") - public WebElement btnContinue; - - @AndroidFindBy(accessibility="request-password") - public WebElement requestPassword; - - @AndroidFindBy(accessibility="chat-message-input") - public WebElement inputChatMessage; - - @AndroidFindBy(accessibility="chat-send-button") - public WebElement btnSend; - - @AndroidFindBy(accessibility="request-phone") - public WebElement btnRequestPhone; - - @AndroidFindBy(accessibility="chat-cancel-response-button") - public WebElement btnCancelPasswordRequest; - - public void continueForRootedDevice() - { - if (isElementPresent(By.id("android:id/button1"))) { - btnContinue.click(); - } - } - - public void createPassword(String password){ - requestPassword.click(); - for (int i=0; i<2; i++){ - inputChatMessage.sendKeys(password); - btnSend.click(); - } - } - - public void verifyPasswordIsSet(){ - //button "tap to enter phone number" is shown - wait.until(ExpectedConditions.elementToBeClickable(btnRequestPhone)); - - //screen contains text "Phew that was hard" TODO: replace hardcoded string "Find a bug" check - Assert.isTrue(driver.getPageSource().contains("Find a bug"),"Text Phew that was hard is not found on screen"); - } - - public void verifyPasswordRequestIsVisible(){ - //button "tap to enter phone number" is shown - wait.until(ExpectedConditions.elementToBeClickable(requestPassword)); - //screen contains text "Phew that was hard" TODO: replace hardcoded string "Welcome to Status " check - Assert.isTrue(driver.getPageSource().contains("Welcome to Status"),"Welcome to Status is not found on screen"); - } - - - public boolean isElementPresent(By locator) { - try { - return driver.findElements(locator).size() > 0; - } catch (InvalidSelectorException ex) { - throw ex; - } - } -} \ No newline at end of file diff --git a/test/appium/src/test/java/tests/AbstractTest.java b/test/appium/src/test/java/tests/AbstractTest.java deleted file mode 100644 index a708907aaf..0000000000 --- a/test/appium/src/test/java/tests/AbstractTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package tests; - -import app.StatusApp; -import com.saucelabs.common.SauceOnDemandSessionIdProvider; -import io.appium.java_client.android.AndroidDriver; -import org.junit.*; -import org.junit.rules.TestName; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebDriver; -import utility.AppiumDriverBuilder; -import java.net.MalformedURLException; -import java.net.URL; -import com.saucelabs.junit.SauceOnDemandTestWatcher; -import com.saucelabs.common.SauceOnDemandAuthentication; - -public abstract class AbstractTest implements SauceOnDemandSessionIdProvider { - - private AndroidDriver driver; - protected StatusApp app; - - public static final String USERNAME = System.getenv("SAUCE_USERNAME"); - public static final String ACCESS_KEY = System.getenv("SAUCE_ACCESS_KEY"); - public static final String URL = "https://" + USERNAME + ":" + ACCESS_KEY + "@ondemand.saucelabs.com:443/wd/hub"; - // public String buildTag = System.getenv("JOB_NAME") + "__" + System.getenv("BUILD_NUMBER"); - - protected String sessionId; - - public SauceOnDemandAuthentication authentication = new SauceOnDemandAuthentication(USERNAME, ACCESS_KEY); - - @Rule - public SauceOnDemandTestWatcher resultReportingTestWatcher = new SauceOnDemandTestWatcher(this, authentication); - - @Rule - public TestName name = new TestName() { - public String getMethodName() { - return String.format("%s", super.getMethodName()); - } - }; - - - @Before - public void SetupRemote() throws MalformedURLException { - driver = AppiumDriverBuilder.forAndroid() - .withEndpoint(new URL(URL)) - .build(); - - app = new StatusApp(driver); - this.sessionId = (((RemoteWebDriver) driver).getSessionId()).toString(); - } - - @After - public void teardown(){ - //close the app - driver.quit(); - } - - @Override - public String getSessionId() { - return sessionId; - } - -} diff --git a/test/appium/src/test/java/tests/InitialRunTest.java b/test/appium/src/test/java/tests/InitialRunTest.java deleted file mode 100644 index ae5a0318ae..0000000000 --- a/test/appium/src/test/java/tests/InitialRunTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package tests; - - -import org.junit.*; - - - -public class InitialRunTest extends AbstractTest { - - - @Test - public void canRunAppTest() - { - app.ChatScreen().continueForRootedDevice(); - app.ChatScreen().verifyPasswordRequestIsVisible(); - } - - @Test - public void canCreatePasswordTest() - { - - app.ChatScreen().continueForRootedDevice(); - app.ChatScreen().createPassword("password"); - app.ChatScreen().verifyPasswordIsSet(); - } - - -} \ No newline at end of file diff --git a/test/appium/src/test/java/utility/AppiumDriverBuilder.java b/test/appium/src/test/java/utility/AppiumDriverBuilder.java deleted file mode 100644 index ed42a704fa..0000000000 --- a/test/appium/src/test/java/utility/AppiumDriverBuilder.java +++ /dev/null @@ -1,58 +0,0 @@ -package utility; - - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.android.AndroidDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; - - -import java.net.MalformedURLException; -import java.net.URL; -import java.sql.Timestamp; - - -public abstract class AppiumDriverBuilder { - - public String jobName = System.getenv("JOB_NAME"); - public String buildNumber = System.getenv("BUILD_NUMBER"); - protected URL endpoint; - DesiredCapabilities capabilities = new DesiredCapabilities(); - - public static AndroidDriverBuilder forAndroid() throws MalformedURLException { - return new AndroidDriverBuilder(); - } - - public static class AndroidDriverBuilder extends AppiumDriverBuilder { - - - - public AndroidDriver build() { - capabilities.setCapability("appiumVersion", "1.6.3"); - capabilities.setCapability("deviceName","Samsung Galaxy S4 Emulator"); - capabilities.setCapability("deviceOrientation", "portrait"); - capabilities.setCapability("browserName", ""); - capabilities.setCapability("platformVersion","4.4"); - capabilities.setCapability("platformName","Android"); - - jobName = (null == jobName) ? "Local run " : jobName; - buildNumber = (null == buildNumber) ? new Timestamp(System.currentTimeMillis()).toString() : buildNumber; - capabilities.setCapability("build", jobName + "__" + buildNumber); - - //read url to apk file from Jenkins property or from maven parameter - // example of maven run: mvn -DapkUrl=http://artifacts.status.im:8081/artifactory/nightlies-local/im.status.ethereum-baebbe.apk test - capabilities.setCapability("app", System.getProperty("apkUrl")); - return new AndroidDriver(endpoint, capabilities); - } - } - - public SELF withEndpoint(URL endpoint) { - this.endpoint = endpoint; - - return (SELF) this; - } - - public abstract DRIVER build(); - -} - diff --git a/test/appium/tests/__init__.py b/test/appium/tests/__init__.py new file mode 100644 index 0000000000..63727a34d8 --- /dev/null +++ b/test/appium/tests/__init__.py @@ -0,0 +1,20 @@ +import asyncio + + +@asyncio.coroutine +def start_threads(amount, func, *args): + features = dict() + loop = asyncio.get_event_loop() + for i in range(amount): + features['feature_' + str(i)] = loop.run_in_executor(None, func, *args) + for k in features: + features[k] = yield from features[k] + return (features[k] for k in features) + + +class TestData(object): + + def __init__(self): + self.name = None + +tests_data = TestData() diff --git a/test/appium/tests/basetestcase.py b/test/appium/tests/basetestcase.py new file mode 100644 index 0000000000..2bbf643046 --- /dev/null +++ b/test/appium/tests/basetestcase.py @@ -0,0 +1,107 @@ +import pytest +import sys +from tests import * +from os import environ +from appium import webdriver +from abc import ABCMeta, \ + abstractmethod +import hmac +from hashlib import md5 + + +class AbstractTestCase: + + __metaclass__ = ABCMeta + + @property + def sauce_access_key(self): + return environ.get('SAUCE_ACCESS_KEY') + + @property + def sauce_username(self): + return environ.get('SAUCE_USERNAME') + + @property + def executor_sauce_lab(self): + 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'] = 500 + return desired_caps + + def get_public_url(self, driver): + token = hmac.new(bytes(self.sauce_username + ":" + self.sauce_access_key, 'latin-1'), + bytes(driver.session_id, 'latin-1'), md5).hexdigest() + return "https://saucelabs.com/jobs/%s?auth=%s" % (driver.session_id, token) + + def print_sauce_lab_info(self, driver): + sys.stdout = sys.stderr + print("SauceOnDemandSessionID=%s job-name=%s" % (driver.session_id, + pytest.config.getoption('build'))) + print(self.get_public_url(driver)) + + @property + def executor_local(self): + return 'http://localhost:4723/wd/hub' + + @property + def capabilities_local(self): + desired_caps = dict() + desired_caps['deviceName'] = 'takoe' + desired_caps['platformName'] = 'Android' + desired_caps['appiumVersion'] = '1.6.5' + desired_caps['platformVersion'] = '6.0' + desired_caps['app'] = pytest.config.getoption('apk') + return desired_caps + + @abstractmethod + def setup_method(self, method): + raise NotImplementedError('Should be overridden from a child class') + + @abstractmethod + def teardown_method(self, method): + raise NotImplementedError('Should be overridden from a child class') + + +class SingleDeviceTestCase(AbstractTestCase): + + def setup_method(self, method): + self.driver = webdriver.Remote(self.executor_sauce_lab, + self.capabilities_sauce_lab) + self.driver.implicitly_wait(10) + + def teardown_method(self, method): + self.print_sauce_lab_info(self.driver) + self.driver.quit() + + +class MultiplyDeviceTestCase(AbstractTestCase): + + def setup_method(self, method): + + loop = asyncio.get_event_loop() + self.driver_1, \ + self.driver_2 = loop.run_until_complete(start_threads(2, + webdriver.Remote, + self.executor_sauce_lab, + self.capabilities_sauce_lab)) + loop.close() + for driver in self.driver_1, self.driver_2: + 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() diff --git a/test/appium/tests/conftest.py b/test/appium/tests/conftest.py new file mode 100644 index 0000000000..888c476806 --- /dev/null +++ b/test/appium/tests/conftest.py @@ -0,0 +1,12 @@ +from tests import tests_data +import time + + +def pytest_addoption(parser): + parser.addoption("--build", action="store", default='build_' + time.strftime('%Y_%m_%d_%H_%M'), + help="Specify build name") + parser.addoption('--apk', action='store', default=None, help='Please provide url or local path to apk') + + +def pytest_runtest_setup(item): + tests_data.name = item.name diff --git a/test/appium/tests/preconditions.py b/test/appium/tests/preconditions.py new file mode 100644 index 0000000000..2b7aaeb52c --- /dev/null +++ b/test/appium/tests/preconditions.py @@ -0,0 +1,10 @@ + + +def set_chat_for_users_from_scratch(*args): + for view in args: + view.request_password_icon.click() + view.type_message_edit_box.send_keys("qwerty1234") + view.confirm() + view.type_message_edit_box.send_keys("qwerty1234") + view.confirm() + view.find_text("Tap here to enter your phone number & I\'ll find your friends") diff --git a/test/appium/tests/test_chats.py b/test/appium/tests/test_chats.py new file mode 100644 index 0000000000..f41a560a30 --- /dev/null +++ b/test/appium/tests/test_chats.py @@ -0,0 +1,33 @@ +import pytest +from tests.basetestcase import MultiplyDeviceTestCase +from tests.preconditions import set_chat_for_users_from_scratch +from views.home import HomeView + + +@pytest.mark.sanity +class TestMultiplyDevices(MultiplyDeviceTestCase): + + def test_private_chat(self): + + device_1, device_2 = HomeView(self.driver_1), HomeView(self.driver_2) + set_chat_for_users_from_scratch(device_1, device_2) + + device_1.back_button.click() + chats_d1 = device_1.get_chats() + chats_d1.profile_button.click() + profile_d1 = chats_d1.profile_icon.click() + key = profile_d1.public_key_text.get_key() + + device_2.back_button.click() + chats_d2 = device_2.get_chats() + chats_d2.plus_button.click() + chats_d2.add_new_contact.click() + chats_d2.public_key_edit_box.send_keys(key) + chats_d2.confirm() + chats_d2.confirm_public_key_button.click() + + chats_d2.chat_message_input.send_keys('SOMETHING') + chats_d2.send_message_button.click() + + profile_d1.back_button.click() + profile_d1.find_text('SOMETHING') diff --git a/test/appium/tests/test_sanity.py b/test/appium/tests/test_sanity.py new file mode 100644 index 0000000000..10f307b127 --- /dev/null +++ b/test/appium/tests/test_sanity.py @@ -0,0 +1,28 @@ +import pytest +from tests.basetestcase import SingleDeviceTestCase +from views.home import HomeView + + +@pytest.mark.sanity +class TestSanity(SingleDeviceTestCase): + + @pytest.mark.parametrize("verification", ["short", "mismatch", "valid"]) + def test_password(self, verification): + + verifications = {"short": {"input": "qwe1", + "outcome": + "Password should be not less then 6 symbols."}, + "mismatch": {"input": "mismatch1234", + "outcome": + "Password confirmation doesn\'t match password."}, + "valid": {"input": "qwerty1234", + "outcome": + "Tap here to enter your phone number & I\'ll find your friends"}} + home = HomeView(self.driver) + home.request_password_icon.click() + home.type_message_edit_box.send_keys(verifications[verification]["input"]) + home.confirm() + if 'short' not in verification: + home.type_message_edit_box.send_keys(verifications["valid"]["input"]) + home.confirm() + home.find_text(verifications[verification]["outcome"]) diff --git a/test/appium/views/__init__.py b/test/appium/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/appium/views/base_element.py b/test/appium/views/base_element.py new file mode 100644 index 0000000000..519dfaee4a --- /dev/null +++ b/test/appium/views/base_element.py @@ -0,0 +1,75 @@ +from selenium.webdriver.common.by import By +from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions +import pytest + + +class BaseElement(object): + + class Locator(object): + + def __init__(self, by, value): + self.by = by + self.value = value + + @classmethod + def xpath_selector(locator, value): + return locator(By.XPATH, value) + + def __str__(self, *args, **kwargs): + return "%s:%s" % (self.by, self.value) + + def __init__(self, driver): + self.driver = driver + self.locator = None + + @property + def name(self): + return self.__class__.__name__ + + def navigate(self): + return None + + def find_element(self): + try: + return self.wait_for_element() + except (NoSuchElementException, TimeoutException): + pytest.fail("'%s' not found by %s '%s'" % ( + self.name, self.locator.by, self.locator.value), pytrace=False) + + def wait_for_element(self, seconds=30): + return WebDriverWait(self.driver, seconds)\ + .until(expected_conditions.presence_of_element_located((self.locator.by, self.locator.value))) + + +class BaseEditBox(BaseElement): + + def __init__(self, driver): + super(BaseEditBox, self).__init__(driver) + self.driver = driver + + def send_keys(self, value): + self.find_element().send_keys(value) + + +class BaseText(BaseElement): + + def __init__(self, driver): + super(BaseText, self).__init__(driver) + self.driver = driver + + @property + def text(self): + return self.find_element().text + + +class BaseButton(BaseElement): + + def __init__(self, driver): + super(BaseButton, self).__init__(driver) + self.driver = driver + + def click(self): + self.find_element().click() + return self.navigate() diff --git a/test/appium/views/base_view.py b/test/appium/views/base_view.py new file mode 100644 index 0000000000..5fa6f8e486 --- /dev/null +++ b/test/appium/views/base_view.py @@ -0,0 +1,27 @@ +from views.base_element import BaseElement, BaseButton + + +class BackButton(BaseButton): + + def __init__(self, driver): + super(BackButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//*[@content-desc='toolbar-back-button']") + + +class BaseViewObject(object): + + def __init__(self, driver): + self.driver = driver + self.back_button = BackButton(self.driver) + + def confirm(self): + self.driver.keyevent(66) + + def find_text(self, text): + element = BaseElement(self.driver) + element.locator = element.Locator.xpath_selector('//*[@text="' + text + '"]') + return element.wait_for_element(30) + + def get_chats(self): + from views.chats import ChatsViewObject + return ChatsViewObject(self.driver) diff --git a/test/appium/views/chats.py b/test/appium/views/chats.py new file mode 100644 index 0000000000..103d7f9962 --- /dev/null +++ b/test/appium/views/chats.py @@ -0,0 +1,89 @@ +from views.base_view import BaseViewObject +from views.base_element import * + + +class ProfileButton(BaseButton): + + def __init__(self, driver): + super(ProfileButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "//android.support.v4.view.ViewPager//android.view.ViewGroup[1]/android.widget.ImageView") + + +class ProfileIcon(BaseButton): + + def __init__(self, driver): + super(ProfileIcon, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "//android.widget.EditText/../android.view.ViewGroup") + + def navigate(self): + from views.profile import ProfileViewObject + return ProfileViewObject(self.driver) + + +class PlusButton(BaseButton): + + def __init__(self, driver): + super(PlusButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "//android.widget.TextView[@text='+']") + + +class AddNewContactButton(BaseButton): + + def __init__(self, driver): + super(AddNewContactButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "//android.widget.TextView[@text='Add new contact']") + + +class PublicKeyEditBox(BaseEditBox): + + def __init__(self, driver): + super(PublicKeyEditBox, self).__init__(driver) + self.locator = \ + self.Locator.xpath_selector("//android.widget.EditText[@NAF='true']") + + +class ConfirmPublicKeyButton(BaseButton): + + def __init__(self, driver): + super(ConfirmPublicKeyButton, self).__init__(driver) + self.locator = \ + self.Locator.xpath_selector("//android.widget.TextView[@text='Add new contact']" + "/following-sibling::android.view.ViewGroup/" + "android.widget.ImageView") + + +class ChatMessageInput(BaseEditBox): + + def __init__(self, driver): + super(ChatMessageInput, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//*[@content-desc='chat-message-input']") + + +class SendMessageButton(BaseButton): + + def __init__(self, driver): + super(SendMessageButton, self).__init__(driver) + self.locator = \ + self.Locator.xpath_selector("//android.widget.FrameLayout//" + "android.view.ViewGroup[3]//" + "android.view.ViewGroup[2]//android.widget.ImageView") + + +class ChatsViewObject(BaseViewObject): + + def __init__(self, driver): + super(ChatsViewObject, self).__init__(driver) + self.driver = driver + + self.profile_button = ProfileButton(self.driver) + self.profile_icon = ProfileIcon(self.driver) + self.plus_button = PlusButton(self.driver) + self.add_new_contact = AddNewContactButton(self.driver) + self.public_key_edit_box = PublicKeyEditBox(self.driver) + self.confirm_public_key_button = ConfirmPublicKeyButton(self.driver) + self.chat_message_input = ChatMessageInput(self.driver) + self.send_message_button = SendMessageButton(self.driver) diff --git a/test/appium/views/home.py b/test/appium/views/home.py new file mode 100644 index 0000000000..554948b24a --- /dev/null +++ b/test/appium/views/home.py @@ -0,0 +1,39 @@ +from views.base_view import BaseViewObject +from views.base_element import * + + +class ContinueButton(BaseButton): + + def __init__(self, driver): + super(ContinueButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//*[@text='Continue']") + + +class TypeMessageEditBox(BaseEditBox): + + def __init__(self, driver): + super(TypeMessageEditBox, self).__init__(driver) + self.locator = \ + self.Locator.xpath_selector("//android.widget.EditText[@content-desc!='chat-message-input']") + + +class RequestPasswordIcon(BaseButton): + + def __init__(self, driver): + super(RequestPasswordIcon, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//*[@content-desc='request-password']") + + +class HomeView(BaseViewObject): + + def __init__(self, driver): + super(HomeView, self).__init__(driver) + self.continue_button = ContinueButton(driver) + + try: + self.continue_button.click() + except Exception: + pass + + self.type_message_edit_box = TypeMessageEditBox(driver) + self.request_password_icon = RequestPasswordIcon(driver) diff --git a/test/appium/views/profile.py b/test/appium/views/profile.py new file mode 100644 index 0000000000..7b12840a60 --- /dev/null +++ b/test/appium/views/profile.py @@ -0,0 +1,25 @@ +from views.base_view import BaseViewObject +from views.base_element import * + + +class PublicKeyText(BaseText): + + def __init__(self, driver): + super(PublicKeyText, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//android.widget.TextView") + + def get_key(self): + texts = self.driver.find_elements(self.locator.by, self.locator.value) + for i in texts: + if i.text.startswith('0x04'): + return i.text + pytest.fail("Public key wasn't found!") + + +class ProfileViewObject(BaseViewObject): + + def __init__(self, driver): + super(ProfileViewObject, self).__init__(driver) + self.driver = driver + + self.public_key_text = PublicKeyText(self.driver)