import XCTest @testable import MixBoard /// UI-level verification tests for the MixBoard UI Revamp. /// Tests panel state management, player bar layout invariants, and edge cases. final class UIRevampTests: XCTestCase { // MARK: - BrowsePanelTab Enum func testBrowsePanelTabHasCloudAndQueueCases() { let cloud = BrowsePanelTab.cloud let queue = BrowsePanelTab.queue XCTAssertEqual(cloud.rawValue, "Cloud") XCTAssertEqual(queue.rawValue, "Queue") } func testBrowsePanelTabConformsToAllCases() { let allCases = BrowsePanelTab.allCases XCTAssertEqual(allCases.count, 2) XCTAssertTrue(allCases.contains(.cloud)) XCTAssertTrue(allCases.contains(.queue)) } // MARK: - Player ViewModel State (Player Bar Support) @MainActor func testPlayerVMDefaultState_NoTrackLoaded() { let playerVM = PlayerViewModel() // When no track is loaded, currentTrack should be nil XCTAssertNil(playerVM.currentTrack, "currentTrack should be nil when nothing is loaded") XCTAssertFalse(playerVM.isPlaying) } @MainActor func testPlayerVMShuffleToggle() { let playerVM = PlayerViewModel() let initial = playerVM.shuffleEnabled playerVM.shuffleEnabled.toggle() XCTAssertNotEqual(playerVM.shuffleEnabled, initial) playerVM.shuffleEnabled.toggle() XCTAssertEqual(playerVM.shuffleEnabled, initial) } @MainActor func testPlayerVMRepeatModeCycles() { let playerVM = PlayerViewModel() // Test repeat mode cycling: off → all → one → off XCTAssertEqual(playerVM.repeatMode, .off) playerVM.repeatMode = .all XCTAssertEqual(playerVM.repeatMode, .all) playerVM.repeatMode = .one XCTAssertEqual(playerVM.repeatMode, .one) playerVM.repeatMode = .off XCTAssertEqual(playerVM.repeatMode, .off) } @MainActor func testPlayerVMVolumeRange() { let playerVM = PlayerViewModel() // Volume should accept full range playerVM.volume = 0.0 XCTAssertEqual(playerVM.volume, 0.0, accuracy: 0.001) playerVM.volume = 0.5 XCTAssertEqual(playerVM.volume, 0.5, accuracy: 0.001) playerVM.volume = 1.0 XCTAssertEqual(playerVM.volume, 1.0, accuracy: 0.001) } // MARK: - Panel Toggle Logic (Functional Verification) /// Simulates the sidebar toggle logic for the Cloud button. /// This mirrors the exact logic in SidebarView lines 37-43. func testCloudToggleLogic_OpensPanel() { var isBrowsePanelOpen = false var browsePanelTab: BrowsePanelTab = .cloud // Simulate clicking "Chad Music" when panel is closed if isBrowsePanelOpen && browsePanelTab == .cloud { isBrowsePanelOpen = false } else { browsePanelTab = .cloud isBrowsePanelOpen = true } XCTAssertTrue(isBrowsePanelOpen) XCTAssertEqual(browsePanelTab, .cloud) } func testCloudToggleLogic_ClosesWhenAlreadyShowing() { var isBrowsePanelOpen = true var browsePanelTab: BrowsePanelTab = .cloud // Simulate clicking "Chad Music" when panel already shows cloud if isBrowsePanelOpen && browsePanelTab == .cloud { isBrowsePanelOpen = false } else { browsePanelTab = .cloud isBrowsePanelOpen = true } XCTAssertFalse(isBrowsePanelOpen) } func testQueueToggleLogic_OpensPanel() { var isBrowsePanelOpen = false var browsePanelTab: BrowsePanelTab = .cloud // Simulate clicking "Queue" when panel is closed if isBrowsePanelOpen && browsePanelTab == .queue { isBrowsePanelOpen = false } else { browsePanelTab = .queue isBrowsePanelOpen = true } XCTAssertTrue(isBrowsePanelOpen) XCTAssertEqual(browsePanelTab, .queue) } func testQueueToggleLogic_ClosesWhenAlreadyShowing() { var isBrowsePanelOpen = true var browsePanelTab: BrowsePanelTab = .queue // Simulate clicking "Queue" when panel already shows queue if isBrowsePanelOpen && browsePanelTab == .queue { isBrowsePanelOpen = false } else { browsePanelTab = .queue isBrowsePanelOpen = true } XCTAssertFalse(isBrowsePanelOpen) } func testCloudToggle_SwitchesFromQueueToCloud() { var isBrowsePanelOpen = true var browsePanelTab: BrowsePanelTab = .queue // Clicking "Chad Music" when panel shows queue → switch to cloud (don't close) if isBrowsePanelOpen && browsePanelTab == .cloud { isBrowsePanelOpen = false } else { browsePanelTab = .cloud isBrowsePanelOpen = true } XCTAssertTrue(isBrowsePanelOpen, "Panel should stay open when switching tabs") XCTAssertEqual(browsePanelTab, .cloud, "Tab should switch to cloud") } func testQueueToggle_SwitchesFromCloudToQueue() { var isBrowsePanelOpen = true var browsePanelTab: BrowsePanelTab = .cloud // Clicking "Queue" when panel shows cloud → switch to queue (don't close) if isBrowsePanelOpen && browsePanelTab == .queue { isBrowsePanelOpen = false } else { browsePanelTab = .queue isBrowsePanelOpen = true } XCTAssertTrue(isBrowsePanelOpen, "Panel should stay open when switching tabs") XCTAssertEqual(browsePanelTab, .queue, "Tab should switch to queue") } // MARK: - Edge Case: playbackMode Change While Queue Tab Active /// Verifies that when playbackMode changes from "queue" while the browse /// panel has queue tab selected, the tab resets to .cloud (BrowsePanel onChange fix). func testPlaybackModeChange_QueueTabResetsToCloud() { var isBrowsePanelOpen = true var browsePanelTab: BrowsePanelTab = .queue var playbackMode = "queue" // Precondition: panel open with queue tab, queue mode active XCTAssertTrue(isBrowsePanelOpen) XCTAssertEqual(browsePanelTab, .queue) // Simulate changing playback mode away from queue playbackMode = "linear" // Simulate BrowsePanel.onChange(of: playbackMode) logic if playbackMode != "queue" && browsePanelTab == .queue { browsePanelTab = .cloud } // After the fix: tab resets to .cloud, cloud view is visible XCTAssertEqual(browsePanelTab, .cloud, "Tab should reset to .cloud when queue mode disabled") XCTAssertTrue(isBrowsePanelOpen, "Panel should remain open") let cloudOpacity: Double = browsePanelTab == .cloud ? 1 : 0 XCTAssertEqual(cloudOpacity, 1, "Cloud view should be visible after tab reset") } /// Verifies no reset happens when playbackMode changes but tab is already .cloud. func testPlaybackModeChange_CloudTabActive_NoChange() { var browsePanelTab: BrowsePanelTab = .cloud var playbackMode = "queue" playbackMode = "linear" // Simulate BrowsePanel.onChange(of: playbackMode) logic if playbackMode != "queue" && browsePanelTab == .queue { browsePanelTab = .cloud } XCTAssertEqual(browsePanelTab, .cloud, "Tab should remain .cloud — no change needed") } // MARK: - Panel Keyboard Shortcut Toggle func testCommandBToggle() { var isBrowsePanelOpen = false // Simulate ⌘B press (the hidden button action) isBrowsePanelOpen.toggle() XCTAssertTrue(isBrowsePanelOpen) isBrowsePanelOpen.toggle() XCTAssertFalse(isBrowsePanelOpen) } // MARK: - Close Button func testCloseButtonSetsFalse() { var isBrowsePanelOpen = true // Simulate xmark close button action isBrowsePanelOpen = false XCTAssertFalse(isBrowsePanelOpen) } // MARK: - Player Bar: Track Info Presence @MainActor func testPlayerBar_NoTrack_ShowsNotPlaying() { let playerVM = PlayerViewModel() XCTAssertNil(playerVM.currentTrack, "No track loaded — center zone should show 'Not Playing'") XCTAssertFalse(playerVM.isPlaying) } @MainActor func testPlayerBar_WithTrack_CenterZonePopulated() async throws { let url = try TestHelpers.createTestAudioFile(name: "ui_test", duration: 1.0) let track = Track(title: "Test Track", artist: "Test Artist", filePath: url.path, duration: 1.0, fileFormat: "WAV") let playerVM = PlayerViewModel() playerVM.loadAndPlay(track) playerVM.syncForTest() XCTAssertNotNil(playerVM.currentTrack) XCTAssertEqual(playerVM.currentTrack?.title, "Test Track") XCTAssertEqual(playerVM.currentTrack?.artist, "Test Artist") playerVM.stop() } // MARK: - Player Bar: Volume Icon Mapping @MainActor func testVolumeIconMapping() { let testCases: [(volume: Double, expectedIcon: String)] = [ (0.0, "speaker.slash.fill"), (0.1, "speaker.wave.1.fill"), (0.15, "speaker.wave.1.fill"), (0.32, "speaker.wave.1.fill"), (0.33, "speaker.wave.2.fill"), (0.5, "speaker.wave.2.fill"), (0.65, "speaker.wave.2.fill"), (0.66, "speaker.wave.3.fill"), (0.85, "speaker.wave.3.fill"), (0.9, "speaker.wave.3.fill"), (1.0, "speaker.wave.3.fill"), ] for testCase in testCases { let icon = volumeIcon(for: testCase.volume) XCTAssertEqual(icon, testCase.expectedIcon, "Volume \(testCase.volume) should show \(testCase.expectedIcon)") } } /// Mirror of PlayerView.volumeIcon computed property. private func volumeIcon(for volume: Double) -> String { if volume == 0 { return "speaker.slash.fill" } if volume < 0.33 { return "speaker.wave.1.fill" } if volume < 0.66 { return "speaker.wave.2.fill" } return "speaker.wave.3.fill" } // MARK: - selectedPlaylist Preservation func testSelectedPlaylistNotClearedByPanelToggle() { // Simulates ContentView state: opening the browse panel should not affect selectedPlaylist let playlistID = UUID() var selectedPlaylistID: UUID? = playlistID var isBrowsePanelOpen = false var browsePanelTab: BrowsePanelTab = .cloud // Open panel via sidebar cloud button browsePanelTab = .cloud isBrowsePanelOpen = true XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared when panel opens") // Switch tab browsePanelTab = .queue XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared on tab switch") // Close panel isBrowsePanelOpen = false XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared when panel closes") // Toggle via ⌘B isBrowsePanelOpen.toggle() XCTAssertEqual(selectedPlaylistID, playlistID, "selectedPlaylist should not be cleared on ⌘B toggle") } }