import XCTest import SwiftData @testable import MixBoard /// Integration tests that exercise full app workflows programmatically. /// These test the ViewModels, services, and data flow as a user would interact. final class IntegrationTests: XCTestCase { override func tearDown() { super.tearDown() TestHelpers.cleanupTestFiles() } // MARK: - Player ViewModel Flow @MainActor func testPlayerLoadAndPlayFlow() async throws { let url = try TestHelpers.createTestAudioFile(name: "play_test", duration: 2.0) let track = Track(title: "Test Song", artist: "Artist", filePath: url.path, duration: 2.0, fileFormat: "WAV") let playerVM = PlayerViewModel() // Initially stopped XCTAssertFalse(playerVM.isPlaying) XCTAssertNil(playerVM.currentTrack) XCTAssertNil(playerVM.currentPlayingEntryID) // Load and play playerVM.loadAndPlay(track) playerVM.syncForTest() XCTAssertTrue(playerVM.isPlaying) XCTAssertEqual(playerVM.currentTrack?.title, "Test Song") XCTAssertGreaterThan(playerVM.duration, 1.0) // Pause playerVM.togglePlayPause() playerVM.syncForTest() XCTAssertFalse(playerVM.isPlaying) // Resume playerVM.togglePlayPause() playerVM.syncForTest() XCTAssertTrue(playerVM.isPlaying) // Stop playerVM.stop() playerVM.syncForTest() XCTAssertFalse(playerVM.isPlaying) XCTAssertEqual(playerVM.currentTime, 0) } @MainActor func testPlayerSeekFlow() async throws { let url = try TestHelpers.createTestAudioFile(name: "seek_test", duration: 5.0) let track = Track(title: "Seek", filePath: url.path, duration: 5.0, fileFormat: "WAV") let playerVM = PlayerViewModel() playerVM.loadAndPlay(track) playerVM.syncForTest() // Seek to middle playerVM.seekToProgress(0.5) playerVM.syncForTest() // Time should be approximately half XCTAssertGreaterThan(playerVM.currentTime, 1.5) XCTAssertLessThan(playerVM.currentTime, 3.5) // Should still be playing XCTAssertTrue(playerVM.isPlaying) playerVM.stop() } @MainActor func testPlayerVolumeControl() { let playerVM = PlayerViewModel() XCTAssertEqual(playerVM.volume, 0.8, accuracy: 0.01) // default playerVM.volume = 0.5 XCTAssertEqual(playerVM.volume, 0.5, accuracy: 0.01) playerVM.volume = 0.0 XCTAssertEqual(playerVM.volume, 0.0, accuracy: 0.01) playerVM.volume = 1.0 XCTAssertEqual(playerVM.volume, 1.0, accuracy: 0.01) } // MARK: - Playlist ViewModel Flow @MainActor func testPlaylistNextPreviousFlow() async throws { let urls = try TestHelpers.createTestAudioFiles(count: 3, duration: 1.0) let playlist = Playlist(name: "Nav Test") var tracks: [Track] = [] for (i, url) in urls.enumerated() { let t = Track(title: "Track \(i+1)", filePath: url.path, duration: 1.0, fileFormat: "WAV") playlist.addTrack(t) tracks.append(t) } let playerVM = PlayerViewModel() // Play first track let entries = playlist.sortedEntries playerVM.loadAndPlay(tracks[0], entryID: entries[0].id, playlist: playlist) playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 1") XCTAssertEqual(playerVM.currentPlayingEntryID, entries[0].id) // Next playerVM.playNext() playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 2") // Next playerVM.playNext() playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 3") // Next at end — should stop playerVM.playNext() playerVM.syncForTest() XCTAssertFalse(playerVM.isPlaying) // Play last, then previous playerVM.loadAndPlay(tracks[2], entryID: entries[2].id, playlist: playlist) playerVM.syncForTest() playerVM.playPrevious() // currentTime < 3, go to previous playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 2") playerVM.stop() } @MainActor func testRepeatAllMode() async throws { let urls = try TestHelpers.createTestAudioFiles(count: 2, duration: 1.0) let playlist = Playlist(name: "Repeat Test") var tracks: [Track] = [] for (i, url) in urls.enumerated() { let t = Track(title: "Track \(i+1)", filePath: url.path, duration: 1.0, fileFormat: "WAV") playlist.addTrack(t) tracks.append(t) } let playerVM = PlayerViewModel() playerVM.repeatMode = .all let entries = playlist.sortedEntries playerVM.loadAndPlay(tracks[1], entryID: entries[1].id, playlist: playlist) playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 2") // Next from last track should wrap to first playerVM.playNext() playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 1") XCTAssertTrue(playerVM.isPlaying) playerVM.stop() } @MainActor func testRepeatOneMode() async throws { let urls = try TestHelpers.createTestAudioFiles(count: 2, duration: 1.0) let playlist = Playlist(name: "Repeat1 Test") var tracks: [Track] = [] for (i, url) in urls.enumerated() { let t = Track(title: "Track \(i+1)", filePath: url.path, duration: 1.0, fileFormat: "WAV") playlist.addTrack(t) tracks.append(t) } let playerVM = PlayerViewModel() playerVM.repeatMode = .one let entries = playlist.sortedEntries playerVM.loadAndPlay(tracks[0], entryID: entries[0].id, playlist: playlist) playerVM.syncForTest() // Next should replay same track playerVM.playNext() playerVM.syncForTest() XCTAssertEqual(playerVM.currentTrack?.title, "Track 1") playerVM.stop() } @MainActor func testShuffleMode() async throws { let urls = try TestHelpers.createTestAudioFiles(count: 5, duration: 0.5) let playlist = Playlist(name: "Shuffle Test") var tracks: [Track] = [] for (i, url) in urls.enumerated() { let t = Track(title: "Track \(i+1)", filePath: url.path, duration: 0.5, fileFormat: "WAV") playlist.addTrack(t) tracks.append(t) } let playerVM = PlayerViewModel() playerVM.shuffleEnabled = true let entries = playlist.sortedEntries playerVM.loadAndPlay(tracks[0], entryID: entries[0].id, playlist: playlist) playerVM.syncForTest() let firstTitle = playerVM.currentTrack?.title // Next should play a different track (with 5 tracks, very likely) playerVM.playNext() playerVM.syncForTest() XCTAssertTrue(playerVM.isPlaying) // Can't guarantee it's different (random), but it should still be a valid track XCTAssertNotNil(playerVM.currentTrack) playerVM.stop() } @MainActor func testShuffleAndRepeatDefaults() { let playerVM = PlayerViewModel() XCTAssertFalse(playerVM.shuffleEnabled) XCTAssertEqual(playerVM.repeatMode, .off) } func testSessionExportFlow() throws { let outputDir = FileManager.default.temporaryDirectory .appendingPathComponent("IntegrationExport", isDirectory: true) try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: outputDir) } let playlist = Playlist(name: "Export Flow Test") let t1 = Track(title: "Song A", artist: "Art1", filePath: "/tmp/a.mp3", duration: 120, fileFormat: "MP3") let t2 = Track(title: "Song B", artist: "Art2", filePath: "/tmp/b.wav", duration: 180, fileFormat: "WAV") t1.bpm = 128 t1.musicalKey = "Am" playlist.addTrack(t1) playlist.addTrack(t2, crossfadeDuration: 3.0) // Export Audition session let sesxURL = outputDir.appendingPathComponent("test.sesx") var options = ExportOptions.default options.copyAudioFiles = false try AuditionExporter.export(playlist: playlist, to: sesxURL, options: options) let sesx = try String(contentsOf: sesxURL, encoding: .utf8) XCTAssertTrue(sesx.contains("Song A")) XCTAssertTrue(sesx.contains("Song B")) XCTAssertTrue(sesx.contains("