package com.doublesymmetry.trackplayer import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import org.junit.After import org.junit.Before import org.junit.Test import org.junit.Assert.* import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class ExoPlayerIntegrationTest { private lateinit var player: ExoPlayer @Before fun setUp() { val context = RuntimeEnvironment.getApplication() player = ExoPlayer.Builder(context).build() } @After fun tearDown() { player.release() } // ---- Helpers ---- private fun buildMediaItem(id: String, title: String, url: String = "https://example.com/$id.mp3"): MediaItem { return MediaItem.Builder() .setMediaId(id) .setUri(url) .setMediaMetadata(MediaMetadata.Builder().setTitle(title).build()) .build() } private fun buildQueue(count: Int): List = (1..count).map { buildMediaItem("track-$it", "Track $it") } // ---- setMediaItems ---- @Test fun `setMediaItems loads queue`() { val items = buildQueue(3) player.setMediaItems(items) assertEquals(3, player.mediaItemCount) } @Test fun `setMediaItems replaces existing queue`() { player.setMediaItems(buildQueue(5)) assertEquals(5, player.mediaItemCount) player.setMediaItems(buildQueue(2)) assertEquals(2, player.mediaItemCount) } @Test fun `setMediaItems preserves mediaId`() { val item = buildMediaItem("my-id", "My Track") player.setMediaItems(listOf(item)) assertEquals("my-id", player.getMediaItemAt(0).mediaId) } // ---- seekToNextMediaItem / seekToPreviousMediaItem ---- @Test fun `seekToNextMediaItem advances index`() { player.setMediaItems(buildQueue(3)) assertEquals(0, player.currentMediaItemIndex) player.seekToNextMediaItem() assertEquals(1, player.currentMediaItemIndex) player.seekToNextMediaItem() assertEquals(2, player.currentMediaItemIndex) } @Test fun `seekToPreviousMediaItem goes back`() { player.setMediaItems(buildQueue(3)) player.seekTo(2, 0L) assertEquals(2, player.currentMediaItemIndex) player.seekToPreviousMediaItem() assertEquals(1, player.currentMediaItemIndex) } @Test fun `seekToNextMediaItem does not advance past last item without repeat`() { player.setMediaItems(buildQueue(2)) player.repeatMode = Player.REPEAT_MODE_OFF player.seekTo(1, 0L) // Already at last item; seekToNext should have no effect player.seekToNextMediaItem() assertEquals(1, player.currentMediaItemIndex) } // ---- repeat modes ---- @Test fun `repeat mode one can be set`() { player.repeatMode = Player.REPEAT_MODE_ONE assertEquals(Player.REPEAT_MODE_ONE, player.repeatMode) } @Test fun `repeat mode all wraps around`() { player.setMediaItems(buildQueue(3)) player.repeatMode = Player.REPEAT_MODE_ALL assertEquals(Player.REPEAT_MODE_ALL, player.repeatMode) // At last item, next should wrap to index 0 player.seekTo(2, 0L) player.seekToNextMediaItem() assertEquals(0, player.currentMediaItemIndex) } @Test fun `repeat mode off is the default`() { assertEquals(Player.REPEAT_MODE_OFF, player.repeatMode) } // ---- shuffle mode ---- @Test fun `shuffle mode can be enabled`() { player.shuffleModeEnabled = true assertTrue(player.shuffleModeEnabled) } @Test fun `shuffle mode can be disabled`() { player.shuffleModeEnabled = true player.shuffleModeEnabled = false assertFalse(player.shuffleModeEnabled) } @Test fun `shuffle mode does not change item count`() { player.setMediaItems(buildQueue(5)) player.shuffleModeEnabled = true assertEquals(5, player.mediaItemCount) } // ---- removeMediaItem ---- @Test fun `removeMediaItem removes from queue`() { player.setMediaItems(buildQueue(3)) player.removeMediaItem(1) assertEquals(2, player.mediaItemCount) assertEquals("track-1", player.getMediaItemAt(0).mediaId) assertEquals("track-3", player.getMediaItemAt(1).mediaId) } @Test fun `removeMediaItem at index 0`() { player.setMediaItems(buildQueue(3)) player.removeMediaItem(0) assertEquals(2, player.mediaItemCount) assertEquals("track-2", player.getMediaItemAt(0).mediaId) } @Test fun `removeMediaItem at last index`() { player.setMediaItems(buildQueue(3)) player.removeMediaItem(2) assertEquals(2, player.mediaItemCount) assertEquals("track-1", player.getMediaItemAt(0).mediaId) assertEquals("track-2", player.getMediaItemAt(1).mediaId) } // ---- replaceMediaItem ---- @Test fun `replaceMediaItem swaps item at index`() { player.setMediaItems(buildQueue(3)) val replacement = buildMediaItem("replacement", "Replacement Track") player.replaceMediaItem(1, replacement) assertEquals(3, player.mediaItemCount) assertEquals("replacement", player.getMediaItemAt(1).mediaId) assertEquals("track-1", player.getMediaItemAt(0).mediaId) assertEquals("track-3", player.getMediaItemAt(2).mediaId) } @Test fun `replaceMediaItem at index 0`() { player.setMediaItems(buildQueue(2)) val newItem = buildMediaItem("new-first", "New First") player.replaceMediaItem(0, newItem) assertEquals("new-first", player.getMediaItemAt(0).mediaId) assertEquals("track-2", player.getMediaItemAt(1).mediaId) } // ---- clearMediaItems ---- @Test fun `clearMediaItems empties queue`() { player.setMediaItems(buildQueue(5)) assertEquals(5, player.mediaItemCount) player.clearMediaItems() assertEquals(0, player.mediaItemCount) } @Test fun `clearMediaItems on empty queue is safe`() { player.clearMediaItems() assertEquals(0, player.mediaItemCount) } // ---- addMediaItem / addMediaItems ---- @Test fun `addMediaItem appends to queue`() { player.setMediaItems(buildQueue(2)) player.addMediaItem(buildMediaItem("track-3", "Track 3")) assertEquals(3, player.mediaItemCount) assertEquals("track-3", player.getMediaItemAt(2).mediaId) } @Test fun `addMediaItem at index inserts at correct position`() { player.setMediaItems(buildQueue(3)) val inserted = buildMediaItem("inserted", "Inserted") player.addMediaItem(1, inserted) assertEquals(4, player.mediaItemCount) assertEquals("track-1", player.getMediaItemAt(0).mediaId) assertEquals("inserted", player.getMediaItemAt(1).mediaId) assertEquals("track-2", player.getMediaItemAt(2).mediaId) } // ---- volume ---- @Test fun `volume defaults to 1`() { assertEquals(1.0f, player.volume, 0.001f) } @Test fun `volume can be set to 0`() { player.volume = 0.0f assertEquals(0.0f, player.volume, 0.001f) } @Test fun `volume can be set to half`() { player.volume = 0.5f assertEquals(0.5f, player.volume, 0.001f) } @Test fun `volume is clamped between 0 and 1`() { // ExoPlayer clamps values; setting beyond range should be bounded player.volume = 1.0f assertEquals(1.0f, player.volume, 0.001f) player.volume = 0.0f assertEquals(0.0f, player.volume, 0.001f) } // ---- seekTo ---- @Test fun `seekTo sets position in queue`() { player.setMediaItems(buildQueue(3)) player.seekTo(2, 5000L) assertEquals(2, player.currentMediaItemIndex) } // ---- moveMediaItem ---- @Test fun `moveMediaItem reorders queue`() { player.setMediaItems(buildQueue(3)) player.moveMediaItem(0, 2) // track-1 should now be at index 2 assertEquals("track-2", player.getMediaItemAt(0).mediaId) assertEquals("track-3", player.getMediaItemAt(1).mediaId) assertEquals("track-1", player.getMediaItemAt(2).mediaId) } // ---- metadata retrieval ---- @Test fun `getMediaItemAt returns correct metadata`() { val item = buildMediaItem("meta-test", "Metadata Test Track") player.setMediaItems(listOf(item)) val retrieved = player.getMediaItemAt(0) assertEquals("meta-test", retrieved.mediaId) assertEquals("Metadata Test Track", retrieved.mediaMetadata.title.toString()) } @Test fun `player initial state is idle`() { assertEquals(Player.STATE_IDLE, player.playbackState) } }