Dev: add simple automated UI test setup (#1027)

## Description
This PR is mostly a proof-of-concept for setting up automated UI
testing. The tests use Roboelectric so they can run without an emulator.
There is also a sample instrumented test for running on emulators or
devices for future tests.

It adds two basic automated UI tests for adding a server. These are sort
integration tests because they use the actual implementations for the
view model and services like `ServerRepository` and `JellyfinServerDao`.

Also, moves the bulk of `MainActivity`'s compose code into a function
for future tests.

### Related issues
N/A

### Testing
Local testing

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-03-03 13:46:46 -05:00 committed by GitHub
parent 8935067c5a
commit 0004701296
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 990 additions and 163 deletions

View file

@ -9,10 +9,12 @@ import androidx.lifecycle.map
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.services.hilt.IoDispatcher
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
@ -41,6 +43,7 @@ class ServerRepository
val serverDao: JellyfinServerDao,
val apiClient: ApiClient,
val userPreferencesDataStore: DataStore<AppPreferences>,
@param:IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {
private var _current = EqualityMutableLiveData<CurrentUser?>(null)
val current: LiveData<CurrentUser?> = _current
@ -57,7 +60,7 @@ class ServerRepository
* The current user is removed
*/
suspend fun addAndChangeServer(server: JellyfinServer) {
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
serverDao.addOrUpdateServer(server)
}
apiClient.update(baseUrl = server.url, accessToken = null)
@ -71,7 +74,7 @@ class ServerRepository
server: JellyfinServer,
user: JellyfinUser,
): CurrentUser? =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
if (server.id != user.serverId) {
throw IllegalStateException("User is not part of the server")
}
@ -126,7 +129,7 @@ class ServerRepository
return null
}
val serverAndUsers =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
serverDao.getServer(serverId)
}
if (serverAndUsers != null) {
@ -149,7 +152,7 @@ class ServerRepository
}
suspend fun fetchLastUsedServer(serverId: UUID?): JellyfinServer? =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
serverId?.let { serverDao.getServer(serverId)?.server }
}
@ -163,7 +166,7 @@ class ServerRepository
suspend fun changeUser(
serverUrl: String,
authenticationResult: AuthenticationResult,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
val accessToken = authenticationResult.accessToken
if (accessToken != null) {
val authedUser = authenticationResult.user
@ -213,7 +216,7 @@ class ServerRepository
}
apiClient.update(accessToken = null)
}
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
serverDao.deleteUser(user.serverId, user.id)
}
}
@ -233,7 +236,7 @@ class ServerRepository
}
apiClient.update(baseUrl = null, accessToken = null)
}
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
serverDao.deleteServer(server.id)
}
}
@ -252,7 +255,7 @@ class ServerRepository
suspend fun setUserPin(
user: JellyfinUser,
pin: String?,
) = withContext(Dispatchers.IO) {
) = withContext(ioDispatcher) {
val newUser = user.copy(pin = pin)
val updatedUser = serverDao.addOrUpdateUser(newUser)
if (currentUser.value?.id == updatedUser.id && currentServer.value?.id == user.serverId) {
@ -265,7 +268,7 @@ class ServerRepository
}
suspend fun authorizeQuickConnect(code: String): Boolean =
withContext(Dispatchers.IO) {
withContext(ioDispatcher) {
val userId = currentUser.value?.id
if (userId == null) {
Timber.e("No user logged in for Quick Connect authorization")