PlayerViewModelTests.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import XCTest
  2. @testable import MixBoard
  3. @MainActor
  4. final class PlayerViewModelTests: XCTestCase {
  5. // MARK: - Initial State
  6. func testInitialIsPlayingFalse() {
  7. let vm = PlayerViewModel()
  8. XCTAssertFalse(vm.isPlaying)
  9. }
  10. func testInitialCurrentTimeZero() {
  11. let vm = PlayerViewModel()
  12. XCTAssertEqual(vm.currentTime, 0)
  13. }
  14. func testInitialDurationZero() {
  15. let vm = PlayerViewModel()
  16. XCTAssertEqual(vm.duration, 0)
  17. }
  18. func testInitialCurrentTrackNil() {
  19. let vm = PlayerViewModel()
  20. XCTAssertNil(vm.currentTrack)
  21. }
  22. func testInitialShowNowPlayingFalse() {
  23. let vm = PlayerViewModel()
  24. XCTAssertFalse(vm.showNowPlaying)
  25. }
  26. func testInitialShowingWaveformTrue() {
  27. let vm = PlayerViewModel()
  28. XCTAssertTrue(vm.showingWaveform)
  29. }
  30. func testInitialWaveformSamplesEmpty() {
  31. let vm = PlayerViewModel()
  32. XCTAssertTrue(vm.waveformSamples.isEmpty)
  33. }
  34. func testInitialCurrentPlayingEntryIDNil() {
  35. let vm = PlayerViewModel()
  36. XCTAssertNil(vm.currentPlayingEntryID)
  37. }
  38. func testInitialCurrentPlaylistNil() {
  39. let vm = PlayerViewModel()
  40. XCTAssertNil(vm.currentPlaylist)
  41. }
  42. func testInitialShuffleDisabled() {
  43. let vm = PlayerViewModel()
  44. XCTAssertFalse(vm.shuffleEnabled)
  45. }
  46. func testInitialRepeatModeOff() {
  47. let vm = PlayerViewModel()
  48. XCTAssertEqual(vm.repeatMode, .off)
  49. }
  50. func testInitialLoadingWaveformFalse() {
  51. let vm = PlayerViewModel()
  52. XCTAssertFalse(vm.isLoadingWaveform)
  53. }
  54. // MARK: - Progress Calculations
  55. func testProgressZeroDuration() {
  56. let vm = PlayerViewModel()
  57. XCTAssertEqual(vm.progress, 0)
  58. }
  59. func testProgressCalculation() {
  60. let vm = PlayerViewModel()
  61. // We can't easily set these through the audio engine,
  62. // but we can verify the formula via the public property
  63. // When duration is 0, progress should be 0
  64. XCTAssertEqual(vm.progress, 0)
  65. }
  66. // MARK: - Time Formatting
  67. func testCurrentTimeFormattedZero() {
  68. let vm = PlayerViewModel()
  69. XCTAssertEqual(vm.currentTimeFormatted, "0:00")
  70. }
  71. func testDurationFormattedZero() {
  72. let vm = PlayerViewModel()
  73. XCTAssertEqual(vm.durationFormatted, "0:00")
  74. }
  75. func testRemainingTimeFormattedZero() {
  76. let vm = PlayerViewModel()
  77. XCTAssertEqual(vm.remainingTimeFormatted, "-0:00")
  78. }
  79. // MARK: - Repeat Mode
  80. func testRepeatModeOffIcon() {
  81. XCTAssertEqual(PlayerViewModel.RepeatMode.off.icon, "repeat")
  82. }
  83. func testRepeatModeAllIcon() {
  84. XCTAssertEqual(PlayerViewModel.RepeatMode.all.icon, "repeat")
  85. }
  86. func testRepeatModeOneIcon() {
  87. XCTAssertEqual(PlayerViewModel.RepeatMode.one.icon, "repeat.1")
  88. }
  89. func testRepeatModeAllCases() {
  90. XCTAssertEqual(PlayerViewModel.RepeatMode.allCases.count, 3)
  91. }
  92. func testRepeatModeRawValues() {
  93. XCTAssertEqual(PlayerViewModel.RepeatMode.off.rawValue, "Off")
  94. XCTAssertEqual(PlayerViewModel.RepeatMode.all.rawValue, "Repeat All")
  95. XCTAssertEqual(PlayerViewModel.RepeatMode.one.rawValue, "Repeat One")
  96. }
  97. // MARK: - Volume
  98. func testDefaultVolume() {
  99. let vm = PlayerViewModel()
  100. XCTAssertEqual(vm.volume, 0.8, accuracy: 0.001)
  101. }
  102. func testSetVolume() {
  103. let vm = PlayerViewModel()
  104. vm.volume = 0.5
  105. XCTAssertEqual(vm.volume, 0.5, accuracy: 0.001)
  106. }
  107. // MARK: - Stop
  108. func testStopClearsWaveform() {
  109. let vm = PlayerViewModel()
  110. vm.waveformSamples = [WaveformGenerator.WaveformSample(min: -0.5, max: 0.5)]
  111. XCTAssertFalse(vm.waveformSamples.isEmpty)
  112. vm.stop()
  113. XCTAssertTrue(vm.waveformSamples.isEmpty)
  114. }
  115. func testStopClearsEntryID() {
  116. let vm = PlayerViewModel()
  117. vm.currentPlayingEntryID = UUID()
  118. vm.stop()
  119. XCTAssertNil(vm.currentPlayingEntryID)
  120. }
  121. func testStopSetsNotPlaying() {
  122. let vm = PlayerViewModel()
  123. vm.stop()
  124. XCTAssertFalse(vm.isPlaying)
  125. }
  126. // MARK: - Load and Play (error path)
  127. func testLoadAndPlayNonExistentFile() {
  128. let vm = PlayerViewModel()
  129. let track = Track(title: "Missing", filePath: "nonexistent/file.mp3")
  130. // Should not crash — error is caught
  131. vm.loadAndPlay(track)
  132. XCTAssertFalse(vm.isPlaying)
  133. }
  134. // MARK: - Toggle Play/Pause Without Track
  135. func testTogglePlayPauseWithoutTrack() {
  136. let vm = PlayerViewModel()
  137. vm.togglePlayPause()
  138. // Should not crash
  139. XCTAssertFalse(vm.isPlaying)
  140. }
  141. // MARK: - Shuffle Toggle
  142. func testShuffleToggle() {
  143. let vm = PlayerViewModel()
  144. XCTAssertFalse(vm.shuffleEnabled)
  145. vm.shuffleEnabled = true
  146. XCTAssertTrue(vm.shuffleEnabled)
  147. vm.shuffleEnabled = false
  148. XCTAssertFalse(vm.shuffleEnabled)
  149. }
  150. // MARK: - Repeat Mode Cycling
  151. func testRepeatModeCycling() {
  152. let vm = PlayerViewModel()
  153. XCTAssertEqual(vm.repeatMode, .off)
  154. vm.repeatMode = .all
  155. XCTAssertEqual(vm.repeatMode, .all)
  156. vm.repeatMode = .one
  157. XCTAssertEqual(vm.repeatMode, .one)
  158. vm.repeatMode = .off
  159. XCTAssertEqual(vm.repeatMode, .off)
  160. }
  161. // MARK: - Play Next/Previous Without Playlist
  162. func testPlayNextWithoutPlaylist() {
  163. let vm = PlayerViewModel()
  164. // Should not crash
  165. vm.playNext()
  166. XCTAssertFalse(vm.isPlaying)
  167. }
  168. func testPlayPreviousWithoutPlaylist() {
  169. let vm = PlayerViewModel()
  170. vm.playPrevious()
  171. XCTAssertFalse(vm.isPlaying)
  172. }
  173. // MARK: - Seek
  174. func testSeekToProgress() {
  175. let vm = PlayerViewModel()
  176. // Without a loaded file, this should not crash
  177. vm.seekToProgress(0.5)
  178. }
  179. func testSkipForwardDefault() {
  180. let vm = PlayerViewModel()
  181. vm.skipForward()
  182. // Should not crash
  183. }
  184. func testSkipBackwardDefault() {
  185. let vm = PlayerViewModel()
  186. vm.skipBackward()
  187. // Should not crash
  188. }
  189. func testSkipForwardCustom() {
  190. let vm = PlayerViewModel()
  191. vm.skipForward(30)
  192. // Should not crash
  193. }
  194. func testSkipBackwardCustom() {
  195. let vm = PlayerViewModel()
  196. vm.skipBackward(30)
  197. // Should not crash
  198. }
  199. // MARK: - Waveform Loading (cached)
  200. func testLoadWaveformFromCache() {
  201. let vm = PlayerViewModel()
  202. let track = Track(title: "Test", filePath: "Music/test.mp3")
  203. let samples = [WaveformGenerator.WaveformSample(min: -0.3, max: 0.7)]
  204. track.waveformData = try? JSONEncoder().encode(samples)
  205. vm.loadWaveform(for: track)
  206. XCTAssertEqual(vm.waveformSamples.count, 1)
  207. XCTAssertEqual(vm.waveformSamples[0].min, -0.3, accuracy: 0.001)
  208. XCTAssertEqual(vm.waveformSamples[0].max, 0.7, accuracy: 0.001)
  209. }
  210. func testLoadWaveformCorruptedCacheTriggersGeneration() {
  211. let vm = PlayerViewModel()
  212. let track = Track(title: "Test", filePath: "Music/test.mp3")
  213. track.waveformData = Data([0xFF, 0xFE])
  214. vm.loadWaveform(for: track)
  215. // Corrupted cache should not set samples synchronously
  216. // (generation will fail async since file doesn't exist)
  217. }
  218. }