diff --git a/.circleci/config.yml b/.circleci/config.yml index dc2df03e..a8439ed1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,6 +26,13 @@ default config for macOS: &macos_defaults macos: xcode: '12.1.0' + +config for macOS (android): &macos_defaults_android + <<: *defaults + resource_class: 'medium' + macos: + xcode: '11.5.0' + default config for android apk builds: &android_defaults <<: *defaults docker: @@ -41,67 +48,76 @@ default config for android apk builds: &android_defaults # CACHE CONFIG # ============================== -# brew -save brew cache: &cache_save_brew - name: Saving Brew cache - paths: +cache keys: + brew ios: &key_brew_ios cache-brew-ios-v3-{{ arch }} + brew android: &key_brew_android cache-brew-android-v3-{{ arch }} + yarn: &key_yarn cache-yarn-{{ checksum "package.json" }}-{{ arch }} + gradle: &key_gradle cache-gradle-{{ checksum "example/android/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "package.json" }}-{{ arch }} + pods: &key_pods cache-pods-v1-{{ checksum "example/ios/Podfile" }}-{{ checksum "package.json" }}-{{ arch }} + +cache: + # brew + save brew cache for ios: &cache_save_brew_ios + name: Saving Brew cache + paths: + - /usr/local/Homebrew + - ~/Library/Caches/Homebrew + key: *key_brew_ios + + restore brew cache for ios: &cache_restore_brew_ios + name: Restoring Brew cache + keys: + - *key_brew_ios + + save brew cache for android: &cache_save_brew_android + name: Saving Brew cache for android + paths: - /usr/local/Homebrew - ~/Library/Caches/Homebrew - key: legacy-brew-cache-node12-{{ arch }} - -restore brew cache: &cache_restore_brew - name: Restoring Brew cache - keys: - - legacy-brew-cache-node12-{{ arch }} - -save brew cache for android: &cache_save_brew_android - name: Saving Brew cache for android - paths: - - /usr/local/Homebrew - - ~/Library/Caches/Homebrew - key: legacy-brew-cache-node12-{{ arch }}-android - -restore brew cache for android: &cache_restore_brew_android - name: Restoring Brew cache for android - keys: - - legacy-brew-cache-node12-{{ arch }}-android - -# yarn -save yarn cache: &cache_save_yarn - name: Saving Yarn cache - paths: - - ~/.cache/yarn - - ~/Library/Detox - key: legacy-yarn-cache-{{ checksum "package.json" }}-{{ arch }} - -restore yarn cache: &cache_restore_yarn - name: Restoring Yarn cache - keys: - - legacy-yarn-cache-{{ checksum "package.json" }}-{{ arch }} - -# gradle -save gradle wrapper cache: &cache_save_gradle_wrapper - name: Saving Gradle Wrapper cache - paths: - - ~/.gradle/wrapper - key: gradle-wrapper-legacy-{{ checksum "example/android/gradle/wrapper/gradle-wrapper.properties" }} - -save gradle build cache: &cache_save_gradle_build - name: Saving Gradle app/build cache - paths: - - ~/.gradle/caches - key: app-build-gradle-legacy-{{ checksum "example/android/app/build.gradle" }} - -restore gradle wrapper cache: &cache_restore_gradle_wrapper - name: Restoring Gradle Wrapper cache - keys: - - gradle-wrapper-legacy-{{ checksum "example/android/gradle/wrapper/gradle-wrapper.properties" }} - -restore gradle build cache: &cache_restore_gradle_build - name: Restoring Gradle app/build cache - keys: - - app-build-gradle-legacy-{{ checksum "example/android/app/build.gradle" }} - + key: *key_brew_android + + restore brew cache for android: &cache_restore_brew_android + name: Restoring Brew cache for android + keys: + - *key_brew_android + + # yarn + save yarn cache: &cache_save_yarn + name: Saving Yarn cache + paths: + - ~/.cache/yarn + - ~/Library/Detox + key: *key_yarn + + restore yarn cache: &cache_restore_yarn + name: Restoring Yarn cache + keys: + - *key_yarn + + # gradle + save gradle cache: &cache_save_gradle + name: Saving Gradle cache + key: *key_gradle + paths: + - ~/.gradle/wrapper + - ~/.gradle/caches + + restore gradle cache: &cache_restore_gradle + name: Restoring Gradle cache + keys: + - *key_gradle + + # cocoapods + save pods cache: &cache_save_pods + name: Saving Pods + key: *key_pods + paths: + - example/ios/Pods + + restore pods cache: &cache_restore_pods + name: Restoring Pods + keys: + - *key_pods # ============================== # JOBS @@ -141,19 +157,20 @@ jobs: <<: *macos_defaults steps: - *addWorkspace - - restore-cache: *cache_restore_brew + - restore-cache: *cache_restore_brew_ios - run: name: Configure macOS Environment command: | brew bundle --file=.circleci/Brewfile.ios --no-lock touch .watchmanconfig echo Node $(node --version) - - save-cache: *cache_save_brew + - save-cache: *cache_save_brew_ios - restore-cache: *cache_restore_yarn - run: name: Installing Yarn dependencies command: yarn --pure-lockfile --non-interactive --cache-folder ~/.cache/yarn - save-cache: *cache_save_yarn + - restore-cache: *cache_restore_pods - run: name: Bundle JS command: yarn bundle:ios --dev false @@ -161,6 +178,7 @@ jobs: name: Install pod dependencies working_directory: example/ios command: pod install + - save-cache: *cache_save_pods - run: name: Build iOS app command: yarn build:e2e:ios @@ -177,18 +195,12 @@ jobs: name: Installing Yarn dependencies command: yarn --pure-lockfile --non-interactive --cache-folder ~/.cache/yarn - save-cache: *cache_save_yarn - - # Gradle - - restore-cache: *cache_restore_gradle_wrapper - - restore-cache: *cache_restore_gradle_build + - restore-cache: *cache_restore_gradle - run: name: Downloading Gradle dependencies working_directory: example/android command: ./gradlew --max-workers 2 fetchDependencies - - save-cache: *cache_save_gradle_wrapper - - save-cache: *cache_save_gradle_build - - # Build and test + - save-cache: *cache_save_gradle - run: name: Bundle JS command: yarn bundle:android --dev false @@ -203,7 +215,7 @@ jobs: - example/android/app/build/outputs/apk/* "Test: Android e2e": - <<: *macos_defaults + <<: *macos_defaults_android steps: - *addWorkspace - run: @@ -211,16 +223,15 @@ jobs: command: | echo 'export ANDROID_HOME="/usr/local/share/android-sdk"' >> $BASH_ENV echo 'export ANDROID_SDK_ROOT="/usr/local/share/android-sdk"' >> $BASH_ENV - echo 'export PATH="$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/tools:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/tools/bin:$PATH"' >> $BASH_ENV + echo 'export PATH="$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/tools:$ANDROID_SDK_ROOT/platform-tools:$PATH"' >> $BASH_ENV echo 'export QEMU_AUDIO_DRV=none' >> $BASH_ENV - echo 'export JAVA_HOME=/Library/Java/Home' >> $BASH_ENV + echo 'export JAVA_HOME=$(/usr/libexec/java_home)' >> $BASH_ENV source $BASH_ENV - - # Android tools - restore-cache: *cache_restore_brew_android - run: name: Install Android SDK tools command: | + brew update --preinstall brew bundle --file=.circleci/Brewfile.android --no-lock - save-cache: *cache_save_brew_android - run: @@ -228,7 +239,7 @@ jobs: shell: /bin/bash -e command: | yes | sdkmanager "platform-tools" "tools" 1> /dev/null - yes | sdkmanager "platforms;android-28" "system-images;android-21;google_apis;x86" 1> /dev/null + yes | sdkmanager "platforms;android-28" "system-images;android-28;default;x86_64" 1> /dev/null yes | sdkmanager "emulator" --channel=3 1> /dev/null yes | sdkmanager "build-tools;28.0.3" 1> /dev/null yes | sdkmanager --licenses 1> /dev/null @@ -243,12 +254,11 @@ jobs: - run: name: Create emulator command: | - avdmanager create avd \ - --force \ + avdmanager create avd --force \ -n "Emu_E2E" \ - -k "system-images;android-21;google_apis;x86" \ - -g "google_apis" \ - -d "Nexus 4" + -k "system-images;android-28;default;x86_64" \ + -g "default" \ + -d "pixel" - run: name: Start emulator in background background: true @@ -311,8 +321,6 @@ workflows: filters: branches: only: master -# - "Test: Android e2e": -# requires: -# - "Test: lint" -# - "Test: flow" -# - "Build: Android release apk" + - "Test: Android e2e": + requires: + - "Build: Android release apk" diff --git a/example/.gitignore b/example/.gitignore index 22f263aa..d0792870 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -40,6 +40,7 @@ yarn-error.log buck-out/ \.buckd/ *.keystore +!android/debug.keystore # fastlane # diff --git a/example/android/app/src/androidTest/java/com/microsoft/reacttestapp/DetoxTest.java b/example/android/app/src/androidTest/java/com/microsoft/reacttestapp/DetoxTest.java new file mode 100644 index 00000000..efac899a --- /dev/null +++ b/example/android/app/src/androidTest/java/com/microsoft/reacttestapp/DetoxTest.java @@ -0,0 +1,30 @@ +package com.microsoft.reacttestapp; + +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = 60; + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/example/android/build.gradle b/example/android/build.gradle index e03f6ad3..949174a1 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -24,3 +24,41 @@ task fetchDependencies() { } } } + +allprojects { + repositories { + maven { + url("$rootDir/../../node_modules/detox/Detox-android") + } + } + afterEvaluate { project -> + def androidExtension = project.extensions.findByName('android') + + if(androidExtension != null && project.name == 'app') { + // android extension from test-app pacakge + androidExtension.defaultConfig { + ndk { + abiFilters 'arm64-v8a', 'x86', 'x86_64' + } + } + + androidExtension.signingConfigs { + test { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + + androidExtension.buildTypes.release.signingConfig = androidExtension.signingConfigs.test + androidExtension.testBuildType = 'release' + + androidExtension.sourceSets.androidTest.java.srcDirs += "$rootDir/app/src/androidTest/java" + + project.dependencies { + androidTestImplementation('com.wix:detox:+') + } + } + } +} diff --git a/example/android/debug.keystore b/example/android/debug.keystore new file mode 100644 index 00000000..364e105e Binary files /dev/null and b/example/android/debug.keystore differ diff --git a/example/e2e/asyncstorage.e2e.js b/example/e2e/asyncstorage.e2e.js index c902a244..4713fca9 100644 --- a/example/e2e/asyncstorage.e2e.js +++ b/example/e2e/asyncstorage.e2e.js @@ -13,6 +13,13 @@ describe('Async Storage', () => { let test_getSetClear; let test_mergeItem; + async function dismissKeyboard() { + if (device.getPlatform() === 'android') { + return device.pressBack(); + } + return closeKeyboard.tap(); + } + beforeAll(async () => { await device.reloadReactNative(); restartButton = await element(by.id('restart_button')); @@ -104,19 +111,19 @@ describe('Async Storage', () => { await nameInput.clearText(); await nameInput.typeText(name); - await closeKeyboard.tap(); + await dismissKeyboard(); await ageInput.clearText(); await ageInput.typeText(age); - await closeKeyboard.tap(); + await dismissKeyboard(); await eyesInput.clearText(); await eyesInput.typeText(eyesColor); - await closeKeyboard.tap(); + await dismissKeyboard(); await shoeInput.clearText(); await shoeInput.typeText(shoeSize); - await closeKeyboard.tap(); + await dismissKeyboard(); return `${name} is ${age}, has ${eyesColor} eyes and shoe size of ${shoeSize}.`; } @@ -198,10 +205,20 @@ describe('Async Storage', () => { describe('merge item delegate test', () => { beforeAll(async () => { await test_mergeItem.tap(); + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + await element(by.id('setDelegate_button')).tap(); }); afterAll(async () => { + if (device.getPlatform() === 'android') { + // Not yet supported. + return; + } + await element(by.id('unsetDelegate_button')).tap(); }); @@ -230,19 +247,19 @@ describe('Async Storage', () => { await nameInput.clearText(); await nameInput.typeText(name); - await closeKeyboard.tap(); + await dismissKeyboard(); await ageInput.clearText(); await ageInput.typeText(age); - await closeKeyboard.tap(); + await dismissKeyboard(); await eyesInput.clearText(); await eyesInput.typeText(eyesColor); - await closeKeyboard.tap(); + await dismissKeyboard(); await shoeInput.clearText(); await shoeInput.typeText(shoeSize); - await closeKeyboard.tap(); + await dismissKeyboard(); return `${name} from delegate is ${age} from delegate, has ${eyesColor} eyes and shoe size of ${shoeSize}.`; } diff --git a/example/examples/MergeItem.js b/example/examples/MergeItem.js index 81ac71a1..2ce8681b 100644 --- a/example/examples/MergeItem.js +++ b/example/examples/MergeItem.js @@ -217,7 +217,10 @@ export default class Merge extends Component {