Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ import io.kotest.matchers.shouldBe
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain

@OptIn(ExperimentalCoroutinesApi::class)
class MiniPlayerViewModelTest :
BehaviorSpec({

val testDispatcher = StandardTestDispatcher()
coroutineTestScope = true

beforeSpec { Dispatchers.setMain(testDispatcher) }
afterSpec { Dispatchers.resetMain() }
val testScheduler = TestCoroutineScheduler()
val testDispatcher = StandardTestDispatcher(testScheduler)

afterTest { Dispatchers.resetMain() }

fun makeViewModel(binder: FakeZenPlayerBinder = FakeZenPlayerBinder()): MiniPlayerViewModel {
val connection = FakeZenPlayerServiceConnection(binder)
Expand All @@ -39,128 +41,122 @@ class MiniPlayerViewModelTest :
)

Given("playQueue가 변경될 때") {
Dispatchers.setMain(testDispatcher)

When("현재 트랙이 있으면") {
Then("uiState.currentTrackTitle이 해당 트랙 제목으로 업데이트된다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

viewModel.uiState.test {
awaitItem() // 초기값 (currentTrackTitle=null)
Then("uiState.currentTrackTitle이 해당 트랙 제목으로 업데이트된다") {
viewModel.uiState.test {
awaitItem() // 초기값 소비

binder.playQueue.value = PlayQueue(tracks = listOf(fakeTrack))
testDispatcher.scheduler.advanceUntilIdle()
binder.playQueue.value = PlayQueue(tracks = listOf(fakeTrack))
testScheduler.advanceUntilIdle()

awaitItem().currentTrackTitle shouldBe "Test Track"
cancelAndIgnoreRemainingEvents()
}
awaitItem().currentTrackTitle shouldBe "Test Track"
cancelAndIgnoreRemainingEvents()
}
}
}

When("albumArt가 있는 트랙이 있으면") {
val trackWithArt = fakeTrack.copy(albumArtUri = "content://art/1")
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

Then("uiState.currentTrackArtist, albumArtUri, trackId가 함께 업데이트된다") {
runTest(testDispatcher) {
val trackWithArt = fakeTrack.copy(albumArtUri = "content://art/1")
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

viewModel.uiState.test {
awaitItem() // 초기값

binder.playQueue.value = PlayQueue(tracks = listOf(trackWithArt))
testDispatcher.scheduler.advanceUntilIdle()

val state = awaitItem()
state.currentTrackArtist shouldBe "Artist"
state.albumArtUri shouldBe "content://art/1"
state.trackId shouldBe 1L
cancelAndIgnoreRemainingEvents()
viewModel.uiState.test {
awaitItem() // 초기값 소비

binder.playQueue.value = PlayQueue(tracks = listOf(trackWithArt))
testScheduler.advanceUntilIdle()

awaitItem().also {
it.currentTrackArtist shouldBe "Artist"
it.albumArtUri shouldBe "content://art/1"
it.trackId shouldBe 1L
}
cancelAndIgnoreRemainingEvents()
}
}
}

When("재생 큐가 비어있으면") {
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

Then("uiState.currentTrackTitle이 null이다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

viewModel.uiState.test {
val state = awaitItem() // 초기값
state.currentTrackTitle shouldBe null
cancelAndIgnoreRemainingEvents()
}
viewModel.uiState.test {
awaitItem().currentTrackTitle shouldBe null
cancelAndIgnoreRemainingEvents()
}
}
}
}

Given("PLAYING 상태일 때") {
When("togglePlayPause()를 호출하면") {
Then("pauseAudio()가 호출되어 PAUSED 상태가 된다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.PLAYING
}
val viewModel = makeViewModel(binder)
Dispatchers.setMain(testDispatcher)
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.PLAYING
}
val viewModel = makeViewModel(binder)

viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testDispatcher.scheduler.advanceUntilIdle()
When("togglePlayPause()를 호출하면") {
viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testScheduler.advanceUntilIdle()

binder.playbackState.value shouldBe PlaybackState.PAUSED
}
Then("pauseAudio()가 호출되어 PAUSED 상태가 된다") {
binder.playbackState.value shouldBe PlaybackState.PAUSED
}
}
}

Given("PAUSED 상태일 때") {
When("togglePlayPause()를 호출하면") {
Then("resumeAudio()가 호출되어 PLAYING 상태가 된다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.PAUSED
}
val viewModel = makeViewModel(binder)
Dispatchers.setMain(testDispatcher)
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.PAUSED
}
val viewModel = makeViewModel(binder)

viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testDispatcher.scheduler.advanceUntilIdle()
When("togglePlayPause()를 호출하면") {
viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testScheduler.advanceUntilIdle()

binder.playbackState.value shouldBe PlaybackState.PLAYING
}
Then("resumeAudio()가 호출되어 PLAYING 상태가 된다") {
binder.playbackState.value shouldBe PlaybackState.PLAYING
}
}
}

Given("ERROR 상태일 때") {
When("togglePlayPause()를 호출하면") {
Then("retryCurrentTrack()이 호출된다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.ERROR
}
val viewModel = makeViewModel(binder)
Dispatchers.setMain(testDispatcher)
val binder = FakeZenPlayerBinder().apply {
playbackState.value = PlaybackState.ERROR
}
val viewModel = makeViewModel(binder)

viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testDispatcher.scheduler.advanceUntilIdle()
When("togglePlayPause()를 호출하면") {
viewModel.onAction(MiniPlayerAction.TogglePlayPause)
testScheduler.advanceUntilIdle()

binder.retryCurrentTrackCalled shouldBe true
}
Then("retryCurrentTrack()이 호출된다") {
binder.retryCurrentTrackCalled shouldBe true
}
}
}

Given("skipNext()를 호출할 때") {
When("binder가 연결되어 있으면") {
Then("binder.playNext()가 호출된다") {
runTest(testDispatcher) {
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)
Dispatchers.setMain(testDispatcher)
val binder = FakeZenPlayerBinder()
val viewModel = makeViewModel(binder)

viewModel.onAction(MiniPlayerAction.SkipNext)
testDispatcher.scheduler.advanceUntilIdle()
When("binder가 연결되어 있으면") {
viewModel.onAction(MiniPlayerAction.SkipNext)
testScheduler.advanceUntilIdle()

binder.playNextCalled shouldBe true
}
Then("binder.playNext()가 호출된다") {
binder.playNextCalled shouldBe true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.happyseal.zenplayer.features.playlist.data.FakePlaylistRepository
import com.happyseal.zenplayer.features.playlist.ui.PlaylistListUiState
import com.happyseal.zenplayer.zen.FakeZenPlayerBinder
import com.happyseal.zenplayer.zen.FakeZenPlayerServiceConnection
import io.kotest.assertions.fail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.collections.shouldHaveSize
Expand Down Expand Up @@ -164,7 +165,13 @@ class ZenMusicSelectViewModelTest : BehaviorSpec({
// Retry 이후 Loading → Success 등 여러 번 상태가 갱신될 수 있으므로
// 기대하는 최종 상태(트랙 1개)가 나올 때까지 대기한다.
var finalState = awaitItem()
var retryCount = 0
val maxRetries = 10
while (finalState.musicListUiState.tracks.size != 1) {
retryCount++
if (retryCount >= maxRetries) {
fail("최대 재시도 횟수($maxRetries)를 초과했습니다. 현재 트랙 수: ${finalState.musicListUiState.tracks.size}")
}
finalState = awaitItem()
}
finalState.musicListUiState.tracks shouldHaveSize 1
Expand Down
Loading