E2e android

Summary:- converted shell script `scripts/e2e-test.sh` into JS script to have more programming flexibility
- using appium execute 2 tests after a fresh React Native app installation: check HMR and that debugging mode does not crash the app
- made sure tests can be stable on limited CI systems and added ways to debug any problems in the future

Using appium we can now interact with Android app and test its state.
As a follow up i am planning to write a blog post on how to use appium with android and ios for e2e testing.
Closes https://github.com/facebook/react-native/pull/6840

Differential Revision: D3173635

Pulled By: mkonicek

fb-gh-sync-id: 3cf044bc9f64d1a842ae4589dd1bcab76de3d66a
fbshipit-source-id: 3cf044bc9f64d1a842ae4589dd1bcab76de3d66a
This commit is contained in:
Konstantin Raev 2016-04-13 08:18:49 -07:00 committed by Facebook Github Bot 9
parent c254d081fd
commit f9bd789206
7 changed files with 317 additions and 118 deletions

View File

@ -16,16 +16,15 @@ install:
script:
- if [[ "$TEST_TYPE" = objc ]]; then travis_retry ./scripts/objc-test.sh; fi
- if [[ "$TEST_TYPE" = e2e-objc ]]; then travis_retry ./scripts/e2e-test.sh --ios; fi
- if [[ "$TEST_TYPE" = e2e-objc ]]; then travis_retry node ./scripts/run-ci-e2e-tests.js --ios --js; fi
- if [[ "$TEST_TYPE" = js ]]; then npm run flow check; fi
- if [[ "$TEST_TYPE" = js ]]; then npm test -- --maxWorkers=1; fi
- if [[ "$TEST_TYPE" = js ]]; then travis_retry ./scripts/e2e-test.sh --packager; fi
env:
matrix:
- TEST_TYPE=e2e-objc
- TEST_TYPE=js
- TEST_TYPE=objc
- TEST_TYPE=js
branches:
only:

View File

@ -1,4 +1,4 @@
VERSION_NAME=0.0.1-master
VERSION_NAME=1000.0.0-master
GROUP=com.facebook.react
POM_NAME=ReactNative

View File

@ -71,13 +71,12 @@ test:
# run installed apk with tests
- source scripts/circle-ci-android-setup.sh && retry3 ./scripts/run-android-instrumentation-tests.sh com.facebook.react.tests
# Deprecated: run tests with Gradle, we keep them for a while to compare performance
- ./gradlew :ReactAndroid:testDebugUnitTest
- ./gradlew :ReactAndroid:installDebugAndroidTest
- source scripts/circle-ci-android-setup.sh && retry3 ./scripts/run-android-instrumentation-tests.sh com.facebook.react.tests.gradle
# Deprecated: these tests are executed using Buck above, while we support Gradle we just make sure the test code compiles
- ./gradlew :ReactAndroid:assembleDebugAndroidTest
# JS and Android e2e test
- source scripts/circle-ci-android-setup.sh && retry3 ./scripts/e2e-test.sh --packager --android
# Android e2e test
- source scripts/circle-ci-android-setup.sh && retry3 node ./scripts/run-ci-e2e-tests.js --android --js
# testing docs generation is not broken
- cd website && node ./server/generate.js

View File

@ -5,7 +5,7 @@ import re
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `cp ~/.android/debug.keystore keystores/debug.keystore`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#

126
scripts/android-e2e-test.js Normal file
View File

@ -0,0 +1,126 @@
'use strict';
/**
* Used in run-ci-e2e-test.js and executed in Travis and Circle CI.
* E2e test that verifies that init app can be installed, compiled, started and Hot Module reloading and Chrome debugging work.
* For other examples of appium refer to: https://github.com/appium/sample-code/tree/master/sample-code/examples/node and
* https://www.npmjs.com/package/wd-android
*
*
* To set up:
* - npm install --save-dev appium@1.5.1 mocha@2.4.5 wd@0.3.11 colors@1.0.3 pretty-data2@0.40.1
* - cp <this file> <to app installation path>
* - keytool -genkey -v -keystore android/keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US
*
* To run this test:
* - npm start
* - node node_modules/.bin/appium
* - (cd android && ./gradlew :app:copyDownloadableDepsToLibs)
* - buck build android/app
* - node ../node_modules/.bin/_mocha ../android-e2e-test.js
*/
const wd = require('wd');
const path = require('path');
const fs = require('fs');
const pd = require('pretty-data2').pd;
require('colors');
// value in ms to print out screen contents, set this value in CI to debug if tests are failing
const appiumDebugInterval = process.env.APPIUM_DEBUG_INTERVAL;
describe('Android Test App', function () {
this.timeout(600000);
let driver;
let debugIntervalId;
before(function () {
driver = wd.promiseChainRemote({
host: 'localhost',
port: 4723
});
driver.on('status', function (info) {
console.log(info.cyan);
});
driver.on('command', function (method, path, data) {
if (path === 'source()' && data) {
console.log(' > ' + method.yellow, 'Screen contents'.grey, '\n', pd.xml(data).yellow);
} else {
console.log(' > ' + method.yellow, path.grey, data || '');
}
});
driver.on('http', function (method, path, data) {
console.log(' > ' + method.magenta, path, (data || '').grey);
});
// every interval print what is on the screen
if (appiumDebugInterval) {
debugIntervalId = setInterval(() => {
// it driver.on('command') will log the screen contents
driver.source();
}, appiumDebugInterval);
}
const desired = {
platformName: 'Android',
deviceName: 'Android Emulator',
app: path.resolve('buck-out/gen/android/app/app.apk')
};
// React Native in dev mode often starts with Red Box "Can't fibd variable __fbBatchedBridge..."
// This is fixed by clicking Reload JS which will trigger a request to packager server
return driver
.init(desired)
.setImplicitWaitTimeout(5000)
.waitForElementByXPath('//android.widget.Button[@text="Reload JS"]').
then((elem) => {
elem.click();
}, (err) => {
// ignoring if Reload JS button can't be located
})
.setImplicitWaitTimeout(150000);
});
after(function () {
if (debugIntervalId) {
clearInterval(debugIntervalId);
}
return driver.quit();
});
it('should have Hot Module Reloading working', function () {
const androidAppCode = fs.readFileSync('index.android.js', 'utf-8');
let intervalToUpdate;
return driver
.waitForElementByXPath('//android.widget.TextView[starts-with(@text, "Welcome to React Native!")]')
// http://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_MENU
.pressDeviceKey(82)
.elementByXPath('//android.widget.TextView[starts-with(@text, "Enable Hot Reloading")]')
.click()
.waitForElementByXPath('//android.widget.TextView[starts-with(@text, "Welcome to React Native!")]')
.then(() => {
let iteration = 0;
// CI environment can be quite slow and we can't guarantee that it can consistently motice a file change
// so we change the file every few seconds just in case
intervalToUpdate = setInterval(() => {
fs.writeFileSync('index.android.js', androidAppCode.replace('Welcome to React Native!', 'Welcome to React Native with HMR!' + iteration), 'utf-8');
}, 3000);
})
.waitForElementByXPath('//android.widget.TextView[starts-with(@text, "Welcome to React Native with HMR!")]')
.finally(() => {
clearInterval(intervalToUpdate);
fs.writeFileSync('index.android.js', androidAppCode, 'utf-8');
});
});
it('should have Debug In Chrome working', function () {
const androidAppCode = fs.readFileSync('index.android.js', 'utf-8');
// http://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_MENU
return driver
.waitForElementByXPath('//android.widget.TextView[starts-with(@text, "Welcome to React Native!")]')
.pressDeviceKey(82)
.elementByXPath('//android.widget.TextView[starts-with(@text, "Debug")]')
.click()
.waitForElementByXPath('//android.widget.TextView[starts-with(@text, "Welcome to React Native!")]');
});
});

View File

@ -1,108 +0,0 @@
#!/bin/bash
# The script has one required argument:
# --packager: react-native init, make sure the packager starts
# --ios: react-native init, start the packager and run the iOS app
# --android: same but run the Android app
# Abort the mission if any command fails
set -e
set -x
if [ -z $1 ]; then
echo "Please run the script with --ios, --android or --packager" >&2
exit 1
fi
SCRIPTS=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT=$(dirname $SCRIPTS)
TEMP=$(mktemp -d /tmp/react-native-XXXXXXXX)
# When tests run on CI server, we won't be able to see logs
# from packager because it runs in a separate window. This is
# a simple workaround, see packager/packager.sh
export REACT_PACKAGER_LOG="$TEMP/server.log"
# To make sure we actually installed the local version
# of react-native, we will create a temp file inside the template
# and check that it exists after `react-native init`
MARKER_IOS=$(mktemp $ROOT/local-cli/generator-ios/templates/app/XXXXXXXX)
MARKER_ANDROID=$(mktemp $ROOT/local-cli/generator-android/templates/src/XXXXXXXX)
function cleanup {
EXIT_CODE=$?
set +e
if [ $EXIT_CODE -ne 0 ];
then
WATCHMAN_LOGS=/usr/local/Cellar/watchman/3.1/var/run/watchman/$USER.log
[ -f $WATCHMAN_LOGS ] && cat $WATCHMAN_LOGS
[ -f $REACT_PACKAGER_LOG ] && cat $REACT_PACKAGER_LOG
fi
rm $MARKER_IOS
rm $MARKER_ANDROID
[ $SERVER_PID ] && kill -9 $SERVER_PID
exit $EXIT_CODE
}
trap cleanup EXIT
# pack react-native into a .tgz file
npm pack
PACKAGE=$(pwd)/react-native-*.tgz
# get react-native-cli dependencies
cd react-native-cli
npm pack
CLI_PACKAGE=$(pwd)/react-native-cli-*.tgz
cd $TEMP
npm install -g $CLI_PACKAGE
react-native init EndToEndTest --version $PACKAGE
cd EndToEndTest
# Iterates through all arguments and runs e2e tests for all of them one by one
# e.g. ./scripts/e2e-test.sh --packager --ios --android will run all e2e tests but will install the test app once
while test $# -gt 0
do
case $1 in
"--packager"*)
echo "Running a basic packager test"
# Check the packager produces a bundle (doesn't throw an error)
react-native bundle --platform android --dev true --entry-file index.android.js --bundle-output android-bundle.js
# Check that flow passes
$ROOT/node_modules/.bin/flow check
;;
"--ios"*)
echo "Running an iOS app"
cd ios
# Make sure we installed local version of react-native
ls EndToEndTest/`basename $MARKER_IOS` > /dev/null
../node_modules/react-native/packager/packager.sh --nonPersistent &
SERVER_PID=$!
# Start the app on the simulator
xctool -scheme EndToEndTest -sdk iphonesimulator test
;;
"--android"*)
echo "Running an Android app"
cd android
# Make sure we installed local version of react-native
ls `basename $MARKER_ANDROID` > /dev/null
../node_modules/react-native/packager/packager.sh --nonPersistent &
SERVER_PID=$!
cp ~/.android/debug.keystore keystores/debug.keystore
./gradlew :app:copyDownloadableDepsToLibs
buck install -r android/app
# TODO t10114777 check it renders "Welcome to React Native"
;;
*)
echo "Please run the script with --ios, --android or --packager" >&2
exit 1
;;
esac
shift
done
exit 0

183
scripts/run-ci-e2e-tests.js Normal file
View File

@ -0,0 +1,183 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/**
* This script tests that React Native end to end installation/bootstrap works for different platforms
* Available arguments:
* --ios - to test only ios application end to end
* --android - to test only android application end to end
* --js - to test that JS in the application is compilable
* --skip-cli-install - to skip react-native-cli global installation (for local debugging)
*/
/*eslint-disable no-undef */
require('shelljs/global');
var spawn = require('child_process').spawn;
const path = require('path');
const SCRIPTS = __dirname;
const ROOT = path.normalize(path.join(__dirname, '..'));
const TEMP=exec('mktemp -d /tmp/react-native-XXXXXXXX').stdout.trim();
// To make sure we actually installed the local version
// of react-native, we will create a temp file inside the template
// and check that it exists after `react-native init
const MARKER_IOS = exec(`mktemp ${ROOT}/local-cli/generator-ios/templates/app/XXXXXXXX`).stdout.trim();
const MARKER_ANDROID = exec(`mktemp ${ROOT}/local-cli/generator-android/templates/src/XXXXXXXX`).stdout.trim();
let SERVER_PID;
let APPIUM_PID;
const args = process.argv.slice(2);
function cleanup(errorCode) {
if (errorCode !== 0) {
cat(`${TEMP}/server.log`);
cat(`/usr/local/Cellar/watchman/3.1/var/run/watchman/${process.env.USER}.log`);
}
rm(MARKER_IOS);
rm(MARKER_ANDROID);
if(SERVER_PID) {
echo(`Killing packager ${SERVER_PID}`);
exec(`kill -9 ${SERVER_PID}`);
// this is quite drastic but packager starts a daemon that we can't kill by killing the parent process
// it will be fixed in April (quote David Aurelio), so until then we will kill the zombie by the port number
exec("lsof -i tcp:8081 | awk 'NR!=1 {print $2}' | xargs kill");
}
if(APPIUM_PID) {
echo(`Killing appium ${APPIUM_PID}`);
exec(`kill -9 ${APPIUM_PID}`);
}
return errorCode;
}
// install CLI
cd('react-native-cli');
exec('npm pack');
const CLI_PACKAGE = path.join(ROOT, 'react-native-cli', 'react-native-cli-*.tgz');
cd('..');
// can skip cli install for non sudo mode
if(args.indexOf('--skip-cli-install') === -1) {
if(exec(`npm install -g ${CLI_PACKAGE}`).code) {
echo('Could not install react-native-cli globally, please run in su mode');
echo('Or with --skip-cli-install to skip this step');
exit(cleanup(1));
}
}
if (args.indexOf('--android') !== -1) {
if (exec('./gradlew :ReactAndroid:installArchives -Pjobs=1 -Dorg.gradle.jvmargs="-Xmx512m -XX:+HeapDumpOnOutOfMemoryError"').code) {
echo('Failed to compile Android binaries');
exit(cleanup(1));
}
}
if (exec('npm pack').code) {
echo('Failed to pack react-native');
exit(cleanup(1));
}
// test begins
const PACKAGE = path.join(ROOT, 'react-native-*.tgz');
cd(TEMP);
if (exec(`react-native init EndToEndTest --version ${PACKAGE}`).code) {
echo('Failed to execute react-native init');
echo('Most common reason is npm registry connectivity, try again');
exit(cleanup(1));
}
cd('EndToEndTest');
if (args.indexOf('--android') !== -1) {
echo('Running an Android e2e test');
echo('Installing e2e framework');
if(exec('npm install --save-dev appium@1.5.1 mocha@2.4.5 wd@0.3.11 colors@1.0.3 pretty-data2@0.40.1', {silent: true}).code) {
echo('Failed to install appium');
exit(cleanup(1));
}
cp(`${SCRIPTS}/android-e2e-test.js`, 'android-e2e-test.js');
cd('android');
echo('Downloading Maven deps');
exec('./gradlew :app:copyDownloadableDepsToLibs');
// Make sure we installed local version of react-native
if (!test('-e', path.basename(MARKER_ANDROID))) {
echo('Android marker was not found, react native init command failed?');
exit(cleanup(1));
}
cd('..');
exec('keytool -genkey -v -keystore android/keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"');
echo(`Starting packager server, ${SERVER_PID}`);
const appiumProcess = spawn('node', ['./node_modules/.bin/appium']);
APPIUM_PID = appiumProcess.pid;
echo(`Starting appium server, ${APPIUM_PID}`);
echo('Building app');
if (exec('buck build android/app').code) {
echo('could not execute Buck build, is it installed and in PATH?');
exit(cleanup(1));
}
let packagerEnv = Object.create(process.env);
packagerEnv.REACT_NATIVE_MAX_WORKERS = 1;
// shelljs exec('', {async: true}) does not emit stdout events, so we rely on good old spawn
const packagerProcess = spawn('npm', ['start'], {
// stdio: 'inherit',
env: packagerEnv
});
SERVER_PID = packagerProcess.pid;
// wait a bit to allow packager to startup
exec('sleep 5s');
echo('Executing android e2e test');
if(exec('node node_modules/.bin/_mocha android-e2e-test.js').code) {
exit(cleanup(1));
}
}
if (args.indexOf('--ios') !== -1) {
echo('Running an iOS app');
cd('ios');
// Make sure we installed local version of react-native
if (!test('-e', path.join('EndToEndTest', path.basename(MARKER_IOS)))) {
echo('iOS marker was not found, `react-native init` command failed?');
exit(cleanup(1));
}
// shelljs exec('', {async: true}) does not emit stdout events, so we rely on good old spawn
let packagerEnv = Object.create(process.env);
packagerEnv.REACT_NATIVE_MAX_WORKERS = 1;
const packagerProcess = spawn('npm', ['start'],
{
stdio: 'inherit',
env: packagerEnv
});
SERVER_PID = packagerProcess.pid;
echo(`Starting packager server, ${SERVER_PID}`);
exec('sleep 5s');
// prepare cache to reduce chances of possible red screen "Can't fibd variable __fbBatchedBridge..."
exec('response=$(curl --write-out %{http_code} --silent --output /dev/null localhost:8081/index.ios.bundle?platform=ios)');
echo('Executing ios e2e test');
if (exec('xctool -scheme EndToEndTest -sdk iphonesimulator test').code) {
exit(cleanup(1));
}
}
if (args.indexOf('--js') !== -1) {
// Check the packager produces a bundle (doesn't throw an error)
if (exec('react-native bundle --platform android --dev true --entry-file index.android.js --bundle-output android-bundle.js').code) {
echo('Could not build package');
exit(cleanup(1));
}
if (exec(`${ROOT}/node_modules/.bin/flow check`).code) {
echo('Flow check does not pass');
exit(cleanup(1));
}
}
exit(cleanup(0));
/*eslint-enable no-undef */