From f9bd789206a549528659d24afa7ead6e83bb6f3a Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 13 Apr 2016 08:18:49 -0700 Subject: [PATCH] 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 --- .travis.yml | 5 +- ReactAndroid/gradle.properties | 2 +- circle.yml | 9 +- .../generator-android/templates/src/app/BUCK | 2 +- scripts/android-e2e-test.js | 126 ++++++++++++ scripts/e2e-test.sh | 108 ----------- scripts/run-ci-e2e-tests.js | 183 ++++++++++++++++++ 7 files changed, 317 insertions(+), 118 deletions(-) create mode 100644 scripts/android-e2e-test.js delete mode 100755 scripts/e2e-test.sh create mode 100644 scripts/run-ci-e2e-tests.js diff --git a/.travis.yml b/.travis.yml index 5f19c6b84..0e78ecb0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/ReactAndroid/gradle.properties b/ReactAndroid/gradle.properties index 7feedca64..2d5dd3a7f 100644 --- a/ReactAndroid/gradle.properties +++ b/ReactAndroid/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=0.0.1-master +VERSION_NAME=1000.0.0-master GROUP=com.facebook.react POM_NAME=ReactNative diff --git a/circle.yml b/circle.yml index 5bc086b2b..8c4a57449 100644 --- a/circle.yml +++ b/circle.yml @@ -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 diff --git a/local-cli/generator-android/templates/src/app/BUCK b/local-cli/generator-android/templates/src/app/BUCK index 38d334d86..918be8863 100644 --- a/local-cli/generator-android/templates/src/app/BUCK +++ b/local-cli/generator-android/templates/src/app/BUCK @@ -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 # diff --git a/scripts/android-e2e-test.js b/scripts/android-e2e-test.js new file mode 100644 index 000000000..a0401049f --- /dev/null +++ b/scripts/android-e2e-test.js @@ -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 + * - 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!")]'); + }); +}); diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh deleted file mode 100755 index 52eff36f6..000000000 --- a/scripts/e2e-test.sh +++ /dev/null @@ -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 diff --git a/scripts/run-ci-e2e-tests.js b/scripts/run-ci-e2e-tests.js new file mode 100644 index 000000000..3ffe396dd --- /dev/null +++ b/scripts/run-ci-e2e-tests.js @@ -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 */