java appium tests and jenkins test

This commit is contained in:
Jarrad Hope 2017-04-14 18:46:56 +02:00
parent 58f8eecffa
commit 7e7920358f
10 changed files with 480 additions and 41 deletions

113
Jenkinsfile vendored
View File

@ -1,45 +1,78 @@
node { node {
def apkUrl = ''
def ipaUrl = ''
def testPassed = true
sh 'source /etc/profile' sh 'source /etc/profile'
stage('Git & Dependencies') {
git([url: 'https://github.com/status-im/status-react.git', branch: env.BRANCH_NAME]) try {
// Checkout master because used for iOS Plist version information
sh 'git checkout -- .' stage('Git & Dependencies') {
sh 'git checkout master' git([url: 'https://github.com/status-im/status-react.git', branch: env.BRANCH_NAME])
sh 'git checkout ' + env.BRANCH_NAME // Checkout master because used for iOS Plist version information
sh 'rm -rf node_modules' sh 'git checkout -- .'
sh 'lein deps && npm install && ./re-natal deps' sh 'git checkout master'
sh 'lein generate-externs' sh 'git checkout ' + env.BRANCH_NAME
sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack' sh 'rm -rf node_modules'
sh 'cd ios && pod install && cd ..' sh 'lein deps && npm install && ./re-natal deps'
} sh 'lein generate-externs'
stage('Build') { sh 'mvn -f modules/react-native-status/ios/RCTStatus dependency:unpack'
sh 'lein prod-build' sh 'cd ios && pod install && cd ..'
}
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
} }
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
} }
} }

View File

@ -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") ![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 # Status - a Mobile Ethereum Operating System

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: io.appium:java-client:5.0.0-BETA7" level="project" />
<orderEntry type="library" name="Maven: com.google.code.gson:gson:2.8.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpclient:4.5.2" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpcore:4.4.4" level="project" />
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
<orderEntry type="library" name="Maven: commons-codec:commons-codec:1.9" level="project" />
<orderEntry type="library" name="Maven: cglib:cglib:3.2.4" level="project" />
<orderEntry type="library" name="Maven: org.ow2.asm:asm:5.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.ant:ant:1.9.6" level="project" />
<orderEntry type="library" name="Maven: org.apache.ant:ant-launcher:1.9.6" level="project" />
<orderEntry type="library" name="Maven: commons-validator:commons-validator:1.5.1" level="project" />
<orderEntry type="library" name="Maven: commons-beanutils:commons-beanutils:1.9.2" level="project" />
<orderEntry type="library" name="Maven: commons-digester:commons-digester:1.8.1" level="project" />
<orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.2" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.5" level="project" />
<orderEntry type="library" name="Maven: commons-io:commons-io:2.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:4.3.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-aop:4.3.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-beans:4.3.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:4.3.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-expression:4.3.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.aspectj:aspectjweaver:1.8.10" level="project" />
<orderEntry type="library" name="Maven: org.openpnp:opencv:3.2.0-1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-java:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-chrome-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-remote-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-api:3.3.1" level="project" />
<orderEntry type="library" name="Maven: cglib:cglib-nodep:3.2.4" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-exec:1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:21.0" level="project" />
<orderEntry type="library" name="Maven: net.java.dev.jna:jna-platform:4.1.0" level="project" />
<orderEntry type="library" name="Maven: net.java.dev.jna:jna:4.1.0" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-edge-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-firefox-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-ie-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-opera-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-safari-driver:3.3.1" level="project" />
<orderEntry type="library" name="Maven: com.codeborne:phantomjsdriver:1.4.0" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:htmlunit-driver:2.24" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-support:3.3.1" level="project" />
<orderEntry type="library" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit:2.24" level="project" />
<orderEntry type="library" name="Maven: xalan:xalan:2.7.2" level="project" />
<orderEntry type="library" name="Maven: xalan:serializer:2.7.2" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpmime:4.5.2" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.htmlunit:htmlunit-core-js:2.23" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.htmlunit:neko-htmlunit:2.24" level="project" />
<orderEntry type="library" name="Maven: xerces:xercesImpl:2.11.0" level="project" />
<orderEntry type="library" name="Maven: xml-apis:xml-apis:1.4.01" level="project" />
<orderEntry type="library" name="Maven: net.sourceforge.cssparser:cssparser:0.9.21" level="project" />
<orderEntry type="library" name="Maven: org.w3c.css:sac:1.3" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.2.20.v20161216" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-util:9.2.20.v20161216" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-io:9.2.20.v20161216" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.2.20.v20161216" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.2.20.v20161216" level="project" />
<orderEntry type="library" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: com.saucelabs:sauce_junit:2.1.23" level="project" />
<orderEntry type="library" name="Maven: com.saucelabs:sauce_java_common:2.1.23" level="project" />
<orderEntry type="library" name="Maven: com.saucelabs:saucerest:1.0.32" level="project" />
<orderEntry type="library" name="Maven: com.googlecode.json-simple:json-simple:1.1.1" level="project" />
<orderEntry type="library" name="Maven: org.json:json:20090211" level="project" />
</component>
</module>

56
test/appium/pom.xml Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>im.status</groupId>
<artifactId>AppiumTests</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>5.0.0-BETA7</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>com.saucelabs</groupId>
<artifactId>sauce_junit</artifactId>
<version>2.1.23</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20</version>
<configuration>
<parallel>methods</parallel>
<threadCount>4</threadCount>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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); }
}

View File

@ -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<WebElement> driver;
protected WebDriverWait wait;
public AbstractScreen(AppiumDriver<WebElement> 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);
}
}

View File

@ -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<WebElement> 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;
}
}
}

View File

@ -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<WebElement> 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;
}
}

View File

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

View File

@ -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<SELF, DRIVER extends AppiumDriver> {
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<AndroidDriverBuilder, AndroidDriver> {
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<WebElement>(endpoint, capabilities);
}
}
public SELF withEndpoint(URL endpoint) {
this.endpoint = endpoint;
return (SELF) this;
}
public abstract DRIVER build();
}