import XCTest @testable import MixBoard /// UI-level verification tests for the MixBoard UI Revamp. /// Tests sidebar selection model, player bar layout invariants, and edge cases. final class UIRevampTests: XCTestCase { // MARK: - SidebarSection Selection Model func testSidebarSectionEquality_LibraryDestinations() { let browse1 = SidebarSection.library(.browse) let browse2 = SidebarSection.library(.browse) let albums = SidebarSection.library(.albums) XCTAssertEqual(browse1, browse2) XCTAssertNotEqual(browse1, albums) } func testSidebarSectionEquality_QueueIsSingleton() { let queue1 = SidebarSection.queue let queue2 = SidebarSection.queue XCTAssertEqual(queue1, queue2) } func testSidebarSectionEquality_QueueNotEqualToLibrary() { let queue = SidebarSection.queue let library = SidebarSection.library(.browse) XCTAssertNotEqual(queue, library) } func testLibraryDestination_AllCases() { let allCases = LibraryDestination.allCases XCTAssertEqual(allCases.count, 6) XCTAssertTrue(allCases.contains(.browse)) XCTAssertTrue(allCases.contains(.albums)) XCTAssertTrue(allCases.contains(.artists)) XCTAssertTrue(allCases.contains(.genres)) XCTAssertTrue(allCases.contains(.years)) XCTAssertTrue(allCases.contains(.search)) } func testLibraryDestination_InitialNavStack() { XCTAssertTrue(LibraryDestination.browse.initialNavStack.isEmpty) XCTAssertEqual(LibraryDestination.albums.initialNavStack, [.category(.album)]) XCTAssertEqual(LibraryDestination.artists.initialNavStack, [.category(.artist)]) XCTAssertEqual(LibraryDestination.genres.initialNavStack, [.category(.genre)]) XCTAssertEqual(LibraryDestination.years.initialNavStack, [.category(.year)]) XCTAssertEqual(LibraryDestination.search.initialNavStack, [.search(query: "")]) } func testLibraryDestination_DisplayNames() { XCTAssertEqual(LibraryDestination.browse.displayName, "Browse") XCTAssertEqual(LibraryDestination.albums.displayName, "Albums") XCTAssertEqual(LibraryDestination.artists.displayName, "Artists") XCTAssertEqual(LibraryDestination.genres.displayName, "Genres") XCTAssertEqual(LibraryDestination.years.displayName, "Years") XCTAssertEqual(LibraryDestination.search.displayName, "Search") } func testLibraryDestination_Icons() { for dest in LibraryDestination.allCases { XCTAssertFalse(dest.icon.isEmpty, "\(dest) should have an icon") } } // MARK: - Sidebar Selection Toggle Logic (⌘B) func testCommandBToggle_FromNil_NavigatesToLibrary() { var selection: SidebarSection? = nil // Simulate ⌘B press if case .library = selection { selection = nil } else { selection = .library(.browse) } XCTAssertEqual(selection, .library(.browse)) } func testCommandBToggle_FromLibrary_NavigatesToNil() { var selection: SidebarSection? = SidebarSection.library(.browse) // Simulate ⌘B press if case .library = selection { selection = nil } else { selection = .library(.browse) } XCTAssertNil(selection) } func testCommandBToggle_FromQueue_NavigatesToLibrary() { var selection: SidebarSection? = SidebarSection.queue // Simulate ⌘B press if case .library = selection { selection = nil } else { selection = .library(.browse) } XCTAssertEqual(selection, .library(.browse)) } func testCommandBToggle_FromAlbumsLibrary_NavigatesToNil() { var selection: SidebarSection? = SidebarSection.library(.albums) // Simulate ⌘B press — any .library case should toggle off if case .library = selection { selection = nil } else { selection = .library(.browse) } XCTAssertNil(selection) } // MARK: - Sidebar Selection: Mutual Exclusivity func testSidebarSelection_LibraryReplacesPlaylist() { // Selecting a library item should replace any playlist selection // (single selection model ensures this automatically) var selection: SidebarSection? = nil // Select a library destination selection = .library(.albums) if case .library = selection { // OK } else { XCTFail("Should be library selection") } // Now select queue selection = .queue XCTAssertEqual(selection, .queue) if case .library = selection { XCTFail("Should no longer be library") } } // 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: - 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" } }