UIRevampTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import XCTest
  2. @testable import MixBoard
  3. /// UI-level verification tests for the MixBoard UI Revamp.
  4. /// Tests panel state management, player bar layout invariants, and edge cases.
  5. final class UIRevampTests: XCTestCase {
  6. // MARK: - BrowsePanelTab Enum
  7. func testBrowsePanelTabHasCloudAndQueueCases() {
  8. let cloud = BrowsePanelTab.cloud
  9. let queue = BrowsePanelTab.queue
  10. XCTAssertEqual(cloud.rawValue, "Cloud")
  11. XCTAssertEqual(queue.rawValue, "Queue")
  12. }
  13. func testBrowsePanelTabConformsToAllCases() {
  14. let allCases = BrowsePanelTab.allCases
  15. XCTAssertEqual(allCases.count, 2)
  16. XCTAssertTrue(allCases.contains(.cloud))
  17. XCTAssertTrue(allCases.contains(.queue))
  18. }
  19. // MARK: - Player ViewModel State (Player Bar Support)
  20. @MainActor
  21. func testPlayerVMDefaultState_NoTrackLoaded() {
  22. let playerVM = PlayerViewModel()
  23. // When no track is loaded, currentTrack should be nil
  24. XCTAssertNil(playerVM.currentTrack, "currentTrack should be nil when nothing is loaded")
  25. XCTAssertFalse(playerVM.isPlaying)
  26. }
  27. @MainActor
  28. func testPlayerVMShuffleToggle() {
  29. let playerVM = PlayerViewModel()
  30. let initial = playerVM.shuffleEnabled
  31. playerVM.shuffleEnabled.toggle()
  32. XCTAssertNotEqual(playerVM.shuffleEnabled, initial)
  33. playerVM.shuffleEnabled.toggle()
  34. XCTAssertEqual(playerVM.shuffleEnabled, initial)
  35. }
  36. @MainActor
  37. func testPlayerVMRepeatModeCycles() {
  38. let playerVM = PlayerViewModel()
  39. // Test repeat mode cycling: off → all → one → off
  40. XCTAssertEqual(playerVM.repeatMode, .off)
  41. playerVM.repeatMode = .all
  42. XCTAssertEqual(playerVM.repeatMode, .all)
  43. playerVM.repeatMode = .one
  44. XCTAssertEqual(playerVM.repeatMode, .one)
  45. playerVM.repeatMode = .off
  46. XCTAssertEqual(playerVM.repeatMode, .off)
  47. }
  48. @MainActor
  49. func testPlayerVMVolumeRange() {
  50. let playerVM = PlayerViewModel()
  51. // Volume should accept full range
  52. playerVM.volume = 0.0
  53. XCTAssertEqual(playerVM.volume, 0.0, accuracy: 0.001)
  54. playerVM.volume = 0.5
  55. XCTAssertEqual(playerVM.volume, 0.5, accuracy: 0.001)
  56. playerVM.volume = 1.0
  57. XCTAssertEqual(playerVM.volume, 1.0, accuracy: 0.001)
  58. }
  59. // MARK: - Panel Toggle Logic (Functional Verification)
  60. /// Simulates the sidebar toggle logic for the Cloud button.
  61. /// This mirrors the exact logic in SidebarView lines 37-43.
  62. func testCloudToggleLogic_OpensPanel() {
  63. var isBrowsePanelOpen = false
  64. var browsePanelTab: BrowsePanelTab = .cloud
  65. // Simulate clicking "Chad Music" when panel is closed
  66. if isBrowsePanelOpen && browsePanelTab == .cloud {
  67. isBrowsePanelOpen = false
  68. } else {
  69. browsePanelTab = .cloud
  70. isBrowsePanelOpen = true
  71. }
  72. XCTAssertTrue(isBrowsePanelOpen)
  73. XCTAssertEqual(browsePanelTab, .cloud)
  74. }
  75. func testCloudToggleLogic_ClosesWhenAlreadyShowing() {
  76. var isBrowsePanelOpen = true
  77. var browsePanelTab: BrowsePanelTab = .cloud
  78. // Simulate clicking "Chad Music" when panel already shows cloud
  79. if isBrowsePanelOpen && browsePanelTab == .cloud {
  80. isBrowsePanelOpen = false
  81. } else {
  82. browsePanelTab = .cloud
  83. isBrowsePanelOpen = true
  84. }
  85. XCTAssertFalse(isBrowsePanelOpen)
  86. }
  87. func testQueueToggleLogic_OpensPanel() {
  88. var isBrowsePanelOpen = false
  89. var browsePanelTab: BrowsePanelTab = .cloud
  90. // Simulate clicking "Queue" when panel is closed
  91. if isBrowsePanelOpen && browsePanelTab == .queue {
  92. isBrowsePanelOpen = false
  93. } else {
  94. browsePanelTab = .queue
  95. isBrowsePanelOpen = true
  96. }
  97. XCTAssertTrue(isBrowsePanelOpen)
  98. XCTAssertEqual(browsePanelTab, .queue)
  99. }
  100. func testQueueToggleLogic_ClosesWhenAlreadyShowing() {
  101. var isBrowsePanelOpen = true
  102. var browsePanelTab: BrowsePanelTab = .queue
  103. // Simulate clicking "Queue" when panel already shows queue
  104. if isBrowsePanelOpen && browsePanelTab == .queue {
  105. isBrowsePanelOpen = false
  106. } else {
  107. browsePanelTab = .queue
  108. isBrowsePanelOpen = true
  109. }
  110. XCTAssertFalse(isBrowsePanelOpen)
  111. }
  112. func testCloudToggle_SwitchesFromQueueToCloud() {
  113. var isBrowsePanelOpen = true
  114. var browsePanelTab: BrowsePanelTab = .queue
  115. // Clicking "Chad Music" when panel shows queue → switch to cloud (don't close)
  116. if isBrowsePanelOpen && browsePanelTab == .cloud {
  117. isBrowsePanelOpen = false
  118. } else {
  119. browsePanelTab = .cloud
  120. isBrowsePanelOpen = true
  121. }
  122. XCTAssertTrue(isBrowsePanelOpen, "Panel should stay open when switching tabs")
  123. XCTAssertEqual(browsePanelTab, .cloud, "Tab should switch to cloud")
  124. }
  125. func testQueueToggle_SwitchesFromCloudToQueue() {
  126. var isBrowsePanelOpen = true
  127. var browsePanelTab: BrowsePanelTab = .cloud
  128. // Clicking "Queue" when panel shows cloud → switch to queue (don't close)
  129. if isBrowsePanelOpen && browsePanelTab == .queue {
  130. isBrowsePanelOpen = false
  131. } else {
  132. browsePanelTab = .queue
  133. isBrowsePanelOpen = true
  134. }
  135. XCTAssertTrue(isBrowsePanelOpen, "Panel should stay open when switching tabs")
  136. XCTAssertEqual(browsePanelTab, .queue, "Tab should switch to queue")
  137. }
  138. // MARK: - Edge Case: playbackMode Change While Queue Tab Active
  139. /// Verifies that when playbackMode changes from "queue" while the browse
  140. /// panel has queue tab selected, the tab resets to .cloud (BrowsePanel onChange fix).
  141. func testPlaybackModeChange_QueueTabResetsToCloud() {
  142. var isBrowsePanelOpen = true
  143. var browsePanelTab: BrowsePanelTab = .queue
  144. var playbackMode = "queue"
  145. // Precondition: panel open with queue tab, queue mode active
  146. XCTAssertTrue(isBrowsePanelOpen)
  147. XCTAssertEqual(browsePanelTab, .queue)
  148. // Simulate changing playback mode away from queue
  149. playbackMode = "linear"
  150. // Simulate BrowsePanel.onChange(of: playbackMode) logic
  151. if playbackMode != "queue" && browsePanelTab == .queue {
  152. browsePanelTab = .cloud
  153. }
  154. // After the fix: tab resets to .cloud, cloud view is visible
  155. XCTAssertEqual(browsePanelTab, .cloud, "Tab should reset to .cloud when queue mode disabled")
  156. XCTAssertTrue(isBrowsePanelOpen, "Panel should remain open")
  157. let cloudOpacity: Double = browsePanelTab == .cloud ? 1 : 0
  158. XCTAssertEqual(cloudOpacity, 1, "Cloud view should be visible after tab reset")
  159. }
  160. /// Verifies no reset happens when playbackMode changes but tab is already .cloud.
  161. func testPlaybackModeChange_CloudTabActive_NoChange() {
  162. var browsePanelTab: BrowsePanelTab = .cloud
  163. var playbackMode = "queue"
  164. playbackMode = "linear"
  165. // Simulate BrowsePanel.onChange(of: playbackMode) logic
  166. if playbackMode != "queue" && browsePanelTab == .queue {
  167. browsePanelTab = .cloud
  168. }
  169. XCTAssertEqual(browsePanelTab, .cloud, "Tab should remain .cloud — no change needed")
  170. }
  171. // MARK: - Panel Keyboard Shortcut Toggle
  172. func testCommandBToggle() {
  173. var isBrowsePanelOpen = false
  174. // Simulate ⌘B press (the hidden button action)
  175. isBrowsePanelOpen.toggle()
  176. XCTAssertTrue(isBrowsePanelOpen)
  177. isBrowsePanelOpen.toggle()
  178. XCTAssertFalse(isBrowsePanelOpen)
  179. }
  180. // MARK: - Close Button
  181. func testCloseButtonSetsFalse() {
  182. var isBrowsePanelOpen = true
  183. // Simulate xmark close button action
  184. isBrowsePanelOpen = false
  185. XCTAssertFalse(isBrowsePanelOpen)
  186. }
  187. // MARK: - Player Bar: Track Info Presence
  188. @MainActor
  189. func testPlayerBar_NoTrack_ShowsNotPlaying() {
  190. let playerVM = PlayerViewModel()
  191. XCTAssertNil(playerVM.currentTrack, "No track loaded — center zone should show 'Not Playing'")
  192. XCTAssertFalse(playerVM.isPlaying)
  193. }
  194. @MainActor
  195. func testPlayerBar_WithTrack_CenterZonePopulated() async throws {
  196. let url = try TestHelpers.createTestAudioFile(name: "ui_test", duration: 1.0)
  197. let track = Track(title: "Test Track", artist: "Test Artist", filePath: url.path, duration: 1.0, fileFormat: "WAV")
  198. let playerVM = PlayerViewModel()
  199. playerVM.loadAndPlay(track)
  200. playerVM.syncForTest()
  201. XCTAssertNotNil(playerVM.currentTrack)
  202. XCTAssertEqual(playerVM.currentTrack?.title, "Test Track")
  203. XCTAssertEqual(playerVM.currentTrack?.artist, "Test Artist")
  204. playerVM.stop()
  205. }
  206. // MARK: - Player Bar: Volume Icon Mapping
  207. @MainActor
  208. func testVolumeIconMapping() {
  209. let testCases: [(volume: Double, expectedIcon: String)] = [
  210. (0.0, "speaker.slash.fill"),
  211. (0.1, "speaker.wave.1.fill"),
  212. (0.15, "speaker.wave.1.fill"),
  213. (0.32, "speaker.wave.1.fill"),
  214. (0.33, "speaker.wave.2.fill"),
  215. (0.5, "speaker.wave.2.fill"),
  216. (0.65, "speaker.wave.2.fill"),
  217. (0.66, "speaker.wave.3.fill"),
  218. (0.85, "speaker.wave.3.fill"),
  219. (0.9, "speaker.wave.3.fill"),
  220. (1.0, "speaker.wave.3.fill"),
  221. ]
  222. for testCase in testCases {
  223. let icon = volumeIcon(for: testCase.volume)
  224. XCTAssertEqual(icon, testCase.expectedIcon,
  225. "Volume \(testCase.volume) should show \(testCase.expectedIcon)")
  226. }
  227. }
  228. /// Mirror of PlayerView.volumeIcon computed property.
  229. private func volumeIcon(for volume: Double) -> String {
  230. if volume == 0 { return "speaker.slash.fill" }
  231. if volume < 0.33 { return "speaker.wave.1.fill" }
  232. if volume < 0.66 { return "speaker.wave.2.fill" }
  233. return "speaker.wave.3.fill"
  234. }
  235. // MARK: - selectedPlaylist Preservation
  236. func testSelectedPlaylistNotClearedByPanelToggle() {
  237. // Simulates ContentView state: opening the browse panel should not affect selectedPlaylist
  238. let playlistID = UUID()
  239. var selectedPlaylistID: UUID? = playlistID
  240. var isBrowsePanelOpen = false
  241. var browsePanelTab: BrowsePanelTab = .cloud
  242. // Open panel via sidebar cloud button
  243. browsePanelTab = .cloud
  244. isBrowsePanelOpen = true
  245. XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared when panel opens")
  246. // Switch tab
  247. browsePanelTab = .queue
  248. XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared on tab switch")
  249. // Close panel
  250. isBrowsePanelOpen = false
  251. XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared when panel closes")
  252. // Toggle via ⌘B
  253. isBrowsePanelOpen.toggle()
  254. XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared on ⌘B toggle")
  255. }
  256. }