test: main flow

Closes #727
This commit is contained in:
Jarne Demeulemeester 2024-04-13 16:01:18 +02:00
parent bd98967b78
commit 2d83b38387
No known key found for this signature in database
GPG key ID: 1E5C6AFBD622E9F5
6 changed files with 232 additions and 1 deletions

View file

@ -21,6 +21,8 @@ android {
versionCode = Versions.appCode
versionName = Versions.appName
testInstrumentationRunner = "dev.jdtech.jellyfin.HiltTestRunner"
}
buildTypes {
@ -112,4 +114,10 @@ dependencies {
implementation(libs.timber)
coreLibraryDesugaring(libs.android.desugar.jdk)
androidTestImplementation(libs.androidx.room.runtime)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.hilt.android.testing)
kspTest(libs.hilt.android.compiler)
}

View file

@ -0,0 +1,16 @@
package dev.jdtech.jellyfin
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?,
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

View file

@ -0,0 +1,95 @@
package dev.jdtech.jellyfin
import android.util.Log
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dev.jdtech.jellyfin.di.DatabaseModule
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
@UninstallModules(DatabaseModule::class)
class MainActivityTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Before
fun setUp() {
hiltRule.inject()
val context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun testMainFlow() {
launchActivity<MainActivity>().use {
// Wait for the app to load
waitForElement(allOf(withId(R.id.edit_text_server_address), isDisplayed()))
// Connect to demo server
onView(withId(R.id.edit_text_server_address)).perform(typeText("https://demo.jellyfin.org/stable"), closeSoftKeyboard())
onView(withId(R.id.button_connect)).perform(click())
// Connecting to the server
waitForElement(allOf(withId(R.id.edit_text_username), isDisplayed()))
// Login
onView(withId(R.id.edit_text_username)).perform(typeText("demo"), closeSoftKeyboard())
onView(withId(R.id.button_login)).perform(click())
// Navigate to My media
waitForElement(allOf(withText("Continue Watching"), isDisplayed()))
onView(withId(R.id.mediaFragment)).perform(click())
// Navigate to movies
waitForElement(allOf(withText("Movies"), isDisplayed()))
onView(withText("Movies")).perform(click())
// Navigate to Battle of the Stars
waitForElement(allOf(withText("Battle of the Stars"), isDisplayed()))
onView(withText("Battle of the Stars")).perform(click())
// Play the movie
waitForElement(allOf(withId(R.id.play_button), isEnabled()))
onView(withId(R.id.play_button)).perform(click())
// Wait for movie to start playing
waitForElement(allOf(withId(androidx.media3.ui.R.id.exo_buffering), isDisplayed()))
waitForElement(allOf(withId(androidx.media3.ui.R.id.exo_buffering), not(isDisplayed())))
// Navigate back
Espresso.pressBack()
}
}
}

View file

@ -0,0 +1,64 @@
package dev.jdtech.jellyfin
import android.view.View
import android.view.ViewTreeObserver
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.hamcrest.StringDescription
private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener {
private lateinit var callback: IdlingResource.ResourceCallback
private var matched = false
override fun getName() = "View property change callback"
override fun isIdleNow() = matched
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
override fun onDraw() {
matched = matcher.matches(view)
callback.onTransitionToIdle()
}
}
fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction {
override fun getConstraints(): Matcher<View> {
return CoreMatchers.any(View::class.java)
}
override fun getDescription(): String {
return StringDescription().let {
matcher.describeTo(it)
"wait until: $it"
}
}
override fun perform(uiController: UiController, view: View) {
if (!matcher.matches(view)) {
ViewPropertyChangeCallback(matcher, view).run {
try {
IdlingRegistry.getInstance().register(this)
view.viewTreeObserver.addOnDrawListener(this)
uiController.loopMainThreadUntilIdle()
} finally {
view.viewTreeObserver.removeOnDrawListener(this)
IdlingRegistry.getInstance().unregister(this)
}
}
}
}
}
fun waitForElement(matcher: Matcher<View>) {
onView(isRoot()).perform(waitUntil(hasDescendant(matcher)))
}

View file

@ -0,0 +1,29 @@
package dev.jdtech.jellyfin.di
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.jdtech.jellyfin.database.ServerDatabase
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseTestModule {
@Singleton
@Provides
fun provideServerDatabaseDao(@ApplicationContext app: Context): ServerDatabaseDao {
return Room.inMemoryDatabaseBuilder(
app.applicationContext,
ServerDatabase::class.java,
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
.getServerDatabaseDao()
}
}

View file

@ -17,12 +17,18 @@ androidx-preference = "1.2.1"
androidx-recyclerview = "1.3.2"
androidx-room = "2.6.1"
androidx-swiperefreshlayout = "1.1.0"
androidx-test-core = "1.5.0"
androidx-test-expresso = "3.5.1"
androidx-test-junit = "1.1.5"
androidx-test-rules = "1.5.0"
androidx-test-runner = "1.5.2"
androidx-tv = "1.0.0-alpha10"
androidx-work = "2.9.0"
coil = "2.6.0"
hilt = "2.51.1"
compose-destinations = "1.10.2"
jellyfin = "1.4.7"
junit = "4.13.2"
kotlin = "1.9.23"
kotlinx-serialization = "1.6.3"
ksp = "1.9.23-1.0.20"
@ -66,17 +72,27 @@ androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview"
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" }
androidx-work = { group = "androidx.work", name = "work-runtime", version.ref = "androidx-work" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" }
androidx-test-core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "androidx-test-core" }
androidx-test-expresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-expresso"}
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
androidx-test-rules = { group = "androidx.test" , name = "rules", version.ref = "androidx-test-rules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" }
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "androidx-tv" }
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "androidx-tv" }
androidx-work = { group = "androidx.work", name = "work-runtime", version.ref = "androidx-work" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" }
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
compose-destinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "compose-destinations" }
compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destinations" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
jellyfin-core = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
libmpv = { group = "dev.jdtech.mpv", name = "libmpv", version.ref = "libmpv" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version.ref = "media3-ffmpeg-decoder" }
@ -94,3 +110,6 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref =
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
[bundles]
androidx-test = ["androidx-test-core", "androidx-test-core-ktx", "androidx-test-expresso", "androidx-test-junit", "androidx-test-rules", "androidx-test-runner", "androidx-work-testing"]