Skip to content

Commit

Permalink
implement automatic screenshot generation
Browse files Browse the repository at this point in the history
  • Loading branch information
johan12345 committed Jan 18, 2024
1 parent f6feb2c commit 03ca287
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 18 deletions.
113 changes: 113 additions & 0 deletions .github/workflows/screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 ~/.android
echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > ~/.android/debug.keystore -d
- 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 swiftshader_indirect -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 swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
adb shell input keyevent 82
# Start demo mode
adb shell settings put global sysui_demo_allowed 1
# Display time 12:00
adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200
# Display full mobile data without type
adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 4 -e datatype false
adb shell am broadcast -a com.android.systemui.demo -e command network -e wifi show -e level 4 -e fully true
# Hide notifications
adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false
# Show full battery but not in charging state
adb shell am broadcast -a com.android.systemui.demo -e command battery -e plugged false -e level 100
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 -q en-US,de-DE
adb shell am broadcast -a com.android.systemui.demo -e command exit
- name: Upload screenshots as artifacts
uses: actions/upload-artifact@v3
with:
name: screenshots
path: fastlane/metadata/android
8 changes: 6 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ dependencies {
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")

// AnyMaps
val anyMapsVersion = "8f1226e1c5"
val anyMapsVersion = "b27457c8f4"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
Expand Down Expand Up @@ -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")
Expand All @@ -327,11 +328,14 @@ 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("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")

Expand Down
Original file line number Diff line number Diff line change
@@ -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<out FragmentActivity>
) : IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()

// 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<View>(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<out FragmentActivity>
) {
activityScenario.onActivity {
this.activity = it
}
}

/**
* Find all binding classes in all currently available fragments.
*/
private fun getBindings(): List<ViewDataBinding> {
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
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)
Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())

val context = InstrumentationRegistry.getInstrumentation().targetContext
val prefs = PreferenceDataSource(context)
prefs.dataSourceSet = true
prefs.welcomeDialogShown = true
prefs.privacyAccepted = true
prefs.opensourceDonationsDialogShown = true
prefs.chargepriceMyVehicles = setOf("b58bc94d-d929-ad71-d95b-08b877bf76ba")
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, 49219L 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<MapsActivity> = 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))
}

@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())
Screengrab.screenshot("03_prices")

onView(isRoot()).perform(pressBack())
onView(isRoot()).perform(pressBack())

Thread.sleep(1000L)

onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.menu_edit_filters)).perform(click())

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(3000L)
Screengrab.screenshot("04_favorites")
}
}
Loading

0 comments on commit 03ca287

Please sign in to comment.