@@ -7,6 +7,8 @@ import com.google.common.util.concurrent.ListenableFuture
77import com.theveloper.pixelplay.data.model.SearchFilterType
88import com.theveloper.pixelplay.data.model.SearchHistoryItem
99import com.theveloper.pixelplay.data.model.SearchResultItem
10+ import com.theveloper.pixelplay.data.model.Album
11+ import com.theveloper.pixelplay.data.model.Artist
1012import com.theveloper.pixelplay.data.model.Song
1113import com.theveloper.pixelplay.data.model.SortOption
1214import com.theveloper.pixelplay.data.model.StorageFilter
@@ -389,22 +391,43 @@ class PlayerViewModelTest {
389391 assertEquals(Player .REPEAT_MODE_OFF , controllerRepeatMode)
390392 }
391393
394+ @Test
395+ fun `album navigation from player accepts synthetic negative album ids` () = runTest {
396+ playerViewModel.albumNavigationRequests.test {
397+ playerViewModel.triggerAlbumNavigationFromPlayer(- 42L )
398+ advanceUntilIdle()
399+
400+ assertEquals(- 42L , awaitItem())
401+ cancelAndConsumeRemainingEvents()
402+ }
403+ }
404+
405+ @Test
406+ fun `album navigation from player still ignores sentinel album id` () = runTest {
407+ playerViewModel.albumNavigationRequests.test {
408+ playerViewModel.triggerAlbumNavigationFromPlayer(- 1L )
409+ advanceUntilIdle()
410+
411+ expectNoEvents()
412+ }
413+ }
414+
392415 @Nested
393416 @DisplayName(" Shuffle Functionality" )
394417 inner class ShuffleFunctionalityTests {
395418
396419 private val song1 = Song (id = " 1" , title = " Song 1" , artist = " Artist A" , genre = " Rock" , albumArtUriString = " cover1.png" , artistId = 1L , albumId = 1L , contentUriString = " content://dummy/1" , duration = 180000L , bitrate = null , sampleRate = null , album = " Album" , path = " path" , mimeType = " audio/mpeg" )
397420 private val song2 = Song (id = " 2" , title = " Song 2" , artist = " Artist B" , genre = " Pop" , albumArtUriString = " cover2.png" , artistId = 2L , albumId = 2L , contentUriString = " content://dummy/2" , duration = 200000L , bitrate = null , sampleRate = null , album = " Album" , path = " path" , mimeType = " audio/mpeg" )
398421 private val song3 = Song (id = " 3" , title = " Song 3" , artist = " Artist C" , genre = " Jazz" , albumArtUriString = " cover3.png" , artistId = 3L , albumId = 3L , contentUriString = " content://dummy/3" , duration = 210000L , bitrate = null , sampleRate = null , album = " Album" , path = " path" , mimeType = " audio/mpeg" )
399-
400- @Test
401- fun `shuffleAllSongs calls prepareShuffledQueue with random songs` () = runTest {
402- // Arrange
403- val randomSongs = listOf (song2, song3, song1 )
404- coEvery { mockMusicRepository.getRandomSongs( 500 ) } returns randomSongs
405-
406- // Mock queue preparation to return a valid shuffled queue and start song
407- coEvery { mockQueueStateHolder.prepareShuffledQueueSuspending(randomSongs, any()) } returns Pair (randomSongs, song2 )
422+
423+ private fun stubShuffledPlayback (
424+ songs : List < Song >,
425+ queueName : String ,
426+ startSong : Song = songs.first( )
427+ ) {
428+ coEvery {
429+ mockQueueStateHolder.prepareShuffledQueueSuspending(songs, queueName, true )
430+ } returns Pair (songs, startSong )
408431
409432 mockkObject(MediaItemBuilder )
410433 every { MediaItemBuilder .build(any()) } returns MediaItem .Builder ()
@@ -414,14 +437,126 @@ class PlayerViewModelTest {
414437 val mockedPlaybackUri = mockk< android.net.Uri > (relaxed = true )
415438 every { mockedPlaybackUri.scheme } returns " file"
416439 every { MediaItemBuilder .playbackUri(any()) } returns mockedPlaybackUri
440+ }
441+
442+ @Test
443+ fun `shuffleAllSongs calls prepareShuffledQueue with random songs at index zero` () = runTest {
444+ val randomSongs = listOf (song2, song3, song1)
445+ coEvery { mockMusicRepository.getRandomSongs(500 ) } returns randomSongs
446+ stubShuffledPlayback(randomSongs, " All Songs (Shuffled)" , startSong = song2)
417447
418- // Act
419448 playerViewModel.shuffleAllSongs()
420449 advanceUntilIdle()
421450
422- // Assert
423451 coVerify { mockMusicRepository.getRandomSongs(500 ) }
424- coVerify { mockQueueStateHolder.prepareShuffledQueueSuspending(randomSongs, " All Songs (Shuffled)" ) }
452+ coVerify {
453+ mockQueueStateHolder.prepareShuffledQueueSuspending(
454+ randomSongs,
455+ " All Songs (Shuffled)" ,
456+ true
457+ )
458+ }
459+ }
460+
461+ @Test
462+ fun `playRandomSong calls prepareShuffledQueue with startAtZero` () = runTest {
463+ val randomSongs = listOf (song3, song1, song2)
464+ coEvery { mockMusicRepository.getRandomSongs(500 ) } returns randomSongs
465+ stubShuffledPlayback(randomSongs, " All Songs (Shuffled)" , startSong = song3)
466+
467+ playerViewModel.playRandomSong()
468+ advanceUntilIdle()
469+
470+ coVerify {
471+ mockQueueStateHolder.prepareShuffledQueueSuspending(
472+ randomSongs,
473+ " All Songs (Shuffled)" ,
474+ true
475+ )
476+ }
477+ }
478+
479+ @Test
480+ fun `shuffleFavoriteSongs calls prepareShuffledQueue with startAtZero` () = runTest {
481+ val favoriteSongs = listOf (song1, song3)
482+ coEvery { mockMusicRepository.getFavoriteSongsOnce(StorageFilter .ALL ) } returns favoriteSongs
483+ stubShuffledPlayback(favoriteSongs, " Liked Songs (Shuffled)" , startSong = song1)
484+
485+ playerViewModel.shuffleFavoriteSongs()
486+ advanceUntilIdle()
487+
488+ coVerify { mockMusicRepository.getFavoriteSongsOnce(StorageFilter .ALL ) }
489+ coVerify {
490+ mockQueueStateHolder.prepareShuffledQueueSuspending(
491+ favoriteSongs,
492+ " Liked Songs (Shuffled)" ,
493+ true
494+ )
495+ }
496+ }
497+
498+ @Test
499+ fun `shuffleRandomAlbum calls prepareShuffledQueue with startAtZero` () = runTest {
500+ val album = Album (
501+ id = 77L ,
502+ title = " Album Roulette" ,
503+ artist = " Artist A" ,
504+ year = 2024 ,
505+ dateAdded = 0L ,
506+ albumArtUriString = null ,
507+ songCount = 3
508+ )
509+ every { mockLibraryStateHolder.albums } returns MutableStateFlow (persistentListOf(album))
510+ every { mockMusicRepository.getSongsForAlbum(album.id) } returns flowOf(listOf (song1, song2, song3))
511+ stubShuffledPlayback(listOf (song1, song2, song3), album.title, startSong = song2)
512+
513+ playerViewModel.shuffleRandomAlbum()
514+ advanceUntilIdle()
515+
516+ coVerify {
517+ mockQueueStateHolder.prepareShuffledQueueSuspending(
518+ listOf (song1, song2, song3),
519+ " Album Roulette" ,
520+ true
521+ )
522+ }
523+ }
524+
525+ @Test
526+ fun `shuffleRandomArtist calls prepareShuffledQueue with startAtZero` () = runTest {
527+ val artist = Artist (id = 88L , name = " Artist Roulette" , songCount = 3 )
528+ every { mockLibraryStateHolder.artists } returns MutableStateFlow (persistentListOf(artist))
529+ every { mockMusicRepository.getSongsForArtist(artist.id) } returns flowOf(listOf (song3, song2, song1))
530+ stubShuffledPlayback(listOf (song3, song2, song1), artist.name, startSong = song3)
531+
532+ playerViewModel.shuffleRandomArtist()
533+ advanceUntilIdle()
534+
535+ coVerify {
536+ mockQueueStateHolder.prepareShuffledQueueSuspending(
537+ listOf (song3, song2, song1),
538+ " Artist Roulette" ,
539+ true
540+ )
541+ }
542+ }
543+
544+ @Test
545+ fun `triggerShuffleAllFromTile uses startAtZero when library is already loaded` () = runTest {
546+ val songs = listOf (song1, song2, song3)
547+ _allSongsFlow .value = songs.toImmutableList()
548+ stubShuffledPlayback(songs, " All Songs (Shuffled)" , startSong = song1)
549+
550+ playerViewModel.triggerShuffleAllFromTile()
551+ advanceUntilIdle()
552+
553+ coVerify {
554+ mockQueueStateHolder.prepareShuffledQueueSuspending(
555+ songs,
556+ " All Songs (Shuffled)" ,
557+ true
558+ )
559+ }
425560 }
426561 }
427562
0 commit comments