Keep the person picker closed after switching users
All checks were successful
review/code-gpt Picker double-OK fix verified on the real box (B9): three single-OK person switches justin/mindel/shared, no crash, logcat clean; 45 targeted tests, final-head apk build
All checks were successful
review/code-gpt Picker double-OK fix verified on the real box (B9): three single-OK person switches justin/mindel/shared, no crash, logcat clean; 45 targeted tests, final-head apk build
This commit is contained in:
parent
e8d0c52088
commit
1c21f50acb
3 changed files with 168 additions and 3 deletions
|
|
@ -0,0 +1,157 @@
|
|||
package org.justinvisser.wolfskin.tv
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Looper
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.CurrentUser
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.*
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SetupDestination
|
||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||
import io.mockk.*
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.conductarr.MemoryCacheStore
|
||||
import org.justinvisser.wolfskin.design.*
|
||||
import org.justinvisser.wolfskin.tv.read.LibraryReadFacade
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** A selected identity gets a new app ViewModel without opening a second launch picker. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||
class PersonPickerSessionTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
private lateinit var previousUrl: String
|
||||
private var previousFixtures = false
|
||||
private var previousScreens = true
|
||||
private val models = mutableListOf<WolfskinShellViewModel>()
|
||||
private lateinit var session: WolfskinSession
|
||||
|
||||
@Before fun capturePreferences() {
|
||||
ConductarrPreference.initialize(rule.activity)
|
||||
WolfskinScreenPreference.initialize(rule.activity)
|
||||
previousUrl = ConductarrPreference.baseUrl
|
||||
previousFixtures = ConductarrPreference.fixtures
|
||||
previousScreens = WolfskinScreenPreference.enabled
|
||||
ConductarrPreference.updateFixtures(false)
|
||||
WolfskinScreenPreference.enabled = true
|
||||
}
|
||||
|
||||
@After fun restorePreferences() {
|
||||
models.forEach { it.viewModelScope.cancel() }
|
||||
ConductarrPreference.updateBaseUrl(previousUrl)
|
||||
ConductarrPreference.updateFixtures(previousFixtures)
|
||||
WolfskinScreenPreference.enabled = previousScreens
|
||||
}
|
||||
|
||||
private fun select(initial: Int, selected: Int, fromAvatar: Boolean = false, fast: Boolean = false, moves: List<Key> = emptyList()) {
|
||||
FakeConductarr().use { fake ->
|
||||
ConductarrPreference.updateBaseUrl(fake.url)
|
||||
ConductarrSession(MemoryCacheStore()).use { conductarr ->
|
||||
val server = JellyfinServer(UUID.randomUUID(), "Jellyfin", fake.url, null)
|
||||
val users = listOf("Justin" to JUSTIN_TOKEN, "Mindel" to MINDEL_TOKEN, "Shared" to SHARED_TOKEN).map { (name, token) ->
|
||||
JellyfinUser(id = UUID.randomUUID(), serverId = server.id, name = name, accessToken = token)
|
||||
}
|
||||
var current by mutableStateOf(CurrentUser(server, users[initial]))
|
||||
val repository = mockk<ServerRepository>(relaxed = true)
|
||||
coEvery { repository.serverDao.getServer(server.id) } returns JellyfinServerUsers(server, users)
|
||||
coEvery { repository.changeUser(server, any()) } coAnswers { CurrentUser(server, secondArg()) }
|
||||
val setup = mockk<SetupNavigationManager>(relaxed = true)
|
||||
every { setup.navigateTo(any()) } answers { current = (firstArg() as SetupDestination.AppContent).current }
|
||||
val libraries = mockk<LibraryReadFacade>(relaxed = true)
|
||||
coEvery { libraries.readDestinations(any()) } returns emptyMap()
|
||||
coEvery { libraries.hasAnimeTitles(any()) } returns false
|
||||
session = WolfskinSession()
|
||||
try {
|
||||
rule.setContent {
|
||||
key(current.user.id) {
|
||||
val navigation = remember { NavigationManager() }
|
||||
val model = remember {
|
||||
WolfskinShellViewModel(rule.activity, session, repository, navigation, setup, mockk(relaxed = true),
|
||||
libraries, WolfskinSeeAllLists(), conductarr, mockk(relaxed = true)).also { models.add(it) }
|
||||
}
|
||||
DisposableEffect(model) { onDispose { model.viewModelScope.cancel() } }
|
||||
WolfskinApp(server, current.user, navigation, UserPreferences(AppPreferences.getDefaultInstance(), null),
|
||||
viewModel = model, homeContent = { _, _, user, modifier ->
|
||||
androidx.tv.material3.Text("Home for ${user.name}", modifier)
|
||||
})
|
||||
}
|
||||
}
|
||||
awaitLoaded(users[initial])
|
||||
if (fromAvatar) {
|
||||
press(listOf(Key.DirectionCenter))
|
||||
press(listOf(Key.DirectionUp, Key.DirectionLeft, Key.DirectionCenter))
|
||||
}
|
||||
val actions = mutableListOf<ShellAction.Person>()
|
||||
rule.runOnIdle {
|
||||
val dispatch = session.onAction
|
||||
session.onAction = { action ->
|
||||
if (action is ShellAction.Person) actions.add(action)
|
||||
dispatch(action)
|
||||
}
|
||||
}
|
||||
if (fast) press(moves + Key.DirectionCenter) else {
|
||||
press(moves)
|
||||
press(listOf(Key.DirectionCenter))
|
||||
}
|
||||
awaitLoaded(users[selected])
|
||||
rule.runOnIdle {
|
||||
assertFalse("The replacement app must not reopen the picker after one OK", session.state!!.pickerVisible)
|
||||
assertEquals("home", session.state!!.route)
|
||||
assertEquals(users[selected].id.toString(), session.state!!.person.jellyfinUserId)
|
||||
assertEquals(listOf(users[selected].id.toString()), actions.map { it.person.jellyfinUserId })
|
||||
assertEquals(if (initial == selected) 1 else 2, models.size)
|
||||
}
|
||||
rule.onNodeWithText("Home for ${users[selected].name}").assertIsDisplayed()
|
||||
} finally { models.forEach { it.viewModelScope.cancel() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitLoaded(user: JellyfinUser) {
|
||||
try {
|
||||
rule.waitUntil(10_000) {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
models.lastOrNull()?.signedInPerson == user.name!!.lowercase() &&
|
||||
models.last().conductarr.identity.value?.signedIn == true &&
|
||||
models.last().conductarr.identity.value?.person?.endsWith(":${user.id}") == true &&
|
||||
session.state?.people?.size == 3 &&
|
||||
models.last().viewModelScope.coroutineContext[Job]!!.children.none { it.isActive }
|
||||
}
|
||||
} catch (timeout: ComposeTimeoutException) {
|
||||
throw AssertionError("Expected ${user.name}; models=${models.size}, signedIn=${models.last().signedInPerson}, " +
|
||||
"identity=${models.last().conductarr.identity.value?.person}, picker=${session.state?.pickerVisible}, " +
|
||||
"person=${session.state?.person}, people=${session.state?.people?.size}", timeout)
|
||||
}
|
||||
rule.waitForIdle()
|
||||
}
|
||||
|
||||
private fun press(keys: List<Key>) {
|
||||
rule.onNodeWithTag("wolfskin-shell").performKeyInput { keys.forEach { pressKey(it) } }
|
||||
}
|
||||
|
||||
@Test fun launchRightThenOneOkKeepsPickerClosed() = select(0, 1, moves = listOf(Key.DirectionRight))
|
||||
@Test fun launchLeftThenOneOkKeepsPickerClosed() = select(1, 0, moves = listOf(Key.DirectionLeft))
|
||||
@Test fun avatarRightThenOneOkKeepsPickerClosed() = select(0, 1, fromAvatar = true, moves = listOf(Key.DirectionRight))
|
||||
@Test fun avatarLeftThenOneOkKeepsPickerClosed() = select(1, 0, fromAvatar = true, moves = listOf(Key.DirectionLeft))
|
||||
@Test fun initialPersonNeedsOnlyOneOk() = select(0, 0)
|
||||
@Test fun gearAndBackThenOneOkKeepsPickerClosed() = select(0, 2, moves = listOf(Key.DirectionRight, Key.DirectionRight, Key.DirectionDown, Key.DirectionUp))
|
||||
@Test fun fastLeftLeftOkKeepsPickerClosed() = select(2, 0, fast = true, moves = listOf(Key.DirectionLeft, Key.DirectionLeft))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue