From 7e7920358fa9aab8c37f2612ed442d09569fc7f5 Mon Sep 17 00:00:00 2001 From: Jarrad Hope Date: Fri, 14 Apr 2017 18:46:56 +0200 Subject: [PATCH] java appium tests and jenkins test --- Jenkinsfile | 113 +++++++++++------- README.md | 2 +- test/appium/AppiumTests.iml | 78 ++++++++++++ test/appium/pom.xml | 56 +++++++++ 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 +++++++++ 10 files changed, 480 insertions(+), 41 deletions(-) create mode 100644 test/appium/AppiumTests.iml create mode 100644 test/appium/pom.xml create mode 100644 test/appium/src/test/java/app/StatusApp.java create mode 100644 test/appium/src/test/java/screens/AbstractScreen.java create mode 100644 test/appium/src/test/java/screens/ChatScreen.java create mode 100644 test/appium/src/test/java/tests/AbstractTest.java create mode 100644 test/appium/src/test/java/tests/InitialRunTest.java create mode 100644 test/appium/src/test/java/utility/AppiumDriverBuilder.java diff --git a/Jenkinsfile b/Jenkinsfile index 6163289968..71fc522de2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,45 +1,78 @@ node { + def apkUrl = '' + def ipaUrl = '' + def testPassed = true + sh 'source /etc/profile' - stage('Git & Dependencies') { - git([url: 'https://github.com/status-im/status-react.git', branch: env.BRANCH_NAME]) - // Checkout master because used for iOS Plist version information - sh 'git checkout -- .' - sh 'git checkout master' - sh 'git checkout ' + env.BRANCH_NAME - sh 'rm -rf node_modules' - sh 'lein deps && npm install && ./re-natal deps' - sh 'lein generate-externs' - sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' - sh 'cd ios && pod install && cd ..' - } - stage('Build') { - sh 'lein prod-build' - } - stage('Build (Android)') { - sh 'cd android && ./gradlew assembleRelease' - } - stage('Build (iOS)') { - sh 'export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' - sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist /Users/Xcloud/archive.plist' - } - stage('Deploy (Android)') { - def artifact_dir = pwd() + '/android/app/build/outputs/apk/' - def artifact = new File(artifact_dir + 'app-release.apk') - assert artifact.exists() - def server = Artifactory.server('artifacts') - def shortCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(6) - def filename = 'im.status.ethereum-' + shortCommit + '.apk' - artifact.renameTo artifact_dir + filename - def uploadSpec = '{ "files": [ { "pattern": "*apk/' + filename + '", "target": "pull-requests" }]}' - def buildInfo = server.upload(uploadSpec) - slackSend color: 'good', message: env.BRANCH_NAME + ' (Android) http://artifacts.status.im:8081/artifactory/pull-requests/' + filename - } - stage('Deploy (iOS)') { - withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { - def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() - sh 'sleep 10' - def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() - slackSend color: 'good', message: env.BRANCH_NAME + ' (iOS) https://i.diawi.com/' + hash + + try { + + stage('Git & Dependencies') { + git([url: 'https://github.com/status-im/status-react.git', branch: env.BRANCH_NAME]) + // Checkout master because used for iOS Plist version information + sh 'git checkout -- .' + sh 'git checkout master' + sh 'git checkout ' + env.BRANCH_NAME + sh 'rm -rf node_modules' + sh 'lein deps && npm install && ./re-natal deps' + sh 'lein generate-externs' + sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' + sh 'cd ios && pod install && cd ..' } + stage('Build') { + sh 'lein prod-build' + } + + // Android + stage('Build (Android)') { + sh 'cd android && ./gradlew assembleRelease' + } + stage('Deploy (Android)') { + def artifact_dir = pwd() + '/android/app/build/outputs/apk/' + def artifact = new File(artifact_dir + 'app-release.apk') + assert artifact.exists() + def server = Artifactory.server('artifacts') + def shortCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(6) + def filename = 'im.status.ethereum-' + shortCommit + '.apk' + artifact.renameTo artifact_dir + filename + def uploadSpec = '{ "files": [ { "pattern": "*apk/' + filename + '", "target": "pull-requests" }]}' + def buildInfo = server.upload(uploadSpec) + apkUrl = 'http://artifacts.status.im:8081/artifactory/pull-requests/' + filename + } + + try { + stage('Test (Android)') { + sauce('b9aded57-5cc1-4f6b-b5ea-42d989987852') { + sh 'cd test/appium && mvn -DapkUrl=' + apkUrl + ' test' + saucePublisher() + } + } + } catch(e) { + testPassed = false + } + + // iOS + stage('Build (iOS)') { + sh 'export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/StatusIm.xcworkspace -scheme StatusIm -configuration release -archivePath status clean archive' + sh 'xcodebuild -exportArchive -exportPath status -archivePath status.xcarchive -exportOptionsPlist /Users/Xcloud/archive.plist' + } + stage('Deploy (iOS)') { + withCredentials([string(credentialsId: 'diawi-token', variable: 'token')]) { + def job = sh(returnStdout: true, script: 'curl https://upload.diawi.com/ -F token='+token+' -F file=@status/StatusIm.ipa -F find_by_udid=0 -F wall_of_apps=0 | jq -r ".job"').trim() + sh 'sleep 10' + def hash = sh(returnStdout: true, script: "curl -vvv 'https://upload.diawi.com/status?token="+token+"&job="+job+"'|jq -r '.hash'").trim() + ipaUrl = 'https://i.diawi.com/' + hash + } + } + + } catch (e) { + slackSend color: 'bad', message: env.BRANCH_NAME + ' failed to build. ' + env.BUILD_URL + throw e + } + + stage('Slack Notification') { + def c = (testPassed ? 'good' : 'warning' ) + slackSend color: c, message: env.BRANCH_NAME + ' (Android, test: ' + (testPassed ? ':+1:' : ':-1:') + ') ' + apkUrl + slackSend color: c, message: env.BRANCH_NAME + ' (iOS) ' + ipaUrl } } diff --git a/README.md b/README.md index 8e9fd0d0e4..343b0df477 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![Status - A Browser, Messenger, and gateway to the decentralised world of Ethereum](https://status.im/img/status-github-banner@2x.png?v=1.1 "Status - A Browser, Messenger, and gateway to the decentralised world of Ethereum") -// TODO badges +[![Build Status](https://saucelabs.com/buildstatus/jarrad-status)](https://saucelabs.com/beta/builds/50ccf11ec1a44d88b6eb989929e5789f) # Status - a Mobile Ethereum Operating System diff --git a/test/appium/AppiumTests.iml b/test/appium/AppiumTests.iml new file mode 100644 index 0000000000..4d44f624cf --- /dev/null +++ b/test/appium/AppiumTests.iml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/appium/pom.xml b/test/appium/pom.xml new file mode 100644 index 0000000000..2680f725e2 --- /dev/null +++ b/test/appium/pom.xml @@ -0,0 +1,56 @@ + + + 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/src/test/java/app/StatusApp.java b/test/appium/src/test/java/app/StatusApp.java new file mode 100644 index 0000000000..2586ac952d --- /dev/null +++ b/test/appium/src/test/java/app/StatusApp.java @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000000..1c853b0d24 --- /dev/null +++ b/test/appium/src/test/java/screens/AbstractScreen.java @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000000..fd57729729 --- /dev/null +++ b/test/appium/src/test/java/screens/ChatScreen.java @@ -0,0 +1,75 @@ +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 new file mode 100644 index 0000000000..a708907aaf --- /dev/null +++ b/test/appium/src/test/java/tests/AbstractTest.java @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000000..ae5a0318ae --- /dev/null +++ b/test/appium/src/test/java/tests/InitialRunTest.java @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000000..ed42a704fa --- /dev/null +++ b/test/appium/src/test/java/utility/AppiumDriverBuilder.java @@ -0,0 +1,58 @@ +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(); + +} +