Skip to content

Commit

Permalink
Add very simple smoke test
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Jun 14, 2023
1 parent 1a8d0fc commit 3d2a2c3
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 114 deletions.
30 changes: 24 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,35 @@ jobs:
- name: Build Android App (skipping benchmark variant)
run: |
./gradlew spotlessCheck \
:android-app:app:bundle \
:android-app:app:build \
jvmTest \
lint \
-x :android-app:app:assembleStandardBenchmark \
-x :android-app:app:bundleStandardBenchmark
:android-app:app:bundle \
:android-app:app:build \
jvmTest \
lint \
-x :android-app:app:assembleStandardBenchmark \
-x :android-app:app:bundleStandardBenchmark
- name: Run smoke tests on Gradle Managed Device
# --info used to add a repro to https://issuetracker.google.com/issues/193118030
# config cache is disabled due to https://issuetracker.google.com/issues/262270582
run: |
./gradlew api31QaDebugAndroidTest \
-Dorg.gradle.workers.max=1 \
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" \
--info \
--no-configuration-cache
- name: Clean secrets
if: always()
run: ./release/clean-secrets.sh

- name: Sanitize output file names (https://issuetracker.google.com/issues/263305468)
run: |
sanitizefilename() {
mv "${1}" "${1//[^A-Za-z0-9._-]/_}"
}
export -f sanitizefilename
find android-app/app/build/outputs -type f -exec bash -c 'sanitizefilename "$0"' {} \;
- name: Upload build outputs
if: always()
uses: actions/upload-artifact@v3
Expand Down
21 changes: 17 additions & 4 deletions android-app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ android {
versionCode = (android.defaultConfig.versionCode ?: 0) + 1
}
}

testOptions {
managedDevices {
devices {
create<com.android.build.api.dsl.ManagedVirtualDevice>("api31") {
device = "Pixel 6"
apiLevel = 31
systemImageSource = "aosp"
}
}
}
}
}

androidComponents {
Expand Down Expand Up @@ -202,10 +214,11 @@ dependencies {

qaImplementation(libs.leakCanary)

testImplementation(libs.junit)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
testImplementation(libs.androidx.test.rules)
androidTestImplementation(projects.androidApp.commonTest)
androidTestImplementation(libs.androidx.uiautomator)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.rules)
}

if (file("google-services.json").exists()) {
Expand Down
48 changes: 48 additions & 0 deletions android-app/app/src/androidTest/kotlin/app/tivi/SmokeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

package app.tivi

import android.content.Intent
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import app.tivi.app.test.AppScenarios
import org.junit.Assert.assertNotNull
import org.junit.Test

class SmokeTest {

@Test
fun openApp() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

startAppAndWait(device)

// Run through the main navigation items
AppScenarios.mainNavigationItems(device)
}
}

private fun startAppAndWait(device: UiDevice) {
device.pressHome()

// Wait for launcher
val launcherPackage = device.launcherPackageName
assertNotNull(launcherPackage)
device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), 5_000)

// Launch the app
val context = ApplicationProvider.getApplicationContext<TiviApplication>()
val packageName = context.packageName
val intent = context.packageManager.getLaunchIntentForPackage(packageName)!!.apply {
// Clear out any previous instances
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
context.startActivity(intent)

// Wait for the app to appear
device.wait(Until.hasObject(By.pkg(packageName).depth(0)), 5_000)
}
2 changes: 2 additions & 0 deletions android-app/benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ dependencies {
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.test.junit)
implementation(libs.kotlin.coroutines.android)

implementation(projects.androidApp.commonTest)
}

androidComponents {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@

package app.tivi.benchmark

import android.os.SystemClock
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import app.tivi.app.test.AppScenarios
import org.junit.Rule
import org.junit.Test

Expand All @@ -26,102 +20,7 @@ class BaselineProfileGenerator {
maxIterations = 8,
) {
startActivityAndWait()
device.waitForIdle()

// -------------
// Discover
// -------------
device.testDiscover() || return@collectBaselineProfile
device.navigateFromDiscoverToShowDetails()

// -------------
// Show Details
// -------------
device.testShowDetails() || return@collectBaselineProfile
device.navigateFromShowDetailsToSeasons()

// -------------
// Seasons
// -------------
device.testSeasons() || return@collectBaselineProfile
device.navigateFromSeasonsToEpisodeDetails()

// -------------
// Episode details
// -------------
device.testEpisodeDetails() || return@collectBaselineProfile
}

private fun UiDevice.testDiscover(): Boolean {
// Scroll one of the Discover Carousels
waitForObject(By.res("discover_carousel"))
.scroll(Direction.RIGHT, 1f)
waitForObject(By.res("discover_carousel"))
.scroll(Direction.LEFT, 1f)

return wait(Until.hasObject(By.res("discover_carousel_item")), 5_000)
}

private fun UiDevice.navigateFromDiscoverToShowDetails() {
// Open a show from one of the carousels
waitForObject(By.res("discover_carousel_item")).click()
waitForIdle()
}

private fun UiDevice.testShowDetails(): Boolean {
// Follow the show
waitForObject(By.res("show_details_follow_button"))
.click()

// Wait 10 seconds for a season item to show
for (i in 1..10) {
if (hasObject(By.res("show_details_season_item"))) {
return true
}

SystemClock.sleep(1000)

// Scroll to the end to show the seasons
waitForObject(By.res("show_details_lazycolumn"))
.scroll(Direction.DOWN, 1f)
}

return false
}

private fun UiDevice.navigateFromShowDetailsToSeasons() {
waitForObject(By.res("show_details_season_item")).click()
waitForIdle()
}

private fun UiDevice.testSeasons(): Boolean {
// Not much to test here at the moment
return wait(Until.hasObject(By.res("show_seasons_episode_item")), 5_000)
}

private fun UiDevice.navigateFromSeasonsToEpisodeDetails() {
waitForObject(By.res("show_seasons_episode_item")).click()
waitForIdle()
// Run through the main navigation items
AppScenarios.mainNavigationItems(device)
}

private fun UiDevice.testEpisodeDetails(): Boolean {
with(waitForObject(By.res("episode_details"))) {
// Need to 'inset' the gesture so that we don't swipe
// the notification tray down
setGestureMargin(displayWidth / 10)

// Swipe the bottom sheet 'up', then 'down'
scroll(Direction.DOWN, 0.8f)
scroll(Direction.UP, 0.8f)
}
return true
}
}

private fun UiDevice.waitForObject(selector: BySelector, timeout: Long = 5_000): UiObject2 {
if (wait(Until.hasObject(selector), timeout)) {
return findObject(selector)
}

error("Object with selector [$selector] not found")
}
17 changes: 17 additions & 0 deletions android-app/common-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2023, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0


plugins {
id("app.tivi.android.library")
id("app.tivi.kotlin.android")
}

android {
namespace = "app.tivi.app.test"
}

dependencies {
implementation(projects.core.base)
api(libs.androidx.uiautomator)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2023, Google LLC, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

package app.tivi.app.test

import android.os.SystemClock
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.SearchCondition
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

object AppScenarios {
fun mainNavigationItems(device: UiDevice) {
device.waitForIdle()

// -------------
// Discover
// -------------
device.testDiscover() || return
device.navigateFromDiscoverToShowDetails()

// -------------
// Show Details
// -------------
device.testShowDetails() || return
device.navigateFromShowDetailsToSeasons()

// -------------
// Seasons
// -------------
device.testSeasons() || return
device.navigateFromSeasonsToEpisodeDetails()

// -------------
// Episode details
// -------------
device.testEpisodeDetails() || return
}
}

private fun UiDevice.testDiscover(): Boolean {
// Might need to wait a while for the app to load
waitForObject(By.res("discover_carousel"), 30.seconds).run {
// Scroll one of the Discover Carousels
scroll(Direction.RIGHT, 1f)
scroll(Direction.LEFT, 1f)
}

return wait(Until.hasObject(By.res("discover_carousel_item")), 5.seconds)
}

private fun UiDevice.navigateFromDiscoverToShowDetails() {
// Open a show from one of the carousels
waitForObject(By.res("discover_carousel_item")).click()
waitForIdle()
}

private fun UiDevice.testShowDetails(): Boolean {
// Follow the show
waitForObject(By.res("show_details_follow_button")).click()

// Keep scrolling to the end of the LazyColumn, waiting for a season item
repeat(20) {
if (hasObject(By.res("show_details_season_item"))) {
return true
}

SystemClock.sleep(1.seconds.inWholeMilliseconds)

// Scroll to the end to show the seasons
waitForObject(By.res("show_details_lazycolumn"))
.scroll(Direction.DOWN, 1f)
}

return false
}

private fun UiDevice.navigateFromShowDetailsToSeasons() {
waitForObject(By.res("show_details_season_item")).click()
waitForIdle()
}

private fun UiDevice.testSeasons(): Boolean {
// Not much to test here at the moment
return wait(Until.hasObject(By.res("show_seasons_episode_item")), 5.seconds)
}

private fun UiDevice.navigateFromSeasonsToEpisodeDetails() {
waitForObject(By.res("show_seasons_episode_item")).click()
waitForIdle()
}

private fun UiDevice.testEpisodeDetails(): Boolean {
waitForObject(By.res("episode_details")).run {
// Need to 'inset' the gesture so that we don't swipe
// the notification tray down
setGestureMargin(displayWidth / 10)

// Swipe the bottom sheet 'up', then 'down'
scroll(Direction.DOWN, 0.8f)
scroll(Direction.UP, 0.8f)
}
return true
}

fun UiDevice.waitForObject(selector: BySelector, timeout: Duration = 5.seconds): UiObject2 {
if (wait(Until.hasObject(selector), timeout)) {
return findObject(selector)
}
error("Object with selector [$selector] not found")
}

fun <R> UiDevice.wait(condition: SearchCondition<R>, timeout: Duration): R {
return wait(condition, timeout.inWholeMilliseconds)
}
Loading

0 comments on commit 3d2a2c3

Please sign in to comment.