├── .gitignore ├── lib ├── index.js ├── reactNativeConfig.js ├── detoxInit.js ├── installPackages.js └── updateFiles.js ├── templates ├── network_security_config.xml ├── .detoxrc.json ├── DetoxTest.java ├── AndroidManifest.xml ├── build.gradle └── app │ └── build.gradle ├── index.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # node.js 6 | # 7 | node_modules/ 8 | npm-debug.log 9 | yarn-error.log -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const updateFiles = require("./updateFiles"); 2 | const installPackages = require("./installPackages"); 3 | const detoxInit = require("./detoxInit"); 4 | 5 | module.exports = { 6 | updateFiles, 7 | installPackages, 8 | detoxInit, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/reactNativeConfig.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | 3 | module.exports = async function reactNativeConfig() { 4 | const { stdout } = await execa("npx", ["react-native", "config"], { 5 | cwd: process.cwd(), 6 | }); 7 | 8 | return JSON.parse(stdout); 9 | }; 10 | -------------------------------------------------------------------------------- /templates/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | localhost 6 | 7 | -------------------------------------------------------------------------------- /lib/detoxInit.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const chalk = require("chalk"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | module.exports = function detoxInit() { 7 | console.log(chalk.yellow("Initializing Detox")); 8 | 9 | if (!fs.existsSync(path.join(process.cwd(), "e2e"))) { 10 | try { 11 | execa.sync("detox", ["init", "-r", "jest"], { 12 | cwd: process.cwd(), 13 | stdio: "inherit", 14 | }); 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/installPackages.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const chalk = require("chalk"); 3 | 4 | const DONE = chalk.reset.inverse.bold.green(" DONE "); 5 | const cwd = process.cwd(); 6 | 7 | module.exports = function installPackages() { 8 | console.log(chalk.yellow("Installing required packages!")); 9 | // global installs 10 | execa.sync("npm", ["install", "detox-cli", "-g"], { 11 | cwd, 12 | stdio: "inherit", 13 | }); 14 | 15 | // dev dependecies 16 | execa.sync( 17 | "npm", 18 | ["install", "jest@^26.6.1", "jest-circus@^26.6.1", "detox", "--save-dev"], 19 | { 20 | cwd, 21 | stdio: "inherit", 22 | } 23 | ); 24 | 25 | console.log(DONE); 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | const clear = require("clear"); 5 | const chalk = require("chalk"); 6 | const figlet = require("figlet"); 7 | const { updateFiles, installPackages, detoxInit } = require("./lib"); 8 | const reactNativeConfig = require("./lib/reactNativeConfig"); 9 | 10 | clear(); 11 | 12 | console.log( 13 | chalk.green(figlet.textSync("Setting up Detox", { horizontalLayout: "full" })) 14 | ); 15 | 16 | async function init() { 17 | const config = await reactNativeConfig(); 18 | installPackages(); 19 | detoxInit(config.project.android); 20 | await updateFiles(config.project.android); 21 | } 22 | 23 | init() 24 | .then(() => console.log(chalk.green("Installation complete"))) 25 | .catch(console.error); 26 | -------------------------------------------------------------------------------- /templates/.detoxrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "testRunner": "jest", 3 | "runnerConfig": "e2e/config.json", 4 | "configurations": { 5 | "android.emu.debug": { 6 | "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk", 7 | "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..", 8 | "type": "android.emulator", 9 | "device": { 10 | "avdName": "change_me" 11 | } 12 | }, 13 | "android.emu.release": { 14 | "binaryPath": "android/app/build/outputs/apk/release/app-release-unsigned.apk", 15 | "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..", 16 | "type": "android.emulator", 17 | "device": { 18 | "avdName": "change_me" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This package was created to simplify setting up Detox testing framework for React Native mobile apps. It is recommended that 4 | you run this package right after you initialize your react native app. 5 | 6 | ## Step 1: Setup 7 | 8 | ```sh 9 | npm install -g react-native-setup-detox 10 | ``` 11 | 12 | Go to your React Native app folder and run: 13 | 14 | ```sh 15 | react-native-setup-detox 16 | ``` 17 | 18 | ## Step 2: Configure emulator 19 | 20 | After successful installation you have to setup an emulator to run the tests. Please follow these steps: 21 | 22 | * [Android](https://github.com/wix/Detox/blob/4fadc000b63f9039c0432b07d709518f95ff9f23/docs/Introduction.AndroidDevEnv.md) 23 | 24 | 25 | ## Step 3: Use emulator 26 | 27 | Once you setup an emulator from step 2 you have to update the emulator name in your `.detoxrc.json` `avdName` property. 28 | 29 | ## TODO: 30 | 31 | - [x] Android 32 | - [ ] iOS (in progress) 33 | 34 | Currently on tested on Mac and Android. 35 | 36 | ## Feedback 37 | 38 | Please log bugs or features in the issues section -------------------------------------------------------------------------------- /templates/DetoxTest.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import com.wix.detox.Detox; 4 | import com.wix.detox.config.DetoxConfig; 5 | 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import androidx.test.ext.junit.runners.AndroidJUnit4; 11 | import androidx.test.filters.LargeTest; 12 | import androidx.test.rule.ActivityTestRule; 13 | 14 | @RunWith(AndroidJUnit4.class) 15 | @LargeTest 16 | public class DetoxTest { 17 | @Rule 18 | // Replace 'MainActivity' with the value of android:name entry in 19 | // in AndroidManifest.xml 20 | public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); 21 | 22 | @Test 23 | public void runDetoxTests() { 24 | DetoxConfig detoxConfig = new DetoxConfig(); 25 | detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; 26 | detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; 27 | detoxConfig.rnContextLoadTimeoutSec = (com.example.BuildConfig.DEBUG ? 180 : 60); 28 | 29 | Detox.runTests(mActivityRule, detoxConfig); 30 | } 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-setup-detox", 3 | "version": "0.0.3", 4 | "description": "setup react native detox automation testing framework", 5 | "main": "./index.js", 6 | "bin": { 7 | "react-native-setup-detox": "./index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"No test specified\"" 11 | }, 12 | "homepage": "https://github.com/tsvetann/react-native-setup-detox#readme", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/tsvetann/react-native-setup-detox" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "native", 20 | "detox", 21 | "automation", 22 | "testing" 23 | ], 24 | "files": [ 25 | "lib", 26 | "templates", 27 | "index.js" 28 | ], 29 | "author": "Tsvetan Nikolov", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/tsvetann/react-native-setup-detox/issues" 33 | }, 34 | "dependencies": { 35 | "chalk": "^4.1.0", 36 | "clear": "^0.1.0", 37 | "execa": "^4.0.3", 38 | "figlet": "^1.5.0", 39 | "fs-copy-file-sync": "^1.1.1", 40 | "merge-files": "^0.1.2", 41 | "replace-in-file": "^6.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /templates/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /templates/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "29.0.2" 6 | minSdkVersion = 18 7 | compileSdkVersion = 29 8 | targetSdkVersion = 29 9 | kotlinVersion = '1.4.10' 10 | } 11 | repositories { 12 | google() 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath("com.android.tools.build:gradle:3.5.3") 17 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | mavenLocal() 26 | maven { 27 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 28 | url("$rootDir/../node_modules/react-native/android") 29 | } 30 | maven { 31 | // Android JSC is installed from npm 32 | url("$rootDir/../node_modules/jsc-android/dist") 33 | } 34 | 35 | google() 36 | jcenter() 37 | maven { url 'https://www.jitpack.io' } 38 | maven { 39 | // All of Detox' artifacts are provided via the npm module 40 | url "$rootDir/../node_modules/detox/Detox-android" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/updateFiles.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const mergeFiles = require("merge-files"); 3 | const fs = require("fs"); 4 | const copyFileSync = require("fs-copy-file-sync"); 5 | const chalk = require("chalk"); 6 | const replace = require("replace-in-file"); 7 | 8 | const DONE = chalk.reset.inverse.bold.green(" DONE "); 9 | const templatesPath = path.join(__dirname, "../templates"); 10 | 11 | /** 12 | * Update various build files with detox configuration 13 | * 14 | * @param {Object} config react native config from `react-native config` 15 | */ 16 | module.exports = async function updateFiles(config) { 17 | console.log(chalk.yellow("Updating files!")); 18 | 19 | const PATHS = { 20 | "build.gradle": config.buildGradlePath, 21 | "AndroidManifest.xml": config.manifestPath, 22 | "app/build.gradle": path.join(config.sourceDir, "app/build.gradle"), 23 | ".detoxrc.json": path.join(config.folder, ".detoxrc.json"), 24 | }; 25 | 26 | const testFileDestination = path.join( 27 | config.sourceDir, 28 | `app/src/androidTest/java/${config.packageFolder}` 29 | ); 30 | 31 | const detoxTestJavaPath = path.join(testFileDestination, "DetoxTest.java"); 32 | 33 | // update all files 34 | const promises = Object.keys(PATHS).map((p) => { 35 | let targetPath = PATHS[p]; 36 | let sourcePath = path.join(templatesPath, p); 37 | return mergeFiles([sourcePath, targetPath], targetPath); 38 | }); 39 | await Promise.all(promises); 40 | 41 | // create directory for security configuration 42 | let dest = path.join(config.sourceDir, "app/src/main/res/xml"); 43 | if (!fs.existsSync(dest)) { 44 | fs.mkdirSync(dest); 45 | copyFileSync( 46 | path.join(templatesPath, "network_security_config.xml"), 47 | path.join(dest, "network_security_config.xml") 48 | ); 49 | } 50 | 51 | // copy DetoxTest.java 52 | if (!fs.existsSync(testFileDestination)) { 53 | fs.mkdirSync(testFileDestination, { recursive: true }); 54 | copyFileSync(path.join(templatesPath, "DetoxTest.java"), detoxTestJavaPath); 55 | } 56 | 57 | // update AndroidManifest.xml, DetoxTest.java, app/build.gradle and settings.gradle app name 58 | replace.sync({ 59 | files: [ 60 | config.manifestPath, 61 | config.settingsGradlePath, 62 | detoxTestJavaPath, 63 | PATHS["app/build.gradle"], 64 | ], 65 | from: /com.example/g, 66 | to: config.packageName, 67 | }); 68 | 69 | // gradle.settings 70 | fs.appendFileSync(config.settingsGradlePath, "include ':detox'\n"); 71 | fs.appendFileSync( 72 | config.settingsGradlePath, 73 | "project(':detox').projectDir = new File(rootProject.projectDir, '../node_modules/detox/android/detox')" 74 | ); 75 | 76 | console.log(DONE); 77 | }; 78 | -------------------------------------------------------------------------------- /templates/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation. If none specified and 19 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is 20 | * // default. Can be overridden with ENTRY_FILE environment variable. 21 | * entryFile: "index.android.js", 22 | * 23 | * // https://reactnative.dev/docs/performance#enable-the-ram-format 24 | * bundleCommand: "ram-bundle", 25 | * 26 | * // whether to bundle JS and assets in debug mode 27 | * bundleInDebug: false, 28 | * 29 | * // whether to bundle JS and assets in release mode 30 | * bundleInRelease: true, 31 | * 32 | * // whether to bundle JS and assets in another build variant (if configured). 33 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 34 | * // The configuration property can be in the following formats 35 | * // 'bundleIn${productFlavor}${buildType}' 36 | * // 'bundleIn${buildType}' 37 | * // bundleInFreeDebug: true, 38 | * // bundleInPaidRelease: true, 39 | * // bundleInBeta: true, 40 | * 41 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 42 | * // for example: to disable dev mode in the staging build type (if configured) 43 | * devDisabledInStaging: true, 44 | * // The configuration property can be in the following formats 45 | * // 'devDisabledIn${productFlavor}${buildType}' 46 | * // 'devDisabledIn${buildType}' 47 | * 48 | * // the root of your project, i.e. where "package.json" lives 49 | * root: "../../", 50 | * 51 | * // where to put the JS bundle asset in debug mode 52 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 53 | * 54 | * // where to put the JS bundle asset in release mode 55 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 56 | * 57 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 58 | * // require('./image.png')), in debug mode 59 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 60 | * 61 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 62 | * // require('./image.png')), in release mode 63 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 64 | * 65 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 66 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 67 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 68 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 69 | * // for example, you might want to remove it from here. 70 | * inputExcludes: ["android/**", "ios/**"], 71 | * 72 | * // override which node gets called and with what additional arguments 73 | * nodeExecutableAndArgs: ["node"], 74 | * 75 | * // supply additional arguments to the packager 76 | * extraPackagerArgs: [] 77 | * ] 78 | */ 79 | 80 | project.ext.react = [ 81 | enableHermes: false, // clean and rebuild if changing 82 | ] 83 | 84 | apply from: "../../node_modules/react-native/react.gradle" 85 | 86 | /** 87 | * Set this to true to create two separate APKs instead of one: 88 | * - An APK that only works on ARM devices 89 | * - An APK that only works on x86 devices 90 | * The advantage is the size of the APK is reduced by about 4MB. 91 | * Upload all the APKs to the Play Store and people will download 92 | * the correct one based on the CPU architecture of their device. 93 | */ 94 | def enableSeparateBuildPerCPUArchitecture = false 95 | 96 | /** 97 | * Run Proguard to shrink the Java bytecode in release builds. 98 | */ 99 | def enableProguardInReleaseBuilds = false 100 | 101 | /** 102 | * The preferred build flavor of JavaScriptCore. 103 | * 104 | * For example, to use the international variant, you can use: 105 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 106 | * 107 | * The international variant includes ICU i18n library and necessary data 108 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 109 | * give correct results when using with locales other than en-US. Note that 110 | * this variant is about 6MiB larger per architecture than default. 111 | */ 112 | def jscFlavor = 'org.webkit:android-jsc:+' 113 | 114 | /** 115 | * Whether to enable the Hermes VM. 116 | * 117 | * This should be set on project.ext.react and mirrored here. If it is not set 118 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode 119 | * and the benefits of using Hermes will therefore be sharply reduced. 120 | */ 121 | def enableHermes = project.ext.react.get("enableHermes", false); 122 | 123 | android { 124 | compileSdkVersion rootProject.ext.compileSdkVersion 125 | 126 | compileOptions { 127 | sourceCompatibility JavaVersion.VERSION_1_8 128 | targetCompatibility JavaVersion.VERSION_1_8 129 | } 130 | 131 | defaultConfig { 132 | applicationId "com.example" 133 | minSdkVersion rootProject.ext.minSdkVersion 134 | targetSdkVersion rootProject.ext.targetSdkVersion 135 | versionCode 1 136 | versionName "1.0" 137 | testBuildType System.getProperty('testBuildType', 'debug') 138 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 139 | } 140 | splits { 141 | abi { 142 | reset() 143 | enable enableSeparateBuildPerCPUArchitecture 144 | universalApk false // If true, also generate a universal APK 145 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 146 | } 147 | } 148 | signingConfigs { 149 | debug { 150 | storeFile file('debug.keystore') 151 | storePassword 'android' 152 | keyAlias 'androiddebugkey' 153 | keyPassword 'android' 154 | } 155 | } 156 | buildTypes { 157 | debug { 158 | signingConfig signingConfigs.debug 159 | } 160 | release { 161 | // Caution! In production, you need to generate your own keystore file. 162 | // see https://reactnative.dev/docs/signed-apk-android. 163 | signingConfig signingConfigs.debug 164 | minifyEnabled enableProguardInReleaseBuilds 165 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 166 | // Detox-specific additions to pro-guard 167 | proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" 168 | } 169 | } 170 | 171 | // applicationVariants are e.g. debug, release 172 | applicationVariants.all { variant -> 173 | variant.outputs.each { output -> 174 | // For each separate APK per architecture, set a unique version code as described here: 175 | // https://developer.android.com/studio/build/configure-apk-splits.html 176 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 177 | def abi = output.getFilter(OutputFile.ABI) 178 | if (abi != null) { // null for the universal-debug, universal-release variants 179 | output.versionCodeOverride = 180 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 181 | } 182 | 183 | } 184 | } 185 | } 186 | 187 | dependencies { 188 | implementation fileTree(dir: "libs", include: ["*.jar"]) 189 | //noinspection GradleDynamicVersion 190 | implementation "com.facebook.react:react-native:+" // From node_modules 191 | 192 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" 193 | 194 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { 195 | exclude group:'com.facebook.fbjni' 196 | } 197 | 198 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 199 | exclude group:'com.facebook.flipper' 200 | exclude group:'com.squareup.okhttp3', module:'okhttp' 201 | } 202 | 203 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { 204 | exclude group:'com.facebook.flipper' 205 | } 206 | 207 | androidTestImplementation(project(path: ":detox")) 208 | 209 | if (enableHermes) { 210 | def hermesPath = "../../node_modules/hermes-engine/android/"; 211 | debugImplementation files(hermesPath + "hermes-debug.aar") 212 | releaseImplementation files(hermesPath + "hermes-release.aar") 213 | } else { 214 | implementation jscFlavor 215 | } 216 | } 217 | 218 | // Run this once to be able to run the application with BUCK 219 | // puts all compile dependencies into folder libs for BUCK to use 220 | task copyDownloadableDepsToLibs(type: Copy) { 221 | from configurations.compile 222 | into 'libs' 223 | } 224 | 225 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 226 | --------------------------------------------------------------------------------