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
+
+
+
+ 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();
+
+}
+