| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- 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")
- }
- }
|