diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index a9abe496a..ca3a3f376 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -27,3 +27,5 @@ kotlin.incremental=false # If you want to treat warnings as errors locally, set this property to true # in your ~/.gradle/gradle.properties file. warningsAsErrors=false + +android.experimental.androidTest.enableEmulatorControl=true diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index a4a49b8ee..7d2e24f22 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -221,6 +221,7 @@ jobs: disable-animations: true disk-size: 6000M heap-size: 600M + emulator-options: -show-kernel -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim script: ./gradlew connectedDemoDebugAndroidTest --daemon - name: Run local tests (including Roborazzi) for the combined coverage report (only API 30) @@ -261,3 +262,143 @@ jobs: compression-level: 1 overwrite: false path: '**/build/reports/jacoco/' + + + androidTestScreenshots: + runs-on: ubuntu-latest + timeout-minutes: 55 + strategy: + matrix: + include: + - api-level: 27 + profile: pixel_5 + target: default +# - api-level: 33 +# profile: pixel_fold +# target: google_apis + - api-level: 35 + profile: pixel_fold + target: google_apis + + steps: + - name: Delete unnecessary tools 🔧 + uses: jlumbroso/free-disk-space@v1.3.1 + with: + android: false # Don't remove Android tools + tool-cache: true # Remove image tool cache - rm -rf "$AGENT_TOOLSDIRECTORY" + dotnet: true # rm -rf /usr/share/dotnet + haskell: true # rm -rf /opt/ghc... + swap-storage: true # rm -f /mnt/swapfile (4GiB) + docker-images: false # Takes 16s, enable if needed in the future + large-packages: false # includes google-cloud-sdk and it's slow + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + + # TODO this workflow needs cmdline-tools 13.0 (for pixel_fold). Remove when not necessary. + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + # android-emulator-runner uses /latest + - name: Remove old cmdlinetools + run: | + rm -rf ${{ env.ANDROID_HOME}}/cmdline-tools/latest +# mv ${{ env.ANDROID_HOME}}/cmdline-tools/11479570 ${{ env.ANDROID_HOME}}/cmdline-tools/latest +# echo ${{ env.ANDROID_HOME}}/cmdline-tools/latest/bin >> $GITHUB_PATH + + # https://github.com/ReactiveCircus/android-emulator-runner/issues/197 + - name: Create directory for AVD + run: mkdir -p /home/runner/.android/avd + + - name: Collect Workflow Telemetry + uses: catchpoint/workflow-telemetry-action@v2 + with: + comment_on_pr: false + + - name: Build projects and run instrumented screenshot tests + id: dropshotsverify + continue-on-error: true + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + disable-animations: true + disk-size: 6000M + emulator-build: 12895296 # 35.4.5 - https://developer.android.com/studio/emulator_archive + heap-size: 600M + profile: ${{ matrix.profile }} + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: + adb logcat > logcat.log & ./gradlew :app:connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests --daemon + + - name: Upload logcat + if: always() + uses: actions/upload-artifact@v4 + with: + name: logcat-${{ matrix.profile }}-${{ matrix.api-level }} + path: logcat.log + + - name: Prevent pushing new screenshots if this is a fork + id: checkfork_screenshots_instrumented + continue-on-error: false + if: steps.dropshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository + run: | + echo "::error::Instrumented screenshot tests failed, please create a PR in your fork first." && exit 1 + + - name: Record new screenshots if verification failed + id: dropshotsrecord + continue-on-error: false + if: steps.dropshotsverify.outcome == 'failure' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + disable-animations: true + disk-size: 6000M + emulator-build: 12895296 # 35.4.5 - https://developer.android.com/studio/emulator_archive + heap-size: 600M + profile: ${{ matrix.profile }} + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + script: + ./gradlew :app:connectedDemoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.google.samples.apps.nowinandroid.ui.InstrumentedScreenshotTests -Pdropshots.record --daemon --stacktrace + + + - name: Checkout new changes (in case another job already uploaded screenshots) + uses: actions/checkout@v4 +# if: steps.dropshotsverify.outputs.newgoldens == 'true' + with: + clean: false + + - name: Push new device screenshots if available + uses: stefanzweifel/git-auto-commit-action@4b8a201e31cadd9829df349894b28c54e6c19fe6 +# if: steps.dropshotsverify.outputs.newgoldens == 'true' + with: + file_pattern: 'app/src/androidTest/screenshots/*.png' + disable_globbing: true + commit_message: "🤖 Updates instrumented screenshots. API ${{ matrix.api-level }}" + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports-${{ matrix.profile }}-${{ matrix.api-level }} + path: '**/build/reports/androidTests' + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 682fbc1b3..3c4817855 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,11 +62,6 @@ android { excludes.add("/META-INF/{AL2.0,LGPL2.1}") } } - testOptions { - unitTests { - isIncludeAndroidResources = true - } - } namespace = "com.google.samples.apps.nowinandroid" } @@ -128,9 +123,13 @@ dependencies { androidTestImplementation(projects.core.dataTest) androidTestImplementation(projects.core.datastoreTest) androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.navigation.testing) androidTestImplementation(libs.androidx.compose.ui.test) androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.kotlin.test) + androidTestImplementation(libs.androidx.test.espresso.device) + androidTestImplementation(libs.androidx.test.uiautomator) baselineProfile(projects.benchmarks) } diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt new file mode 100644 index 000000000..ee21fc322 --- /dev/null +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/EdgeToEdgeTest.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.ui + +import android.graphics.Bitmap +import android.util.Log +import androidx.test.core.app.takeScreenshot +import androidx.test.espresso.device.DeviceInteraction.Companion.setClosedMode +import androidx.test.espresso.device.DeviceInteraction.Companion.setFlatMode +import androidx.test.espresso.device.DeviceInteraction.Companion.setScreenOrientation +import androidx.test.espresso.device.EspressoDevice.Companion.onDevice +import androidx.test.espresso.device.action.ScreenOrientation.LANDSCAPE +import androidx.test.espresso.device.action.ScreenOrientation.PORTRAIT +import androidx.test.espresso.device.common.executeShellCommand +import androidx.test.espresso.device.controller.DeviceMode.CLOSED +import androidx.test.espresso.device.controller.DeviceMode.FLAT +import androidx.test.espresso.device.filter.RequiresDeviceMode +import androidx.test.espresso.device.filter.RequiresDisplay +import androidx.test.espresso.device.sizeclass.HeightSizeClass.Companion.HeightSizeClassEnum +import androidx.test.espresso.device.sizeclass.WidthSizeClass.Companion.WidthSizeClassEnum +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.filters.SdkSuppress +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.dropbox.dropshots.Dropshots +import com.google.samples.apps.nowinandroid.MainActivity +import com.google.samples.apps.nowinandroid.core.rules.GrantPostNotificationsPermissionRule +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +/** + * These tests must be run on the following devices: + * - A phone on API 27 (pixel_5) + * - A foldable on API 33 (pixel_fold) + * - A foldable on API 35 (pixel_fold) + */ +@HiltAndroidTest +@InstrumentedScreenshotTests +class EdgeToEdgeTest { + /** + * Manages the components' state and is used to perform injection on your test + */ + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + /** + * Create a temporary folder used to create a Data Store file. This guarantees that + * the file is removed in between each test, preventing a crash. + */ + @BindValue + @get:Rule(order = 1) + val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + /** + * Grant [android.Manifest.permission.POST_NOTIFICATIONS] permission to prevent the + * permissions dialog from showing on top. + */ + @get:Rule(order = 2) + val postNotificationsPermission = GrantPostNotificationsPermissionRule() + + @get:Rule(order = 3) + val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java) + + @get:Rule(order = 4) + val dropshots = Dropshots() + + @Before + fun setup() = hiltRule.inject() + + @Before + fun enableDemoMode() { + executeShellCommand( + "settings put global development_settings_enabled 1", + ) + executeShellCommand("settings put global sysui_demo_allowed 1") + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "enter", + ) + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "notifications -e visible false", + ) + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "clock -e hhmm 1234", + ) + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "network -e wifi hide", + ) + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "network -e mobile hide", + ) + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command " + + "network -e satellite hide", + ) + } + + @After + fun disableDemoMode() { + exitDemoMode() + executeShellCommand( + "settings put global sysui_demo_allowed 0", + ) + executeShellCommand( + "settings put global development_settings_enabled 0", + ) + } + + private fun exitDemoMode() { + executeShellCommand( + "am broadcast -a com.android.systemui.demo -e command exit", + ) + } + + companion object { + @JvmStatic + @AfterClass + fun resetDemoMode() { + executeShellCommand("am broadcast -a com.android.systemui.demo -e command exit") + } + } + + @RequiresDisplay(WidthSizeClassEnum.COMPACT, HeightSizeClassEnum.MEDIUM) + @SdkSuppress(minSdkVersion = 27, maxSdkVersion = 27) + @Test + fun edgeToEdge_Phone_Api27() { + screenshotSystemBar("edgeToEdge_Phone_systemBar_Api27") + screenshotNavigationBar("edgeToEdge_Phone_navBar_Api27") + } + + @RequiresDeviceMode(mode = FLAT) + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 33, maxSdkVersion = 33) + @Test + fun edgeToEdge_Foldable_api33() { + runFoldableTests(apiName = "api33") + } + + @RequiresDeviceMode(mode = FLAT) + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream") + @Test + fun edgeToEdge_Foldable_api35() { + runFoldableTests(apiName = "api35") + } + + @RequiresDeviceMode(mode = FLAT) + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream") + @Test + fun edgeToEdge_Foldable_api35_tallCutout() { + onDevice().setClosedMode() + forceTallCutout() + exitDemoMode() + enableDemoMode() // Mode change resets demo mode! + screenshotSystemBar("edgeToEdge_Foldable_closed_system_tallCutout_api35") + + onDevice().setFlatMode() + exitDemoMode() + forceTallCutout() + enableDemoMode() // Mode change resets demo mode! + screenshotSystemBar("edgeToEdge_Foldable_flat_system_tallCutout_api35") + + onDevice().setClosedMode() + resetCutout() + } + + // Very flaky:DeviceControllerOperationException: Device could not be set to the + // requested screen orientation. + + @RequiresDeviceMode(mode = CLOSED) + @SdkSuppress(minSdkVersion = 35, codeName = "VanillaIceCream") + @Test + fun edgeToEdge_Foldable_api35_landscape() { + onDevice().setClosedMode() + onDevice().setScreenOrientation(LANDSCAPE) + forceThreeButtonNavigation() + exitDemoMode() + enableDemoMode() // This fixes the satellite icon showing up uninvited. + screenshotSideNavigationBar("edgeToEdge_Foldable_landscape_sideNav3button_35") + onDevice().setScreenOrientation(PORTRAIT) + } + + private fun runFoldableTests(apiName: String) { + resetCutout() + onDevice().setClosedMode() + screenshotSystemBar("edgeToEdge_Foldable_closed_system_$apiName") + forceThreeButtonNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_closed_nav3button_$apiName") + forceGestureNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_closed_navGesture_$apiName") + + onDevice().setFlatMode() + exitDemoMode() + enableDemoMode() // Flat mode resets demo mode! + screenshotSystemBar("edgeToEdge_Foldable_flat_system_$apiName") + forceThreeButtonNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_flat_nav3button_$apiName") + forceGestureNavigation() + screenshotNavigationBar("edgeToEdge_Foldable_flat_navGesture_$apiName") + onDevice().setClosedMode() + } + + private fun screenshotSystemBar(screenshotFileName: String) { + waitForWindowUpdate() + + // Crop the top, adding extra pixels to check continuity + val bitmap = takeScreenshot().let { + val newHeight = 200 + Bitmap.createBitmap(it, 0, 0, it.width, newHeight) + } + assertSnapshot(bitmap, screenshotFileName) + } + + private fun screenshotNavigationBar(screenshotFileName: String) { + waitForWindowUpdate() + + // Crop the bottom, adding extra pixels to check continuity + val bitmap = takeScreenshot().let { + val newHeight = 200 + Bitmap.createBitmap(it, 0, it.height - newHeight, it.width, newHeight) + } + assertSnapshot(bitmap, screenshotFileName) + } + + private fun screenshotSideNavigationBar(screenshotFileName: String) { + waitForWindowUpdate() + + // Crop the top, adding extra pixels to check continuity + val bitmap = takeScreenshot().let { + Bitmap.createBitmap(it, it.width - 250, 0, 250, it.height) + } + assertSnapshot(bitmap, screenshotFileName) + } + + private fun forceThreeButtonNavigation() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand( + "cmd overlay enable-exclusive " + + "com.android.internal.systemui.navbar.threebutton", + ) + } + waitForWindowUpdate() + } + + private fun forceGestureNavigation() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand( + "cmd overlay enable-exclusive " + + "com.android.internal.systemui.navbar.gestural", + ) + } + waitForWindowUpdate() + } + + private fun forceTallCutout() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand( + "cmd overlay enable-exclusive " + + "--category com.android.internal.display.cutout.emulation.tall ", + ) + } + waitForWindowUpdate() + } + private fun resetCutout() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + executeShellCommand( + "cmd overlay enable-exclusive " + + "--category com.android.internal.display.cutout.emulation.noCutout ", + ) + } + waitForWindowUpdate() + } + + private fun waitForWindowUpdate() { + // TODO: This works but it's unclear if it's making it wait too long. Investigate. + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + .waitForWindowUpdate( + InstrumentationRegistry.getInstrumentation().targetContext.packageName, + 8000, + ) + } + + private fun assertSnapshot( + bitmap: Bitmap, + name: String, + filePath: String? = null, + ) { + // Try to assert 3 times + var count = 2 + while (true) { + try { + dropshots.assertSnapshot(bitmap, name, filePath) + } catch (e: AssertionError) { + if (count == 0) throw e + count -= 1 + Log.i("EdgeToEdgeTest", "Test failed, retrying (count=$count)") + waitForWindowUpdate() + continue + } + break + } + } + + private fun executeShellCommand(command: String) { + runOnUiThread { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).executeShellCommand(command) + } + // ADB commands are not synchronized. This sleep was found empirically. + Thread.sleep(20) + } +} diff --git a/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt new file mode 100644 index 000000000..c279130cb --- /dev/null +++ b/app/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/ui/InstrumentedScreenshotTests.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.ui + +/** + * TODO: Move to a test module. + */ +annotation class InstrumentedScreenshotTests diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png new file mode 100644 index 000000000..1a61f84f5 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_nav3button_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png new file mode 100644 index 000000000..1a61f84f5 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_navGesture_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png new file mode 100644 index 000000000..82c3106ac Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_closed_system_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png new file mode 100644 index 000000000..603a15716 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_nav3button_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png new file mode 100644 index 000000000..720dbc456 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_navGesture_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png new file mode 100644 index 000000000..57e80e433 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Foldable_flat_system_api33.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png b/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png new file mode 100644 index 000000000..ec52601eb Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Phone_navBar_Api27.png differ diff --git a/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png b/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png new file mode 100644 index 000000000..9466cfca1 Binary files /dev/null and b/app/src/androidTest/screenshots/edgeToEdge_Phone_systemBar_Api27.png differ diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..36313049e --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 1ab3a2ca0..d5de40b34 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -34,6 +34,7 @@ class AndroidApplicationConventionPlugin : Plugin { apply(plugin = "org.jetbrains.kotlin.android") apply(plugin = "nowinandroid.android.lint") apply(plugin = "com.dropbox.dependency-guard") + apply(plugin = "com.dropbox.dropshots") extensions.configure { configureKotlinAndroid(this) @@ -41,6 +42,8 @@ class AndroidApplicationConventionPlugin : Plugin { @Suppress("UnstableApiUsage") testOptions.animationsDisabled = true configureGradleManagedDevices(this) + testOptions.emulatorControl.enable = true +// testOptions.unitTests.isIncludeAndroidResources } extensions.configure { configurePrintApksTask(this) diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 1af5523c5..e04635d02 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -32,6 +32,7 @@ class AndroidFeatureConventionPlugin : Plugin { extensions.configure { testOptions.animationsDisabled = true + testOptions.emulatorControl.enable = true configureGradleManagedDevices(this) } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 3fe727410..6dea352e4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -40,6 +40,7 @@ class AndroidLibraryConventionPlugin : Plugin { defaultConfig.targetSdk = 35 defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testOptions.animationsDisabled = true + testOptions.emulatorControl.enable = true configureFlavors(this) configureGradleManagedDevices(this) // The resource prefix is derived from the module name, diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt index f67e9093d..f6a599cd2 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/GradleManagedDevices.kt @@ -27,12 +27,12 @@ import org.gradle.kotlin.dsl.invoke internal fun configureGradleManagedDevices( commonExtension: CommonExtension<*, *, *, *, *, *>, ) { - val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd") + val pixel4 = DeviceConfig("Pixel 4", 27, "aosp") val pixel6 = DeviceConfig("Pixel 6", 31, "aosp") - val pixelC = DeviceConfig("Pixel C", 30, "aosp-atd") + val pixelC = DeviceConfig("Pixel C", 30, "aosp") val allDevices = listOf(pixel4, pixel6, pixelC) - val ciDevices = listOf(pixel4, pixelC) + val ciDevices = listOf(pixel4, pixel6, pixelC) commonExtension.testOptions { managedDevices { diff --git a/build.gradle.kts b/build.gradle.kts index b7989bab4..da1ea46cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,6 +52,7 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dependencyGuard) apply false + alias(libs.plugins.dropshots) apply false alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.firebase.perf) apply false alias(libs.plugins.gms) apply false diff --git a/gradle.properties b/gradle.properties index 2e9d9fc30..f0f1407f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,12 +54,14 @@ kotlin.code.style=official # Disable build features that are enabled by default, # https://developer.android.com/build/releases/gradle-plugin#default-changes -android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false # Run Roborazzi screenshot tests with the local tests roborazzi.test.verify=true +# Espresso Device +android.experimental.androidTest.enableEmulatorControl=true + # Prevent uninstall app after instrumented tests # https://issuetracker.google.com/issues/295039976 android.injected.androidTest.leaveApksInstalledAfterRun=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b6f96968..801e7da44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ androidxCore = "1.15.0" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.1" androidxEspresso = "3.6.1" +androidxEspressoDevice = "1.0.0-beta01" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.8.7" androidxLintGradle = "1.0.0-alpha03" @@ -32,6 +33,7 @@ androidxWindowManager = "1.3.0" androidxWork = "2.10.0" coil = "2.7.0" dependencyGuard = "0.5.0" +dropshots = "0.4.2" firebaseBom = "33.7.0" firebaseCrashlyticsPlugin = "3.0.2" firebasePerfPlugin = "1.4.2" @@ -102,6 +104,7 @@ androidx-navigation-testing = { group = "androidx.navigation", name = "navigatio androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" } androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } +androidx-test-espresso-device = { group = "androidx.test.espresso", name = "espresso-device", version.ref = "androidxEspressoDevice" } androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } @@ -170,6 +173,7 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark"} compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" } +dropshots = { id = "com.dropbox.dropshots", version.ref = "dropshots" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }