import XCTest @testable import MixBoard @MainActor final class PlayerViewModelTests: XCTestCase { // MARK: - Initial State func testInitialIsPlayingFalse() { let vm = PlayerViewModel() XCTAssertFalse(vm.isPlaying) } func testInitialCurrentTimeZero() { let vm = PlayerViewModel() XCTAssertEqual(vm.currentTime, 0) } func testInitialDurationZero() { let vm = PlayerViewModel() XCTAssertEqual(vm.duration, 0) } func testInitialCurrentTrackNil() { let vm = PlayerViewModel() XCTAssertNil(vm.currentTrack) } func testInitialShowNowPlayingFalse() { let vm = PlayerViewModel() XCTAssertFalse(vm.showNowPlaying) } func testInitialShowingWaveformTrue() { let vm = PlayerViewModel() XCTAssertTrue(vm.showingWaveform) } func testInitialWaveformSamplesEmpty() { let vm = PlayerViewModel() XCTAssertTrue(vm.waveformSamples.isEmpty) } func testInitialCurrentPlayingEntryIDNil() { let vm = PlayerViewModel() XCTAssertNil(vm.currentPlayingEntryID) } func testInitialCurrentPlaylistNil() { let vm = PlayerViewModel() XCTAssertNil(vm.currentPlaylist) } func testInitialShuffleDisabled() { let vm = PlayerViewModel() XCTAssertFalse(vm.shuffleEnabled) } func testInitialRepeatModeOff() { let vm = PlayerViewModel() XCTAssertEqual(vm.repeatMode, .off) } func testInitialLoadingWaveformFalse() { let vm = PlayerViewModel() XCTAssertFalse(vm.isLoadingWaveform) } // MARK: - Progress Calculations func testProgressZeroDuration() { let vm = PlayerViewModel() XCTAssertEqual(vm.progress, 0) } func testProgressCalculation() { let vm = PlayerViewModel() // We can't easily set these through the audio engine, // but we can verify the formula via the public property // When duration is 0, progress should be 0 XCTAssertEqual(vm.progress, 0) } // MARK: - Time Formatting func testCurrentTimeFormattedZero() { let vm = PlayerViewModel() XCTAssertEqual(vm.currentTimeFormatted, "0:00") } func testDurationFormattedZero() { let vm = PlayerViewModel() XCTAssertEqual(vm.durationFormatted, "0:00") } func testRemainingTimeFormattedZero() { let vm = PlayerViewModel() XCTAssertEqual(vm.remainingTimeFormatted, "-0:00") } // MARK: - Repeat Mode func testRepeatModeOffIcon() { XCTAssertEqual(PlayerViewModel.RepeatMode.off.icon, "repeat") } func testRepeatModeAllIcon() { XCTAssertEqual(PlayerViewModel.RepeatMode.all.icon, "repeat") } func testRepeatModeOneIcon() { XCTAssertEqual(PlayerViewModel.RepeatMode.one.icon, "repeat.1") } func testRepeatModeAllCases() { XCTAssertEqual(PlayerViewModel.RepeatMode.allCases.count, 3) } func testRepeatModeRawValues() { XCTAssertEqual(PlayerViewModel.RepeatMode.off.rawValue, "Off") XCTAssertEqual(PlayerViewModel.RepeatMode.all.rawValue, "Repeat All") XCTAssertEqual(PlayerViewModel.RepeatMode.one.rawValue, "Repeat One") } // MARK: - Volume func testDefaultVolume() { let vm = PlayerViewModel() XCTAssertEqual(vm.volume, 0.8, accuracy: 0.001) } func testSetVolume() { let vm = PlayerViewModel() vm.volume = 0.5 XCTAssertEqual(vm.volume, 0.5, accuracy: 0.001) } // MARK: - Stop func testStopClearsWaveform() { let vm = PlayerViewModel() vm.waveformSamples = [WaveformGenerator.WaveformSample(min: -0.5, max: 0.5)] XCTAssertFalse(vm.waveformSamples.isEmpty) vm.stop() XCTAssertTrue(vm.waveformSamples.isEmpty) } func testStopClearsEntryID() { let vm = PlayerViewModel() vm.currentPlayingEntryID = UUID() vm.stop() XCTAssertNil(vm.currentPlayingEntryID) } func testStopSetsNotPlaying() { let vm = PlayerViewModel() vm.stop() XCTAssertFalse(vm.isPlaying) } // MARK: - Load and Play (error path) func testLoadAndPlayNonExistentFile() { let vm = PlayerViewModel() let track = Track(title: "Missing", filePath: "nonexistent/file.mp3") // Should not crash — error is caught vm.loadAndPlay(track) XCTAssertFalse(vm.isPlaying) } // MARK: - Toggle Play/Pause Without Track func testTogglePlayPauseWithoutTrack() { let vm = PlayerViewModel() vm.togglePlayPause() // Should not crash XCTAssertFalse(vm.isPlaying) } // MARK: - Shuffle Toggle func testShuffleToggle() { let vm = PlayerViewModel() XCTAssertFalse(vm.shuffleEnabled) vm.shuffleEnabled = true XCTAssertTrue(vm.shuffleEnabled) vm.shuffleEnabled = false XCTAssertFalse(vm.shuffleEnabled) } // MARK: - Repeat Mode Cycling func testRepeatModeCycling() { let vm = PlayerViewModel() XCTAssertEqual(vm.repeatMode, .off) vm.repeatMode = .all XCTAssertEqual(vm.repeatMode, .all) vm.repeatMode = .one XCTAssertEqual(vm.repeatMode, .one) vm.repeatMode = .off XCTAssertEqual(vm.repeatMode, .off) } // MARK: - Play Next/Previous Without Playlist func testPlayNextWithoutPlaylist() { let vm = PlayerViewModel() // Should not crash vm.playNext() XCTAssertFalse(vm.isPlaying) } func testPlayPreviousWithoutPlaylist() { let vm = PlayerViewModel() vm.playPrevious() XCTAssertFalse(vm.isPlaying) } // MARK: - Seek func testSeekToProgress() { let vm = PlayerViewModel() // Without a loaded file, this should not crash vm.seekToProgress(0.5) } func testSkipForwardDefault() { let vm = PlayerViewModel() vm.skipForward() // Should not crash } func testSkipBackwardDefault() { let vm = PlayerViewModel() vm.skipBackward() // Should not crash } func testSkipForwardCustom() { let vm = PlayerViewModel() vm.skipForward(30) // Should not crash } func testSkipBackwardCustom() { let vm = PlayerViewModel() vm.skipBackward(30) // Should not crash } // MARK: - Waveform Loading (cached) func testLoadWaveformFromCache() { let vm = PlayerViewModel() let track = Track(title: "Test", filePath: "Music/test.mp3") let samples = [WaveformGenerator.WaveformSample(min: -0.3, max: 0.7)] track.waveformData = try? JSONEncoder().encode(samples) vm.loadWaveform(for: track) XCTAssertEqual(vm.waveformSamples.count, 1) XCTAssertEqual(vm.waveformSamples[0].min, -0.3, accuracy: 0.001) XCTAssertEqual(vm.waveformSamples[0].max, 0.7, accuracy: 0.001) } func testLoadWaveformCorruptedCacheTriggersGeneration() { let vm = PlayerViewModel() let track = Track(title: "Test", filePath: "Music/test.mp3") track.waveformData = Data([0xFF, 0xFE]) vm.loadWaveform(for: track) // Corrupted cache should not set samples synchronously // (generation will fail async since file doesn't exist) } }