parent
bd98967b78
commit
2d83b38387
6 changed files with 232 additions and 1 deletions
|
@ -21,6 +21,8 @@ android {
|
||||||
|
|
||||||
versionCode = Versions.appCode
|
versionCode = Versions.appCode
|
||||||
versionName = Versions.appName
|
versionName = Versions.appName
|
||||||
|
|
||||||
|
testInstrumentationRunner = "dev.jdtech.jellyfin.HiltTestRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -112,4 +114,10 @@ dependencies {
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.android.desugar.jdk)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)))
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,18 @@ androidx-preference = "1.2.1"
|
||||||
androidx-recyclerview = "1.3.2"
|
androidx-recyclerview = "1.3.2"
|
||||||
androidx-room = "2.6.1"
|
androidx-room = "2.6.1"
|
||||||
androidx-swiperefreshlayout = "1.1.0"
|
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-tv = "1.0.0-alpha10"
|
||||||
androidx-work = "2.9.0"
|
androidx-work = "2.9.0"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
hilt = "2.51.1"
|
hilt = "2.51.1"
|
||||||
compose-destinations = "1.10.2"
|
compose-destinations = "1.10.2"
|
||||||
jellyfin = "1.4.7"
|
jellyfin = "1.4.7"
|
||||||
|
junit = "4.13.2"
|
||||||
kotlin = "1.9.23"
|
kotlin = "1.9.23"
|
||||||
kotlinx-serialization = "1.6.3"
|
kotlinx-serialization = "1.6.3"
|
||||||
ksp = "1.9.23-1.0.20"
|
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-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-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
|
||||||
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" }
|
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-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-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 = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", 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" }
|
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-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" }
|
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 = { 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" }
|
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
||||||
jellyfin-core = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin" }
|
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" }
|
libmpv = { group = "dev.jdtech.mpv", name = "libmpv", version.ref = "libmpv" }
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
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" }
|
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" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
|
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"]
|
Loading…
Reference in a new issue