| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- 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"
- }
- }
|