diff --git a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeListControls.kt b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeListControls.kt index e76cefa45e1..bb7a76798cc 100644 --- a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeListControls.kt +++ b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/component/TvEpisodeListControls.kt @@ -91,13 +91,19 @@ fun TvSortButton( label: @Composable (T) -> String, onSelect: (T) -> Unit, modifier: Modifier = Modifier, + onExpand: () -> Unit = {}, + leftFocusRequester: FocusRequester? = null, ) { var isExpanded by remember { mutableStateOf(false) } Box(modifier = modifier) { IconButton( - onClick = { isExpanded = true }, + onClick = { + onExpand() + isExpanded = true + }, colors = TvButtonDefaults.iconButtonColors(), + modifier = Modifier.focusProperties { leftFocusRequester?.let { left = it } }, ) { Icon( painter = painterResource(IR.drawable.ic_sort), diff --git a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsScreen.kt b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsScreen.kt index 83f6e608c2a..81ad6064b02 100644 --- a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsScreen.kt +++ b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsScreen.kt @@ -75,6 +75,15 @@ fun TvPlaylistsScreen( var isDownloadModalVisible by rememberSaveable { mutableStateOf(false) } var openedPlaylist by rememberSaveable(stateSaver = OpenedPlaylistSaver) { mutableStateOf(null) } var restoreFocusTrigger by remember { mutableIntStateOf(0) } + var filterListShownTracked by remember { mutableStateOf(false) } + + LaunchedEffect(uiState) { + val state = uiState + if (!filterListShownTracked && state is TvPlaylistsUiState.Loaded) { + viewModel.trackPlaylistsListShown(state.playlists.size) + filterListShownTracked = true + } + } val playlist = openedPlaylist Box(modifier = modifier.fillMaxSize()) { @@ -85,7 +94,10 @@ fun TvPlaylistsScreen( refreshArtworkUuids = viewModel::refreshArtworkUuids, refreshEpisodeCount = viewModel::refreshEpisodeCount, findPodcastTint = viewModel::findPodcastTint, - onCreatePlaylist = { isDownloadModalVisible = true }, + onCreatePlaylist = { + viewModel.trackCreateButtonTapped() + isDownloadModalVisible = true + }, onOpenPlaylist = { preview -> isDownloadModalVisible = false openedPlaylist = OpenedPlaylist(preview.uuid, preview.type) diff --git a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModel.kt b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModel.kt index b28375aacc0..e301fe23b2e 100644 --- a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModel.kt +++ b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModel.kt @@ -7,6 +7,9 @@ import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistManager import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistPreview import au.com.shiftyjelly.pocketcasts.repositories.playlist.SmartPlaylistPreview import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager +import com.automattic.eventhorizon.EventHorizon +import com.automattic.eventhorizon.FilterCreateButtonTappedEvent +import com.automattic.eventhorizon.FilterListShownEvent import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -20,6 +23,7 @@ import kotlinx.coroutines.flow.stateIn class TvPlaylistsViewModel @Inject constructor( private val playlistManager: PlaylistManager, private val podcastManager: PodcastManager, + private val eventHorizon: EventHorizon, ) : ViewModel() { val uiState: StateFlow = playlistManager.playlistPreviewsFlow() @@ -48,6 +52,14 @@ class TvPlaylistsViewModel @Inject constructor( return podcastManager.findPodcastByUuid(podcastUuid)?.tintColorForLightBg?.takeIf { it != 0 } } + fun trackPlaylistsListShown(filterCount: Int) { + eventHorizon.track(FilterListShownEvent(filterCount = filterCount.toLong())) + } + + fun trackCreateButtonTapped() { + eventHorizon.track(FilterCreateButtonTappedEvent) + } + private fun isDownloadPlaylist(preview: PlaylistPreview): Boolean { return preview is SmartPlaylistPreview && preview.smartRules.downloadStatus == SmartRules.DownloadStatusRule.Downloaded } diff --git a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsScreen.kt b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsScreen.kt index ead860f9ce4..9228783e545 100644 --- a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsScreen.kt +++ b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsScreen.kt @@ -51,6 +51,7 @@ import au.com.shiftyjelly.pocketcasts.component.TvModalButton import au.com.shiftyjelly.pocketcasts.component.TvModalSurface import au.com.shiftyjelly.pocketcasts.component.TvSortButton import au.com.shiftyjelly.pocketcasts.component.rememberTvEpisodeListFocus +import au.com.shiftyjelly.pocketcasts.compose.CallOnce import au.com.shiftyjelly.pocketcasts.compose.components.PlaylistArtwork import au.com.shiftyjelly.pocketcasts.compose.components.displayLabel import au.com.shiftyjelly.pocketcasts.compose.loading.LoadingView @@ -92,6 +93,8 @@ fun TvPlaylistDetailsScreen( val savedToast = stringResource(LR.string.up_next_as_playlist_saved) val noEpisodesToast = stringResource(LR.string.play_all_no_episodes_message) + CallOnce { viewModel.trackFilterShown() } + LaunchedEffect(uiState, onClose) { if (uiState is TvPlaylistDetailsUiState.NotFound) { onClose() @@ -121,6 +124,7 @@ fun TvPlaylistDetailsScreen( TvPlaylistDetailsContent( uiState = uiState, onChangeSortType = viewModel::changeSortType, + onSortTap = viewModel::trackSortByTapped, onToggleArchiveFilter = viewModel::toggleArchiveFilter, onOpenPodcast = { openedPodcastUuid = it }, onPlayAll = viewModel::playAll, @@ -138,7 +142,10 @@ fun TvPlaylistDetailsScreen( isReplaceUpNextConfirmationVisible = false viewModel.replaceUpNextAndPlay(saveUpNext = true, upNextName = upNextName) }, - onCancel = { isReplaceUpNextConfirmationVisible = false }, + onCancel = { + isReplaceUpNextConfirmationVisible = false + viewModel.trackPlayAllDismissed() + }, ) } } @@ -147,6 +154,7 @@ fun TvPlaylistDetailsScreen( private fun TvPlaylistDetailsContent( uiState: TvPlaylistDetailsUiState, onChangeSortType: (PlaylistEpisodeSortType) -> Unit, + onSortTap: () -> Unit, onToggleArchiveFilter: () -> Unit, onOpenPodcast: (String) -> Unit, onPlayAll: () -> Unit, @@ -182,6 +190,7 @@ private fun TvPlaylistDetailsContent( SortableEpisodeList( uiState = uiState, onChangeSortType = onChangeSortType, + onSortTap = onSortTap, onToggleArchiveFilter = onToggleArchiveFilter, onOpenPodcast = onOpenPodcast, playAllFocusRequester = playAllFocusRequester, @@ -198,6 +207,7 @@ private fun TvPlaylistDetailsContent( private fun SortableEpisodeList( uiState: TvPlaylistDetailsUiState.Loaded, onChangeSortType: (PlaylistEpisodeSortType) -> Unit, + onSortTap: () -> Unit, onToggleArchiveFilter: () -> Unit, onOpenPodcast: (String) -> Unit, playAllFocusRequester: FocusRequester, @@ -212,6 +222,8 @@ private fun SortableEpisodeList( listState.scrollToItem(0) } } + val isManual = uiState.playlist.type == Playlist.Type.Manual + val leftFocusRequester = playAllFocusRequester.takeIf { uiState.episodes.isNotEmpty() } Column(modifier = modifier) { Row( horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -220,16 +232,20 @@ private fun SortableEpisodeList( .align(Alignment.End) .padding(bottom = 12.dp), ) { - TvArchivedFilterButton( - isShowingArchived = uiState.isShowingArchivedOnDevice, - onToggleArchiveFilter = onToggleArchiveFilter, - leftFocusRequester = playAllFocusRequester.takeIf { uiState.episodes.isNotEmpty() }, - ) + if (isManual) { + TvArchivedFilterButton( + isShowingArchived = uiState.isShowingArchivedOnDevice, + onToggleArchiveFilter = onToggleArchiveFilter, + leftFocusRequester = leftFocusRequester, + ) + } TvSortButton( selected = sortType, options = uiState.playlist.availableSortTypes, label = { it.displayLabel() }, onSelect = onChangeSortType, + onExpand = onSortTap, + leftFocusRequester = if (isManual) null else leftFocusRequester, ) } if (uiState.episodes.isNotEmpty()) { @@ -487,6 +503,7 @@ private fun TvPlaylistDetailsPreview() { isShowingArchivedOnDevice = false, ), onChangeSortType = {}, + onSortTap = {}, onToggleArchiveFilter = {}, onOpenPodcast = {}, onPlayAll = {}, @@ -519,6 +536,7 @@ private fun TvPlaylistDetailsLoadedPreview() { isShowingArchivedOnDevice = false, ), onChangeSortType = {}, + onSortTap = {}, onToggleArchiveFilter = {}, onOpenPodcast = {}, onPlayAll = {}, diff --git a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModel.kt b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModel.kt index aaad34df44f..7d4286bd651 100644 --- a/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModel.kt +++ b/tv/src/main/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModel.kt @@ -12,6 +12,15 @@ import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllHandler import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllResponse import au.com.shiftyjelly.pocketcasts.repositories.playlist.Playlist import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistManager +import com.automattic.eventhorizon.EventHorizon +import com.automattic.eventhorizon.FilterHideArchivedTappedEvent +import com.automattic.eventhorizon.FilterPlayAllDismissedEvent +import com.automattic.eventhorizon.FilterPlayAllReplaceAndPlayTappedEvent +import com.automattic.eventhorizon.FilterPlayAllTappedEvent +import com.automattic.eventhorizon.FilterShowArchivedTappedEvent +import com.automattic.eventhorizon.FilterShownEvent +import com.automattic.eventhorizon.FilterSortByChangedEvent +import com.automattic.eventhorizon.FilterSortByTappedEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,11 +50,14 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor( @Assisted private val playlistType: Playlist.Type, private val playlistManager: PlaylistManager, private val preferences: TvPreferences, + private val eventHorizon: EventHorizon, playAllHandlerFactory: PlayAllHandler.Factory, ) : ViewModel() { private val playAllHandler = playAllHandlerFactory.create(SourceView.FILTERS) + private val filterType = playlistType.analyticsValue + private val _events = MutableSharedFlow(extraBufferCapacity = 2) val events: SharedFlow = _events.asSharedFlow() @@ -59,7 +71,9 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor( Playlist.Type.Smart -> playlistManager.smartPlaylistFlow(playlistUuid, includeArchived = true) } - private val isShowingArchivedFlow = MutableStateFlow(preferences.isPlaylistShowingArchived(playlistUuid)) + private val isShowingArchivedFlow = MutableStateFlow( + playlistType == Playlist.Type.Manual && preferences.isPlaylistShowingArchived(playlistUuid), + ) val uiState: StateFlow = combine( playlistFlow, @@ -81,14 +95,29 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor( TvPlaylistDetailsUiState.Loading, ) + fun trackSortByTapped() { + eventHorizon.track(FilterSortByTappedEvent(filterType = filterType)) + } + fun changeSortType(sortType: PlaylistEpisodeSortType) { + eventHorizon.track(FilterSortByChangedEvent(sortOrder = sortType.analyticsValue, filterType = filterType)) viewModelScope.launch { playlistManager.updateSortType(playlistUuid, sortType) } } + fun trackFilterShown() { + eventHorizon.track(FilterShownEvent(filterType = filterType)) + } + + fun trackPlayAllDismissed() { + eventHorizon.track(FilterPlayAllDismissedEvent(filterType = filterType)) + } + fun toggleArchiveFilter() { + if (playlistType != Playlist.Type.Manual) return val isShowingArchived = !isShowingArchivedFlow.value + eventHorizon.track(if (isShowingArchived) FilterShowArchivedTappedEvent else FilterHideArchivedTappedEvent) preferences.setPlaylistShowingArchived(playlistUuid, isShowingArchived) isShowingArchivedFlow.value = isShowingArchived } @@ -97,9 +126,12 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor( if (isBusy) { return } + val episodes = (uiState.value as? TvPlaylistDetailsUiState.Loaded)?.episodes.orEmpty() + if (episodes.isNotEmpty()) { + eventHorizon.track(FilterPlayAllTappedEvent(filterType = filterType)) + } playAllJob = viewModelScope.launch { try { - val episodes = (uiState.value as? TvPlaylistDetailsUiState.Loaded)?.episodes.orEmpty() when (playAllHandler.handlePlayAllEpisodes(episodes)) { PlayAllResponse.DoNothing -> _events.tryEmit(TvPlaylistDetailsEvent.OpenNowPlaying) PlayAllResponse.ShowWarning -> _events.tryEmit(TvPlaylistDetailsEvent.ShowReplaceUpNextConfirmation) @@ -117,6 +149,7 @@ class TvPlaylistDetailsViewModel @AssistedInject constructor( if (isBusy) { return } + eventHorizon.track(FilterPlayAllReplaceAndPlayTappedEvent(filterType = filterType, saveUpNext = saveUpNext)) replaceUpNextJob = viewModelScope.launch { val played = withContext(NonCancellable) { if (saveUpNext) { diff --git a/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModelTest.kt b/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModelTest.kt index 9b679b93027..f5f55f3f96f 100644 --- a/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModelTest.kt +++ b/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/TvPlaylistsViewModelTest.kt @@ -11,6 +11,9 @@ import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistPreview import au.com.shiftyjelly.pocketcasts.repositories.playlist.SmartPlaylistPreview import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule +import com.automattic.eventhorizon.EventHorizon +import com.automattic.eventhorizon.FilterCreateButtonTappedEvent +import com.automattic.eventhorizon.FilterListShownEvent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runTest @@ -20,6 +23,7 @@ import org.junit.Rule import org.junit.Test import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.verify @OptIn(ExperimentalCoroutinesApi::class) class TvPlaylistsViewModelTest { @@ -32,6 +36,21 @@ class TvPlaylistsViewModelTest { on { playlistPreviewsFlow() } doReturn playlistPreviews } private val podcastManager = mock() + private val eventHorizon = mock() + + @Test + fun `tracks the playlists list shown with the filter count`() = runTest { + createViewModel().trackPlaylistsListShown(3) + + verify(eventHorizon).track(FilterListShownEvent(filterCount = 3)) + } + + @Test + fun `tracks the create button tapped`() = runTest { + createViewModel().trackCreateButtonTapped() + + verify(eventHorizon).track(FilterCreateButtonTappedEvent) + } @Test fun `state starts as loading`() = runTest { @@ -115,6 +134,7 @@ class TvPlaylistsViewModelTest { private fun createViewModel(podcastManager: PodcastManager = this.podcastManager) = TvPlaylistsViewModel( playlistManager = playlistManager, podcastManager = podcastManager, + eventHorizon = eventHorizon, ) private fun smartPreview( diff --git a/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModelTest.kt b/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModelTest.kt index 566d7bc8d5a..56ac1bf8edd 100644 --- a/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModelTest.kt +++ b/tv/src/test/java/au/com/shiftyjelly/pocketcasts/playlists/details/TvPlaylistDetailsViewModelTest.kt @@ -4,13 +4,26 @@ import app.cash.turbine.test import au.com.shiftyjelly.pocketcasts.models.entity.ManualPlaylistEpisode import au.com.shiftyjelly.pocketcasts.models.entity.PodcastEpisode import au.com.shiftyjelly.pocketcasts.models.to.PlaylistEpisode +import au.com.shiftyjelly.pocketcasts.models.type.PlaylistEpisodeSortType +import au.com.shiftyjelly.pocketcasts.models.type.SmartRules import au.com.shiftyjelly.pocketcasts.preferences.TvPreferences import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllHandler import au.com.shiftyjelly.pocketcasts.repositories.playback.PlayAllResponse import au.com.shiftyjelly.pocketcasts.repositories.playlist.ManualPlaylist import au.com.shiftyjelly.pocketcasts.repositories.playlist.Playlist import au.com.shiftyjelly.pocketcasts.repositories.playlist.PlaylistManager +import au.com.shiftyjelly.pocketcasts.repositories.playlist.SmartPlaylist import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule +import com.automattic.eventhorizon.EventHorizon +import com.automattic.eventhorizon.FilterHideArchivedTappedEvent +import com.automattic.eventhorizon.FilterPlayAllDismissedEvent +import com.automattic.eventhorizon.FilterPlayAllReplaceAndPlayTappedEvent +import com.automattic.eventhorizon.FilterPlayAllTappedEvent +import com.automattic.eventhorizon.FilterShowArchivedTappedEvent +import com.automattic.eventhorizon.FilterShownEvent +import com.automattic.eventhorizon.FilterSortByChangedEvent +import com.automattic.eventhorizon.FilterSortByTappedEvent +import com.automattic.eventhorizon.PlaylistType import java.util.Date import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -41,6 +54,7 @@ class TvPlaylistDetailsViewModelTest { private val playlists = MutableSharedFlow(replay = 1) private val playlistManager = mock { on { manualPlaylistFlow(any(), anyOrNull(), any()) } doReturn playlists + on { smartPlaylistFlow(any(), anyOrNull(), any()) } doReturn MutableSharedFlow(replay = 1) } private val preferences = mock() @@ -48,6 +62,7 @@ class TvPlaylistDetailsViewModelTest { private val playAllHandlerFactory = mock { on { create(any()) } doReturn playAllHandler } + private val eventHorizon = mock() private val availableEpisode = episode(uuid = "episode-1", isArchived = false) private val archivedEpisode = episode(uuid = "episode-2", isArchived = true) @@ -86,6 +101,26 @@ class TvPlaylistDetailsViewModelTest { } } + @Test + fun `smart playlists ignore the stored archived preference`() = runTest { + val preferences = mock { + on { isPlaylistShowingArchived("playlist-uuid") } doReturn true + } + val smartPlaylists = MutableSharedFlow(replay = 1) + doReturn(smartPlaylists).whenever(playlistManager).smartPlaylistFlow(any(), anyOrNull(), any()) + val viewModel = createViewModel(preferences, playlistType = Playlist.Type.Smart) + + viewModel.uiState.test { + assertEquals(TvPlaylistDetailsUiState.Loading, awaitItem()) + + smartPlaylists.emit(smartPlaylist(availableEpisode, archivedEpisode)) + + val state = awaitItem() as TvPlaylistDetailsUiState.Loaded + assertEquals(listOf(availableEpisode), state.episodes) + assertEquals(false, state.isShowingArchivedOnDevice) + } + } + @Test fun `only available episodes are surfaced`() = runTest { val available = episode(uuid = "available", isArchived = false) @@ -338,11 +373,123 @@ class TvPlaylistDetailsViewModelTest { } } - private fun createViewModel(prefs: TvPreferences = preferences) = TvPlaylistDetailsViewModel( + @Test + fun `tracks the filter shown with the playlist type`() = runTest { + createViewModel().trackFilterShown() + + verify(eventHorizon).track(FilterShownEvent(filterType = PlaylistType.Manual)) + } + + @Test + fun `toggling the archive filter on a manual playlist tracks show then hide`() = runTest { + val viewModel = createViewModel(playlistType = Playlist.Type.Manual) + + viewModel.toggleArchiveFilter() + viewModel.toggleArchiveFilter() + + verify(eventHorizon).track(FilterShowArchivedTappedEvent) + verify(eventHorizon).track(FilterHideArchivedTappedEvent) + } + + @Test + fun `toggling the archive filter on a smart playlist tracks nothing`() = runTest { + val viewModel = createViewModel(playlistType = Playlist.Type.Smart) + + viewModel.toggleArchiveFilter() + + verify(eventHorizon, never()).track(FilterShowArchivedTappedEvent) + verify(eventHorizon, never()).track(FilterHideArchivedTappedEvent) + verify(preferences, never()).setPlaylistShowingArchived(any(), any()) + } + + @Test + fun `play all tracks the tapped event`() = runTest { + whenever(playAllHandler.handlePlayAllEpisodes(any())) doReturn PlayAllResponse.DoNothing + val viewModel = createViewModel() + + viewModel.uiState.test { + assertEquals(TvPlaylistDetailsUiState.Loading, awaitItem()) + playlists.emit(playlist(availableEpisode)) + awaitItem() + viewModel.playAll() + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + + verify(eventHorizon).track(FilterPlayAllTappedEvent(filterType = PlaylistType.Manual)) + } + + @Test + fun `play all does not track the tapped event when there are no episodes`() = runTest { + whenever(playAllHandler.handlePlayAllEpisodes(any())) doReturn PlayAllResponse.ShowNoEpisodesToPlay + val viewModel = createViewModel() + + viewModel.uiState.test { + assertEquals(TvPlaylistDetailsUiState.Loading, awaitItem()) + playlists.emit(playlist()) + awaitItem() + viewModel.playAll() + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + + verify(eventHorizon, never()).track(any()) + } + + @Test + fun `replacing up next tracks the replace and play event`() = runTest { + val viewModel = createViewModel() + + viewModel.replaceUpNextAndPlay(saveUpNext = true, upNextName = "Up Next") + advanceUntilIdle() + + verify(eventHorizon).track(FilterPlayAllReplaceAndPlayTappedEvent(filterType = PlaylistType.Manual, saveUpNext = true)) + } + + @Test + fun `replacing up next without saving tracks the replace and play event with save disabled`() = runTest { + val viewModel = createViewModel() + + viewModel.replaceUpNextAndPlay(saveUpNext = false, upNextName = "Up Next") + advanceUntilIdle() + + verify(eventHorizon).track(FilterPlayAllReplaceAndPlayTappedEvent(filterType = PlaylistType.Manual, saveUpNext = false)) + } + + @Test + fun `tracks the sort by tapped event`() = runTest { + createViewModel().trackSortByTapped() + + verify(eventHorizon).track(FilterSortByTappedEvent(filterType = PlaylistType.Manual)) + } + + @Test + fun `changing the sort type tracks the sort by changed event`() = runTest { + val sortType = PlaylistEpisodeSortType.NewestToOldest + + createViewModel().changeSortType(sortType) + + verify(eventHorizon).track( + FilterSortByChangedEvent(sortOrder = sortType.analyticsValue, filterType = PlaylistType.Manual), + ) + } + + @Test + fun `tracks the play all dismissed event`() = runTest { + createViewModel().trackPlayAllDismissed() + + verify(eventHorizon).track(FilterPlayAllDismissedEvent(filterType = PlaylistType.Manual)) + } + + private fun createViewModel( + prefs: TvPreferences = preferences, + playlistType: Playlist.Type = Playlist.Type.Manual, + ) = TvPlaylistDetailsViewModel( playlistUuid = "playlist-uuid", - playlistType = Playlist.Type.Manual, + playlistType = playlistType, playlistManager = playlistManager, preferences = prefs, + eventHorizon = eventHorizon, playAllHandlerFactory = playAllHandlerFactory, ) @@ -354,6 +501,15 @@ class TvPlaylistDetailsViewModelTest { metadata = Playlist.Metadata.ForPreview, ) + private fun smartPlaylist(vararg episodes: PodcastEpisode) = SmartPlaylist( + uuid = "playlist-uuid", + title = "Playlist", + smartRules = SmartRules.Default, + episodes = episodes.map(PlaylistEpisode::Available), + settings = Playlist.Settings.ForPreview, + metadata = Playlist.Metadata.ForPreview, + ) + private fun episode(uuid: String, isArchived: Boolean) = PodcastEpisode( uuid = uuid, publishedDate = Date(0),