From 2491ab8c5388efff170ae23273ffee50e3db2703 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sat, 16 Dec 2023 16:57:12 +0100 Subject: [PATCH] implement automatic screenshot generation --- .github/workflows/screenshots.yml | 97 +++++++++++ app/build.gradle.kts | 7 +- .../screenshot/DataBindingIdlingResource.kt | 112 +++++++++++++ .../evmap/screenshot/ScreenshotTest.kt | 155 ++++++++++++++++++ .../ScreenshotyScreenshotStrategy.kt | 24 +++ .../evmap/storage/SavedRegionDaoTest.kt | 2 +- app/src/debug/AndroidManifest.xml | 22 ++- .../java/net/vonforst/evmap/DebugInits.kt | 24 ++- .../java/net/vonforst/evmap/MapsActivity.kt | 22 ++- .../evmap/viewmodel/ChargepriceViewModel.kt | 7 + .../java/net/vonforst/evmap/DebugInits.kt | 8 +- 11 files changed, 463 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/screenshots.yml create mode 100644 app/src/androidTest/java/net/vonforst/evmap/screenshot/DataBindingIdlingResource.kt create mode 100644 app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotTest.kt create mode 100644 app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotyScreenshotStrategy.kt rename app/src/androidTest/java/{com/johan => net/vonforst}/evmap/storage/SavedRegionDaoTest.kt (98%) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml new file mode 100644 index 000000000..f1fe2dbba --- /dev/null +++ b/.github/workflows/screenshots.yml @@ -0,0 +1,97 @@ +on: + push: + branches: + - '*' + +name: Generate Screenshots + +jobs: + + screenshot: + name: Generate screenshots + runs-on: ubuntu-latest + strategy: + matrix: + api-level: [ 34 ] + steps: + - 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 + + - name: Check out code + uses: actions/checkout@v4 + + - name: Retrieve debug keystore + env: + DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} + run: | + mkdir ~/.config/.android + echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > ~/.config/.android/debug.keystore + + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: ${{ runner.os }}-avd-api${{ matrix.api-level }} + + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: playstore + arch: x86_64 + profile: pixel_4 + force-avd-creation: false + ram-size: 2048M + disk-size: 4096M + emulator-options: -no-window -gpu swangle -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: echo "Generated AVD snapshot for caching." + + - name: Set up Java environment + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'zulu' + cache: 'gradle' + + - name: Build app + run: ./gradlew assembleGoogleNormalDebug assembleGoogleNormalAndroidTest + env: + GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }} + OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }} + CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }} + MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }} + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }} + ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }} + + - name: Run emulator and generate screenshots + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: playstore + arch: x86_64 + profile: pixel_4 + force-avd-creation: false + ram-size: 2048M + disk-size: 4096M + emulator-options: -no-snapshot-save -no-window -gpu swangle -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: | + adb shell input keyevent 82 + adb shell am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS + + fastlane screengrab --app_apk_path app/build/outputs/apk/googleNormal/debug/app-google-normal-debug.apk --test_apk_path app/build/outputs/apk/androidTest/googleNormal/debug/app-google-normal-debug-androidTest.apk --tests_package_name=net.vonforst.evmap.debug.test --app_package_name net.vonforst.evmap.debug -p net.vonforst.evmap.screenshot --use_timestamp_suffix false --clear_previous_screenshots true --reinstall_app true -q en-US,de-DE + + - name: Upload screenshots as artifacts + uses: actions/upload-artifact@v3 + with: + name: screenshots + path: fastlane/metadata/android diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a8844b3b..43e6d5436 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -317,6 +317,7 @@ dependencies { debugImplementation("com.facebook.flipper:flipper:0.238.0") debugImplementation("com.facebook.soloader:soloader:0.10.5") debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0") + debugImplementation("androidx.test.espresso:espresso-idling-resource:3.5.1") // testing testImplementation("junit:junit:4.13.2") @@ -327,11 +328,15 @@ dependencies { testImplementation("androidx.test:core:1.5.0") testImplementation("androidx.arch.core:core-testing:2.2.0") testImplementation("androidx.car.app:app-testing:$carAppVersion") - testImplementation("androidx.test:core:1.5.0") androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") + androidTestImplementation("eu.bolt:screenshotty:1.0.4") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1") + androidTestImplementation("androidx.test:rules:1.5.0") androidTestImplementation("androidx.arch.core:core-testing:2.2.0") + androidTestImplementation("tools.fastlane:screengrab:2.1.1") kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0") diff --git a/app/src/androidTest/java/net/vonforst/evmap/screenshot/DataBindingIdlingResource.kt b/app/src/androidTest/java/net/vonforst/evmap/screenshot/DataBindingIdlingResource.kt new file mode 100644 index 000000000..36c0117e1 --- /dev/null +++ b/app/src/androidTest/java/net/vonforst/evmap/screenshot/DataBindingIdlingResource.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2019 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 + * + * http://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 net.vonforst.evmap.screenshot + +import android.view.View +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.FragmentActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.IdlingResource +import androidx.test.ext.junit.rules.ActivityScenarioRule +import java.util.UUID + +/** + * An espresso idling resource implementation that reports idle status for all data binding + * layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet. + * + * Since this application only uses fragments, the resource only checks the fragments and their + * children instead of the whole view tree. + * + * Tracking bug: https://github.com/android/android-test/issues/317 + */ +class DataBindingIdlingResource( + activityScenarioRule: ActivityScenarioRule +) : IdlingResource { + // list of registered callbacks + private val idlingCallbacks = mutableListOf() + + // give it a unique id to workaround an espresso bug where you cannot register/unregister + // an idling resource w/ the same name. + private val id = UUID.randomUUID().toString() + + // holds whether isIdle is called and the result was false. We track this to avoid calling + // onTransitionToIdle callbacks if Espresso never thought we were idle in the first place. + private var wasNotIdle = false + + lateinit var activity: FragmentActivity + + override fun getName() = "DataBinding $id" + + init { + monitorActivity(activityScenarioRule.scenario) + } + + override fun isIdleNow(): Boolean { + val idle = !getBindings().any { it.hasPendingBindings() } + @Suppress("LiftReturnOrAssignment") + if (idle) { + if (wasNotIdle) { + // notify observers to avoid espresso race detector + idlingCallbacks.forEach { it.onTransitionToIdle() } + } + wasNotIdle = false + } else { + wasNotIdle = true + // check next frame + activity.findViewById(android.R.id.content).postDelayed({ + isIdleNow + }, 16) + } + return idle + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { + idlingCallbacks.add(callback) + } + + /** + * Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource]. + */ + private fun monitorActivity( + activityScenario: ActivityScenario + ) { + activityScenario.onActivity { + this.activity = it + } + } + + /** + * Find all binding classes in all currently available fragments. + */ + private fun getBindings(): List { + val fragments = (activity as? FragmentActivity) + ?.supportFragmentManager + ?.fragments + + val bindings = + fragments?.mapNotNull { + it.view?.getBinding() + } ?: emptyList() + val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments } + ?.mapNotNull { it.view?.getBinding() } ?: emptyList() + + return bindings + childrenBindings + } +} + +private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this) \ No newline at end of file diff --git a/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotTest.kt b/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotTest.kt new file mode 100644 index 000000000..faf1b776c --- /dev/null +++ b/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotTest.kt @@ -0,0 +1,155 @@ +package net.vonforst.evmap.screenshot + +import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.content.Intent +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.pressBack +import androidx.test.espresso.contrib.DrawerActions +import androidx.test.espresso.contrib.NavigationViewActions +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import androidx.test.rule.GrantPermissionRule.grant +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import kotlinx.coroutines.runBlocking +import net.vonforst.evmap.EXTRA_CHARGER_ID +import net.vonforst.evmap.EXTRA_LAT +import net.vonforst.evmap.EXTRA_LON +import net.vonforst.evmap.EspressoIdlingResource +import net.vonforst.evmap.MapsActivity +import net.vonforst.evmap.R +import net.vonforst.evmap.api.goingelectric.GEReferenceData +import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper +import net.vonforst.evmap.model.Favorite +import net.vonforst.evmap.storage.AppDatabase +import net.vonforst.evmap.storage.PreferenceDataSource +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import tools.fastlane.screengrab.FalconScreenshotStrategy +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.ScreenshotStrategy +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy +import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar +import tools.fastlane.screengrab.cleanstatusbar.IconVisibility +import tools.fastlane.screengrab.locale.LocaleTestRule +import java.lang.RuntimeException + + +@RunWith(AndroidJUnit4::class) +class ScreenshotTest { + companion object { + @JvmStatic + @BeforeClass + fun beforeAll() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + + CleanStatusBar() + .setWifiVisibility(IconVisibility.HIDE) + .setMobileNetworkVisibility(IconVisibility.HIDE) + .setClock("1200") + .enable() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + val prefs = PreferenceDataSource(context) + prefs.dataSourceSet = true + prefs.welcomeDialogShown = true + prefs.privacyAccepted = true + prefs.opensourceDonationsDialogShown = true + prefs.chargepriceMyVehicles = setOf("733f75ea-02ed-4602-bf96-4e1733ade9c1") + prefs.appStartCounter = 0 + + // insert favorites + val db = AppDatabase.getInstance(context) + val api = GoingElectricApiWrapper( + context.getString(R.string.goingelectric_key), + context = context + ) + val ids = listOf(70774L to true, 40315L to true, 65330L to true, 62489L to false) + runBlocking { + val refData = api.getReferenceData().data as GEReferenceData + ids.forEachIndexed { i, (id, favorite) -> + val detail = api.getChargepointDetail(refData, id).data!! + db.chargeLocationsDao().insert(detail) + if (db.favoritesDao().findFavorite(id, "goingelectric") == null && favorite) { + db.favoritesDao().insert( + Favorite( + chargerId = id, + chargerDataSource = "goingelectric" + ) + ) + } + } + } + } + } + + @get:Rule + val localeTestRule = LocaleTestRule() + + @get:Rule + val activityRule: ActivityScenarioRule = ActivityScenarioRule( + Intent( + InstrumentationRegistry.getInstrumentation().targetContext, + MapsActivity::class.java + ).apply { + putExtra(EXTRA_CHARGER_ID, 62489L) + putExtra(EXTRA_LAT, 53.099512) + putExtra(EXTRA_LON, 9.981884) + }) + + @get:Rule + val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION) + + @Before + fun setUp() { + IdlingRegistry.getInstance().register(DataBindingIdlingResource(activityRule)) + + activityRule.scenario.onActivity { + Screengrab.setDefaultScreenshotStrategy(ScreenshotyScreenshotStrategy(it)) + } + } + + @Test + fun testTakeScreenshot() { + Thread.sleep(10000L) + Screengrab.screenshot("01_map") + + onView(withId(R.id.topPart)).perform(click()) + Screengrab.screenshot("02_detail") + + onView(withId(R.id.btnChargeprice)).perform(click()) + Thread.sleep(5000L) + Screengrab.screenshot("03_prices") + + onView(isRoot()).perform(pressBack()) + onView(isRoot()).perform(pressBack()) + + Thread.sleep(1000L) + + onView(withId(R.id.menu_filter)).perform(click()) + Thread.sleep(1000L) + onView(withText(R.string.menu_edit_filters)).perform(click()) + + Thread.sleep(1000L) + + Screengrab.screenshot("05_filters") + onView(isRoot()).perform(pressBack()) + + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.favs)) + + Thread.sleep(5000L) + Screengrab.screenshot("04_favorites") + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotyScreenshotStrategy.kt b/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotyScreenshotStrategy.kt new file mode 100644 index 000000000..6384919e8 --- /dev/null +++ b/app/src/androidTest/java/net/vonforst/evmap/screenshot/ScreenshotyScreenshotStrategy.kt @@ -0,0 +1,24 @@ +package net.vonforst.evmap.screenshot + +import android.app.Activity +import eu.bolt.screenshotty.ScreenshotBitmap +import eu.bolt.screenshotty.ScreenshotManagerBuilder +import tools.fastlane.screengrab.ScreenshotCallback +import tools.fastlane.screengrab.ScreenshotStrategy + +class ScreenshotyScreenshotStrategy(activity: Activity) : ScreenshotStrategy { + val screenshotMan = ScreenshotManagerBuilder(activity).build() + override fun takeScreenshot(screenshotName: String, screenshotCallback: ScreenshotCallback) { + screenshotMan.makeScreenshot().observe({ + when (it) { + is ScreenshotBitmap -> screenshotCallback.screenshotCaptured( + screenshotName, + it.bitmap + ) + } + }, { + throw it + }) + } + +} diff --git a/app/src/androidTest/java/com/johan/evmap/storage/SavedRegionDaoTest.kt b/app/src/androidTest/java/net/vonforst/evmap/storage/SavedRegionDaoTest.kt similarity index 98% rename from app/src/androidTest/java/com/johan/evmap/storage/SavedRegionDaoTest.kt rename to app/src/androidTest/java/net/vonforst/evmap/storage/SavedRegionDaoTest.kt index 9facff520..e48dbfe15 100644 --- a/app/src/androidTest/java/com/johan/evmap/storage/SavedRegionDaoTest.kt +++ b/app/src/androidTest/java/net/vonforst/evmap/storage/SavedRegionDaoTest.kt @@ -1,4 +1,4 @@ -package com.johan.evmap.storage +package net.vonforst.evmap.storage import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 519962535..251e5650c 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,5 +1,25 @@ - + + + + + + + + + + + + + + + +