Round 2 release candidate 3: polish lanes A-E, season reel and trailer return #82
8 changed files with 281 additions and 34 deletions
|
|
@ -53,9 +53,10 @@ class InstrumentedBasicUiTests {
|
||||||
MainContent(
|
MainContent(
|
||||||
backStack = mutableListOf(SetupDestination.ServerList),
|
backStack = mutableListOf(SetupDestination.ServerList),
|
||||||
navigationManager = navigationManager,
|
navigationManager = navigationManager,
|
||||||
appPreferences = mockk(relaxed = true),
|
userPreferences = mockk(relaxed = true),
|
||||||
backdropService = mockk(relaxed = true),
|
backdropService = mockk(relaxed = true),
|
||||||
screensaverService = screensaverService,
|
screensaverService = screensaverService,
|
||||||
|
liveSession = MutableStateFlow(null),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
playerBackend = prefs.playbackPreferences.playerBackend
|
playerBackend = prefs.playbackPreferences.playerBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.appStart(intent)
|
viewModel.appStart(intent, resuming = savedInstanceState != null)
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
|
|
@ -283,6 +283,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
userPreferences = userPreferences,
|
userPreferences = userPreferences,
|
||||||
backdropService = backdropService,
|
backdropService = backdropService,
|
||||||
screensaverService = screensaverService,
|
screensaverService = screensaverService,
|
||||||
|
liveSession = viewModel.serverRepository.current,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +319,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onRestart() {
|
override fun onRestart() {
|
||||||
super.onRestart()
|
super.onRestart()
|
||||||
Timber.d("onRestart")
|
Timber.d("onRestart")
|
||||||
viewModel.appStart(null)
|
viewModel.appStart(null, resuming = true)
|
||||||
if (!playExternalViewModel.launched.value) {
|
if (!playExternalViewModel.launched.value) {
|
||||||
// If restarting during playback that is not external, go back a page
|
// If restarting during playback that is not external, go back a page
|
||||||
val lastDest = navigationManager.backStack.lastOrNull()
|
val lastDest = navigationManager.backStack.lastOrNull()
|
||||||
|
|
@ -440,7 +441,7 @@ class MainActivityViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
fun appStart(intent: Intent?) {
|
fun appStart(intent: Intent?, resuming: Boolean = false) {
|
||||||
viewModelScope.launchDefault {
|
viewModelScope.launchDefault {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
try {
|
try {
|
||||||
|
|
@ -464,7 +465,7 @@ class MainActivityViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleAppStart()
|
handleAppStart(resuming)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Error during appStart")
|
Timber.e(ex, "Error during appStart")
|
||||||
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
setupNavigationManager.navigateTo(SetupDestination.ServerList)
|
||||||
|
|
@ -567,11 +568,16 @@ class MainActivityViewModel
|
||||||
* Handle starting the app including auto sign in and navigating to the right starting page
|
* Handle starting the app including auto sign in and navigating to the right starting page
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal suspend fun handleAppStart() {
|
internal suspend fun handleAppStart(resuming: Boolean = false) {
|
||||||
val signInAutomatically = preferences.data.first().signInAutomatically
|
val signInAutomatically = preferences.data.first().signInAutomatically
|
||||||
val currentUser = serverRepository.current.value
|
val currentUser = serverRepository.current.value
|
||||||
if (signInAutomatically && currentUser != null && !currentUser.user.isProtected) {
|
// A picker may be an explicit switch or expired login, even while a user remains cached.
|
||||||
// Hot reload
|
val shown = if (resuming && !signInAutomatically) {
|
||||||
|
(setupNavigationManager.backStack.lastOrNull() as? SetupDestination.AppContent)?.current
|
||||||
|
} else null
|
||||||
|
val canContinue = currentUser != null && shown != null &&
|
||||||
|
currentUser.server.id == shown.server.id && currentUser.user.id == shown.user.id
|
||||||
|
if ((signInAutomatically || canContinue) && currentUser != null && !currentUser.user.isProtected) {
|
||||||
Timber.v("Hot reload restoring state")
|
Timber.v("Hot reload restoring state")
|
||||||
setupNavigationManager.navigateTo(
|
setupNavigationManager.navigateTo(
|
||||||
SetupDestination.AppContent(currentUser),
|
SetupDestination.AppContent(currentUser),
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,13 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.NavEntry
|
import androidx.navigation3.runtime.NavEntry
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
|
|
@ -29,6 +25,7 @@ import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||||
|
|
@ -40,7 +37,9 @@ import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
|
||||||
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
|
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
|
||||||
import com.github.damontecres.wholphin.ui.util.InterfaceCustomization
|
import com.github.damontecres.wholphin.ui.util.InterfaceCustomization
|
||||||
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
|
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/** Setup entries carry display snapshots; lifecycle privacy checks use the live repository identity. */
|
||||||
@Composable
|
@Composable
|
||||||
fun MainContent(
|
fun MainContent(
|
||||||
backStack: MutableList<SetupDestination>,
|
backStack: MutableList<SetupDestination>,
|
||||||
|
|
@ -48,6 +47,7 @@ fun MainContent(
|
||||||
userPreferences: UserPreferences,
|
userPreferences: UserPreferences,
|
||||||
backdropService: BackdropService,
|
backdropService: BackdropService,
|
||||||
screensaverService: ScreensaverService,
|
screensaverService: ScreensaverService,
|
||||||
|
liveSession: StateFlow<CurrentUser?>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
setupInterceptor: @Composable (SetupDestination, Modifier) -> Boolean = { key, contentModifier -> WolfskinSetupContent(key, contentModifier) },
|
setupInterceptor: @Composable (SetupDestination, Modifier) -> Boolean = { key, contentModifier -> WolfskinSetupContent(key, contentModifier) },
|
||||||
) {
|
) {
|
||||||
|
|
@ -104,16 +104,7 @@ fun MainContent(
|
||||||
backdropService.clearBackdrop()
|
backdropService.clearBackdrop()
|
||||||
}
|
}
|
||||||
val current = key.current
|
val current = key.current
|
||||||
var showContent by remember {
|
SignedInContent(current, liveSession, preferences.appPreferences.signInAutomatically) {
|
||||||
mutableStateOf(true)
|
|
||||||
}
|
|
||||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
|
||||||
if (!userPreferences.appPreferences.signInAutomatically) {
|
|
||||||
showContent = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showContent) {
|
|
||||||
WolfskinApp(
|
WolfskinApp(
|
||||||
user = current.user,
|
user = current.user,
|
||||||
server = current.server,
|
server = current.server,
|
||||||
|
|
@ -121,16 +112,6 @@ fun MainContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.size(200.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = MaterialTheme.colorScheme.border,
|
|
||||||
modifier = Modifier.align(Alignment.Center),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/** Keep an unprotected live session mounted across external-app hand-offs; protected people re-enter setup. */
|
||||||
|
@Composable
|
||||||
|
internal fun SignedInContent(
|
||||||
|
current: CurrentUser,
|
||||||
|
liveSession: StateFlow<CurrentUser?>,
|
||||||
|
signInAutomatically: Boolean,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
var showContent by remember(current.server.id, current.user.id) { mutableStateOf(true) }
|
||||||
|
var hasStopped by remember(current.server.id, current.user.id) { mutableStateOf(false) }
|
||||||
|
fun hideInvalidSession() {
|
||||||
|
if (!signInAutomatically) {
|
||||||
|
val live = liveSession.value
|
||||||
|
val canContinue = live != null && live.server.id == current.server.id &&
|
||||||
|
live.user.id == current.user.id && !live.user.isProtected
|
||||||
|
if (!canContinue) showContent = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
||||||
|
hasStopped = true
|
||||||
|
hideInvalidSession()
|
||||||
|
}
|
||||||
|
LifecycleEventEffect(Lifecycle.Event.ON_START) {
|
||||||
|
// A newly unlocked entry may paint; a returning entry must recheck protection changed while away.
|
||||||
|
if (hasStopped) {
|
||||||
|
hideInvalidSession()
|
||||||
|
hasStopped = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showContent) {
|
||||||
|
content()
|
||||||
|
} else {
|
||||||
|
Box(Modifier.size(200.dp), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.border, modifier = Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
|
import com.github.damontecres.wholphin.data.RestoredSession
|
||||||
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
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.preferences.update
|
||||||
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
|
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||||
|
import com.github.damontecres.wholphin.util.configure
|
||||||
|
import com.github.damontecres.wholphin.util.reset
|
||||||
|
import io.mockk.*
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** Warm continuation retains only an already active, unprotected identity; cold and locked entries stay gated. */
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class MainActivityResumeTest {
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
private val repository = mockk<ServerRepository>()
|
||||||
|
private val setup = mockk<SetupNavigationManager>(relaxed = true)
|
||||||
|
private val navigation = NavigationManager()
|
||||||
|
private val preferences = mockk<DataStore<AppPreferences>>()
|
||||||
|
private val server = JellyfinServer(UUID.randomUUID(), "Household", "https://jellyfin.test", null)
|
||||||
|
private val current = CurrentUser(server,
|
||||||
|
JellyfinUser(id = UUID.randomUUID(), serverId = server.id, name = "Justin", accessToken = "test-token"))
|
||||||
|
private val automatic = MutableStateFlow(AppPreferences.getDefaultInstance().update { signInAutomatically = false })
|
||||||
|
private val model = MainActivityViewModel(mockk(), preferences, repository, setup, navigation,
|
||||||
|
mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true), mockk(relaxed = true))
|
||||||
|
|
||||||
|
@Before fun configure() {
|
||||||
|
WholphinDispatchers.configure(dispatcher)
|
||||||
|
every { preferences.data } returns automatic
|
||||||
|
every { repository.current } returns MutableStateFlow(current)
|
||||||
|
every { setup.backStack } returns mutableListOf(SetupDestination.AppContent(current))
|
||||||
|
coEvery { repository.getMostRecentServer() } returns RestoredSession.ServerOnly(current.server)
|
||||||
|
coEvery { repository.restoreSavedSession() } returns RestoredSession.ServerOnly(current.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After fun reset() { WholphinDispatchers.reset() }
|
||||||
|
|
||||||
|
@Test fun warmReturnKeepsTheSignedInPersonAndTitleWithAutomaticSignInOff() = runTest(dispatcher) {
|
||||||
|
val title = Destination.Wolfskin("title/${UUID.randomUUID()}")
|
||||||
|
navigation.backStack.add(title)
|
||||||
|
model.handleAppStart(resuming = true)
|
||||||
|
verify { setup.navigateTo(SetupDestination.AppContent(current)) }
|
||||||
|
assertEquals(listOf(Destination.Home(), title), navigation.backStack.toList())
|
||||||
|
coVerify(exactly = 0) { repository.restoreSavedSession() }
|
||||||
|
coVerify(exactly = 0) { repository.getMostRecentServer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun warmReturnCannotUnlockPinOrRequireLoginProfiles() = runTest(dispatcher) {
|
||||||
|
for (enabled in listOf(false, true)) {
|
||||||
|
for (user in listOf(current.user.copy(pin = "1234"), current.user.copy(requireLogin = true))) {
|
||||||
|
automatic.value = automatic.value.update { signInAutomatically = enabled }
|
||||||
|
val protected = current.copy(user = user)
|
||||||
|
every { repository.current } returns MutableStateFlow(protected)
|
||||||
|
every { setup.backStack } returns mutableListOf(SetupDestination.AppContent(protected))
|
||||||
|
model.handleAppStart(resuming = true)
|
||||||
|
verify { setup.navigateTo(SetupDestination.UserList(current.server)) }
|
||||||
|
verify(exactly = 0) { setup.navigateTo(SetupDestination.AppContent(protected)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun recreatedActivityWithoutALiveIdentityStillUsesTheColdStartPicker() = runTest(dispatcher) {
|
||||||
|
every { repository.current } returns MutableStateFlow<CurrentUser?>(null)
|
||||||
|
model.handleAppStart(resuming = true)
|
||||||
|
verify { setup.navigateTo(SetupDestination.UserList(current.server)) }
|
||||||
|
coVerify(exactly = 0) { repository.restoreSavedSession() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun warmReturnDoesNotDismissAnExplicitPickerOrAnotherPersonsEntry() = runTest(dispatcher) {
|
||||||
|
val other = current.copy(user = current.user.copy(id = UUID.randomUUID()))
|
||||||
|
for (shown in listOf(SetupDestination.UserList(current.server), SetupDestination.AppContent(other))) {
|
||||||
|
every { setup.backStack } returns mutableListOf(shown)
|
||||||
|
model.handleAppStart(resuming = true)
|
||||||
|
verify { setup.navigateTo(SetupDestination.UserList(current.server)) }
|
||||||
|
verify(exactly = 0) { setup.navigateTo(SetupDestination.AppContent(current)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.*
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.justinvisser.wolfskin.design.*
|
||||||
|
import org.justinvisser.wolfskin.tv.TitleFixture
|
||||||
|
import org.justinvisser.wolfskin.tv.TitleTrailer
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.Shadows.shadowOf
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.GraphicsMode
|
||||||
|
|
||||||
|
/** The signed-in title survives a stopped activity without removing protected-person gates. */
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
|
||||||
|
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||||
|
class SignedInContentTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
private val server = JellyfinServer(UUID.randomUUID(), "Household", "https://jellyfin.test", null)
|
||||||
|
private val user = JellyfinUser(id = UUID.randomUUID(), serverId = server.id, name = "Justin", accessToken = "test-token")
|
||||||
|
private val liveSession = MutableStateFlow<CurrentUser?>(CurrentUser(server, user))
|
||||||
|
|
||||||
|
private fun mount(person: JellyfinUser = user) {
|
||||||
|
liveSession.value = CurrentUser(server, person)
|
||||||
|
val fixture = TitleFixture(rule.activity.assets.open("wolfskin-titles.json").bufferedReader().use { JSONObject(it.readText()) })
|
||||||
|
val data = fixture.title("Locke", "justin").copy(trailerUrl = "https://www.youtube.com/watch?v=ab_-defghij")
|
||||||
|
val shell = ShellState(fixture.people, "justin").apply { pickerVisible = false; pickerLaunch = false; enterDestination("title/${data.item.id}") }
|
||||||
|
val title = TitleState(data, shell.person) { action ->
|
||||||
|
if (action is TitleAction.Trailer) assertTrue(TitleTrailer(rule.activity).open(action.url))
|
||||||
|
}
|
||||||
|
rule.setContent {
|
||||||
|
WolfskinTheme {
|
||||||
|
SignedInContent(CurrentUser(server, person), liveSession, signInAutomatically = false) {
|
||||||
|
TitleSurface(title, shell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule.onNodeWithTag("title-heading").assertTextEquals("Locke")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopAndReturn() {
|
||||||
|
rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
|
||||||
|
rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
rule.waitForIdle()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun closingTrailerKeepsTheSameTitleMountedWithAutomaticSignInOff() {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube://ab_-defghij"))
|
||||||
|
val handler = ResolveInfo().apply {
|
||||||
|
activityInfo = ActivityInfo().apply { packageName = "test.youtube"; name = "test.youtube.Player"; exported = true }
|
||||||
|
}
|
||||||
|
shadowOf(rule.activity.packageManager).addResolveInfoForIntent(intent, handler)
|
||||||
|
mount()
|
||||||
|
rule.onNodeWithTag("title-option-trailer").performClick()
|
||||||
|
assertEquals("test.youtube", shadowOf(rule.activity).nextStartedActivity.`package`)
|
||||||
|
stopAndReturn()
|
||||||
|
rule.onNodeWithTag("title-heading").assertIsDisplayed().assertTextEquals("Locke")
|
||||||
|
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "trailer-return").apply { mkdirs() }
|
||||||
|
rule.onRoot().captureRoboImage(File(directory, "returned-title.png"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun stoppedPinProtectedTitleRemainsHiddenUntilThePickerUnlocksIt() {
|
||||||
|
mount(user.copy(pin = "1234"))
|
||||||
|
stopAndReturn()
|
||||||
|
rule.onNodeWithTag("title-heading").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun stoppedRequireLoginTitleRemainsHiddenUntilAuthentication() {
|
||||||
|
mount(user.copy(requireLogin = true))
|
||||||
|
stopAndReturn()
|
||||||
|
rule.onNodeWithTag("title-heading").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun protectionAddedAfterOpeningTheTitleIsReadAtStopNotFromTheOldEntry() {
|
||||||
|
mount()
|
||||||
|
liveSession.value = CurrentUser(server, user.copy(pin = "1234"))
|
||||||
|
stopAndReturn()
|
||||||
|
rule.onNodeWithTag("title-heading").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun profileProtectedWhileAwayDoesNotReappearOnReturn() {
|
||||||
|
mount()
|
||||||
|
rule.activityRule.scenario.moveToState(Lifecycle.State.CREATED)
|
||||||
|
liveSession.value = CurrentUser(server, user.copy(pin = "1234"))
|
||||||
|
rule.activityRule.scenario.moveToState(Lifecycle.State.RESUMED)
|
||||||
|
rule.waitForIdle()
|
||||||
|
rule.onNodeWithTag("title-heading").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -99,7 +99,7 @@ class WolfskinSetupNavigationTest {
|
||||||
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
|
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
MainContent(stack, navigation, UserPreferences(AppPreferences.getDefaultInstance(), null),
|
MainContent(stack, navigation, UserPreferences(AppPreferences.getDefaultInstance(), null),
|
||||||
mockk(relaxed = true), screensaver, Modifier,
|
mockk(relaxed = true), screensaver, liveSession = repository.current, modifier = Modifier,
|
||||||
setupInterceptor = { destination, modifier -> WolfskinSetupContent(destination, modifier, model) })
|
setupInterceptor = { destination, modifier -> WolfskinSetupContent(destination, modifier, model) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ class WolfskinSetupRollbackTest {
|
||||||
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
|
every { screensaver.state } returns MutableStateFlow(ScreensaverState(false, false, false, false))
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
MainContent(stack, NavigationManager(), UserPreferences(AppPreferences.getDefaultInstance(), null),
|
MainContent(stack, NavigationManager(), UserPreferences(AppPreferences.getDefaultInstance(), null),
|
||||||
mockk(relaxed = true), screensaver)
|
mockk(relaxed = true), screensaver, liveSession = MutableStateFlow(null))
|
||||||
}
|
}
|
||||||
fun awaitHeading(resource: Int) {
|
fun awaitHeading(resource: Int) {
|
||||||
rule.waitUntil(5_000) {
|
rule.waitUntil(5_000) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue