From face0f564a0172a64c1782beeb97112057722f3c Mon Sep 17 00:00:00 2001 From: Andrew Brogdon Date: Thu, 12 Mar 2020 20:12:24 -0700 Subject: [PATCH] Adds espresso testing for add_to_app/android_fullscreen (#323) --- .gitignore | 3 + .travis.yml | 129 ++++++++++-------- .../android_fullscreen/app/build.gradle | 10 +- .../ExampleInstrumentedTest.kt | 71 ++++++++-- .../app/src/debug/AndroidManifest.xml | 9 ++ .../app/src/main/AndroidManifest.xml | 8 +- .../androidfullscreen/MyApplication.kt | 4 +- add_to_app/android_fullscreen/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- .../android_using_plugin/app/build.gradle | 1 - .../app/build.gradle | 9 +- .../MyApplication.kt | 4 +- add_to_app/flutter_module/pubspec.lock | 83 ++++++++++- add_to_app/flutter_module/pubspec.yaml | 5 +- .../flutter_module/test/widget_test.dart | 12 +- .../flutter_module/test_driver/example.dart | 13 ++ svc-keyfile.json.enc | Bin 0 -> 2384 bytes tool/travis_android_script.sh | 24 ++++ 18 files changed, 294 insertions(+), 97 deletions(-) create mode 100644 add_to_app/android_fullscreen/app/src/debug/AndroidManifest.xml create mode 100644 add_to_app/flutter_module/test_driver/example.dart create mode 100644 svc-keyfile.json.enc diff --git a/.gitignore b/.gitignore index 345b5e305..3a68b78a2 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ # Web related **/web/**/lib/generated_plugin_registrant.dart +# Service account files +svc-keyfile.json + # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 diff --git a/.travis.yml b/.travis.yml index 22558495e..06b88f11c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,74 +3,95 @@ git: jobs: include: - - name: "iOS tests, stable channel" - os: osx - osx_image: xcode11.2 - language: objective-c - script: ./tool/travis_ios_script.sh - env: FLUTTER_VERSION=stable + - name: iOS tests, stable channel + os: osx + osx_image: xcode11.2 + language: objective-c + script: "./tool/travis_ios_script.sh" + env: FLUTTER_VERSION=stable - - name: "Android tests, stable channel" - os: linux - dist: trusty - language: android - android: - components: - - build-tools-28.0.3 - - android-28 - script: ./tool/travis_android_script.sh - env: FLUTTER_VERSION=stable + - name: Android tests, stable channel + os: linux + dist: trusty + language: android + android: + components: + - build-tools-28.0.3 + - android-28 + script: "./tool/travis_android_script.sh" + env: FLUTTER_VERSION=stable - - name: "Flutter tests, stable channel" - os: linux - dist: trusty - language: ruby - script: ./tool/travis_flutter_script.sh - env: FLUTTER_VERSION=stable + - name: Flutter tests, stable channel + os: linux + dist: trusty + language: ruby + script: "./tool/travis_flutter_script.sh" + env: FLUTTER_VERSION=stable - - name: "iOS tests, beta channel" - os: osx - osx_image: xcode11.2 - language: objective-c - script: ./tool/travis_ios_script.sh - env: FLUTTER_VERSION=beta + - name: iOS tests, beta channel + os: osx + osx_image: xcode11.2 + language: objective-c + script: "./tool/travis_ios_script.sh" + env: FLUTTER_VERSION=beta - - name: "Android tests, beta channel" - os: linux - dist: trusty - language: android - android: - components: - - build-tools-28.0.3 - - android-28 - script: ./tool/travis_android_script.sh - env: FLUTTER_VERSION=beta + - name: Android tests, beta channel + os: linux + dist: trusty + language: android + android: + components: + - build-tools-28.0.3 + - android-28 + script: "./tool/travis_android_script.sh" + env: FLUTTER_VERSION=beta - - name: "Flutter tests, beta channel" - os: linux - dist: trusty - language: ruby - script: ./tool/travis_flutter_script.sh - env: FLUTTER_VERSION=beta + - name: Flutter tests, beta channel + os: linux + dist: trusty + language: ruby + script: "./tool/travis_flutter_script.sh" + env: FLUTTER_VERSION=beta allow_failures: - - env: FLUTTER_VERSION=beta + - env: FLUTTER_VERSION=beta + +env: + global: + # Disabling this makes it easier to install our own copy of gcloud + - CLOUDSDK_CORE_DISABLE_PROMPTS=1 before_script: - - git clone https://github.com/flutter/flutter.git -b $FLUTTER_VERSION - - ./flutter/bin/flutter doctor - - chmod +x tool/travis_*_script.sh +- git clone https://github.com/flutter/flutter.git -b $FLUTTER_VERSION +- "./flutter/bin/flutter doctor" +- chmod +x tool/travis_*_script.sh cache: cocoapods: true directories: - - $HOME/shared/.pub-cache + - "$HOME/shared/.pub-cache" + - "$HOME/google-cloud-sdk" notifications: - email: - brogdon+github@gmail.com + email: brogdon+github@gmail.com -# Building master alone means that we don't run two builds for -# each pull request. branches: - only: [master] + only: + - master + +before_install: +# Decrypt credentials for Firebase Test Lab service account. This key will only +# decrypt properly when run from within the flutter/samples repo (not a PR from +# a fork, for example), so failure is expected in some cases. The Android test +# script is the only one that uses this value, and will account for a lack of +# credentials. +- openssl aes-256-cbc -K $encrypted_ccb0f43ba178_key -iv $encrypted_ccb0f43ba178_iv + -in svc-keyfile.json.enc -out svc-keyfile.json -d || rm svc-keyfile.json || true +- ls -la +# Install our own copy of gcloud. This is necessary because the default copy is +# so old it doesn't offer a firebase component. +- if [ ! -d "$HOME/google-cloud-sdk/bin" ]; then rm -rf "$HOME/google-cloud-sdk"; + curl https://sdk.cloud.google.com | bash > /dev/null; fi +- source $HOME/google-cloud-sdk/path.bash.inc +- gcloud components update +- gcloud version diff --git a/add_to_app/android_fullscreen/app/build.gradle b/add_to_app/android_fullscreen/app/build.gradle index 736117de5..cb26dce65 100644 --- a/add_to_app/android_fullscreen/app/build.gradle +++ b/add_to_app/android_fullscreen/app/build.gradle @@ -13,6 +13,7 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true } buildTypes { release { @@ -26,13 +27,16 @@ android { } dependencies { + implementation 'androidx.multidex:multidex:2.0.1' implementation project(':flutter') - implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation project(':espresso') + androidTestImplementation "com.google.truth:truth:0.42" + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + api 'androidx.test:core:1.2.0' } diff --git a/add_to_app/android_fullscreen/app/src/androidTest/java/dev/flutter/example/androidfullscreen/ExampleInstrumentedTest.kt b/add_to_app/android_fullscreen/app/src/androidTest/java/dev/flutter/example/androidfullscreen/ExampleInstrumentedTest.kt index 49be549e8..74383f889 100644 --- a/add_to_app/android_fullscreen/app/src/androidTest/java/dev/flutter/example/androidfullscreen/ExampleInstrumentedTest.kt +++ b/add_to_app/android_fullscreen/app/src/androidTest/java/dev/flutter/example/androidfullscreen/ExampleInstrumentedTest.kt @@ -4,25 +4,68 @@ package dev.flutter.example.androidfullscreen -import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget +import androidx.test.espresso.flutter.action.FlutterActions.click +import androidx.test.espresso.flutter.assertion.FlutterAssertions.matches +import androidx.test.espresso.flutter.matcher.FlutterMatchers +import androidx.test.espresso.flutter.matcher.FlutterMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 - +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ @RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { +class MainActivityTest { + @Before + fun setUp() { + ActivityScenario.launch(MainActivity::class.java) + } + + @Test + fun flutterTextUpdatesOnClick() { + // Launch Flutter module. + onView(withId(R.id.launch_button)).perform(androidx.test.espresso.action.ViewActions.click()) + + // Verify state is inited correctly. + onFlutterWidget(withText("Taps: 0")) + .check(matches(FlutterMatchers.isExisting())) + + // Verify the increment button works. + onFlutterWidget(withText("Tap me!")).perform(click()) + onFlutterWidget(withText("Taps: 1")) + .check(matches(FlutterMatchers.isExisting())) + } + @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.flutter.example.androidfullscreen", appContext.packageName) + fun nativeTextViewUpdatesOnClick() { + // Verify Android TextView is inited correctly. + onView(withId(R.id.counter_label)).check( + androidx.test.espresso.assertion.ViewAssertions.matches( + // TODO(redbrogdon): This should be a check for 0 rather than 1. Because our state + // management is hardcoded into the Application object, though, and that object is + // reused across tests, this test begins with a counter already incremented by the + // previous one. This situation can be corrected via DI or a number of other + // approaches. + androidx.test.espresso.matcher.ViewMatchers.withText("Current count: 1") + ) + ) + + // Launch Flutter module. + onView(withId(R.id.launch_button)).perform(androidx.test.espresso.action.ViewActions.click()) + + // Increment count. + onFlutterWidget(withText("Tap me!")).perform(click()) + + // Exit Flutter module and verify that the Android TextView is updated correctly. + onFlutterWidget(withText("Exit this screen")).perform(click()) + onView(withId(R.id.counter_label)).check( + androidx.test.espresso.assertion.ViewAssertions.matches( + // TODO(redbrogdon): s/2/1 + androidx.test.espresso.matcher.ViewMatchers.withText("Current count: 2") + ) + ) } } diff --git a/add_to_app/android_fullscreen/app/src/debug/AndroidManifest.xml b/add_to_app/android_fullscreen/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..7121739cc --- /dev/null +++ b/add_to_app/android_fullscreen/app/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/add_to_app/android_fullscreen/app/src/main/AndroidManifest.xml b/add_to_app/android_fullscreen/app/src/main/AndroidManifest.xml index bd2b7ff9e..1be187567 100644 --- a/add_to_app/android_fullscreen/app/src/main/AndroidManifest.xml +++ b/add_to_app/android_fullscreen/app/src/main/AndroidManifest.xml @@ -13,17 +13,15 @@ - + android:hardwareAccelerated="true" + android:windowSoftInputMode="adjustResize" /> - \ No newline at end of file + diff --git a/add_to_app/android_fullscreen/app/src/main/java/dev/flutter/example/androidfullscreen/MyApplication.kt b/add_to_app/android_fullscreen/app/src/main/java/dev/flutter/example/androidfullscreen/MyApplication.kt index bf65b3f68..cc79fb209 100644 --- a/add_to_app/android_fullscreen/app/src/main/java/dev/flutter/example/androidfullscreen/MyApplication.kt +++ b/add_to_app/android_fullscreen/app/src/main/java/dev/flutter/example/androidfullscreen/MyApplication.kt @@ -4,7 +4,7 @@ package dev.flutter.example.androidfullscreen -import android.app.Application +import androidx.multidex.MultiDexApplication import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor @@ -12,7 +12,7 @@ import io.flutter.plugin.common.MethodChannel const val ENGINE_ID = "1" -class MyApplication : Application() { +class MyApplication : MultiDexApplication() { var count = 0 private lateinit var channel: MethodChannel diff --git a/add_to_app/android_fullscreen/build.gradle b/add_to_app/android_fullscreen/build.gradle index e796b11e6..d35e43dc9 100644 --- a/add_to_app/android_fullscreen/build.gradle +++ b/add_to_app/android_fullscreen/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.android.tools.build:gradle:3.6.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/add_to_app/android_fullscreen/gradle/wrapper/gradle-wrapper.properties b/add_to_app/android_fullscreen/gradle/wrapper/gradle-wrapper.properties index ec11f69a2..9833519b2 100644 --- a/add_to_app/android_fullscreen/gradle/wrapper/gradle-wrapper.properties +++ b/add_to_app/android_fullscreen/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Sep 17 09:33:13 PDT 2019 +#Wed Mar 11 09:25:37 PDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/add_to_app/android_using_plugin/app/build.gradle b/add_to_app/android_using_plugin/app/build.gradle index bd1535e87..98e2b93b7 100644 --- a/add_to_app/android_using_plugin/app/build.gradle +++ b/add_to_app/android_using_plugin/app/build.gradle @@ -27,7 +27,6 @@ android { dependencies { implementation project(':flutter') - implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' diff --git a/add_to_app/android_using_prebuilt_module/app/build.gradle b/add_to_app/android_using_prebuilt_module/app/build.gradle index 4ce85081d..775822943 100644 --- a/add_to_app/android_using_prebuilt_module/app/build.gradle +++ b/add_to_app/android_using_prebuilt_module/app/build.gradle @@ -13,6 +13,7 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true } buildTypes { release { @@ -46,12 +47,14 @@ dependencies { transitive = true } - implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.multidex:multidex:2.0.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation "com.google.truth:truth:0.42" + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + api 'androidx.test:core:1.2.0' } diff --git a/add_to_app/android_using_prebuilt_module/app/src/main/java/dev/flutter/example/androidusingprebuiltmodule/MyApplication.kt b/add_to_app/android_using_prebuilt_module/app/src/main/java/dev/flutter/example/androidusingprebuiltmodule/MyApplication.kt index 2bca6f536..de97dc6bd 100644 --- a/add_to_app/android_using_prebuilt_module/app/src/main/java/dev/flutter/example/androidusingprebuiltmodule/MyApplication.kt +++ b/add_to_app/android_using_prebuilt_module/app/src/main/java/dev/flutter/example/androidusingprebuiltmodule/MyApplication.kt @@ -4,7 +4,7 @@ package dev.flutter.example.androidusingprebuiltmodule -import android.app.Application +import androidx.multidex.MultiDexApplication import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor @@ -12,7 +12,7 @@ import io.flutter.plugin.common.MethodChannel const val ENGINE_ID = "1" -class MyApplication : Application() { +class MyApplication : MultiDexApplication() { var count = 0 private lateinit var channel: MethodChannel diff --git a/add_to_app/flutter_module/pubspec.lock b/add_to_app/flutter_module/pubspec.lock index 839a266d1..5be070c3c 100644 --- a/add_to_app/flutter_module/pubspec.lock +++ b/add_to_app/flutter_module/pubspec.lock @@ -57,16 +57,40 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + espresso: + dependency: "direct dev" + description: + name: espresso + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" image: dependency: transitive description: @@ -74,6 +98,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.4" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" matcher: dependency: transitive description: @@ -88,6 +126,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.8" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" path: dependency: transitive description: @@ -109,13 +154,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.12" provider: dependency: "direct main" description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.0.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" quiver: dependency: transitive description: @@ -184,6 +250,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -193,3 +273,4 @@ packages: version: "3.5.0" sdks: dart: ">=2.6.0-dev <3.0.0" + flutter: ">=1.12.1 <2.0.0" diff --git a/add_to_app/flutter_module/pubspec.yaml b/add_to_app/flutter_module/pubspec.yaml index 3d2738496..b3b327b3a 100644 --- a/add_to_app/flutter_module/pubspec.yaml +++ b/add_to_app/flutter_module/pubspec.yaml @@ -9,11 +9,14 @@ environment: dependencies: flutter: sdk: flutter - provider: ^3.1.0 + provider: ^4.0.2 dev_dependencies: flutter_test: sdk: flutter + flutter_driver: + sdk: flutter + espresso: ^0.0.1+2 flutter: uses-material-design: true diff --git a/add_to_app/flutter_module/test/widget_test.dart b/add_to_app/flutter_module/test/widget_test.dart index 5aae2e4b2..43ba66ba4 100644 --- a/add_to_app/flutter_module/test/widget_test.dart +++ b/add_to_app/flutter_module/test/widget_test.dart @@ -1,14 +1,10 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - import 'package:flutter_module/main.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; class MockCounterModel extends ChangeNotifier implements CounterModel { diff --git a/add_to_app/flutter_module/test_driver/example.dart b/add_to_app/flutter_module/test_driver/example.dart new file mode 100644 index 000000000..5f1e45315 --- /dev/null +++ b/add_to_app/flutter_module/test_driver/example.dart @@ -0,0 +1,13 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_module/main.dart' as app; + +// This alternate entrypoint is used for espresso testing. See +// https://pub.dev/packages/espresso for details. +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/svc-keyfile.json.enc b/svc-keyfile.json.enc new file mode 100644 index 0000000000000000000000000000000000000000..dd1c114d7b95907b685d62cacc51df785cd19928 GIT binary patch literal 2384 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