import XCTest @testable import MixBoard @MainActor final class AudioEngineTests: XCTestCase { // MARK: - Error Descriptions func testFileNotFoundErrorDescription() { let error = AudioEngineError.fileNotFound("missing.mp3") XCTAssertEqual(error.errorDescription, "Audio file not found: missing.mp3") } func testFileNotFoundErrorWithEmptyName() { let error = AudioEngineError.fileNotFound("") XCTAssertNotNil(error.errorDescription) } // MARK: - Initial State func testInitialIsPlayingFalse() { let engine = AudioEngine() XCTAssertFalse(engine.isPlaying) } func testInitialCurrentTimeZero() { let engine = AudioEngine() XCTAssertEqual(engine.currentTime, 0) } func testInitialDurationZero() { let engine = AudioEngine() XCTAssertEqual(engine.duration, 0) } func testDefaultVolume() { let engine = AudioEngine() XCTAssertEqual(engine.volume, 0.8, accuracy: 0.001) } func testCurrentTrackNil() { let engine = AudioEngine() XCTAssertNil(engine.currentTrack) } // MARK: - Volume func testSetVolume() { let engine = AudioEngine() engine.volume = 0.5 XCTAssertEqual(engine.volume, 0.5, accuracy: 0.001) } func testSetVolumeZero() { let engine = AudioEngine() engine.volume = 0 XCTAssertEqual(engine.volume, 0, accuracy: 0.001) } func testSetVolumeMax() { let engine = AudioEngine() engine.volume = 1.0 XCTAssertEqual(engine.volume, 1.0, accuracy: 0.001) } // MARK: - Load Track Error Paths func testLoadTrackFileNotFound() { let engine = AudioEngine() let track = Track(title: "Missing", filePath: "nonexistent/path/missing.mp3") XCTAssertThrowsError(try engine.loadTrack(track)) { error in guard case AudioEngineError.fileNotFound = error else { XCTFail("Expected fileNotFound error, got \(error)") return } } } func testLoadTrackResetsPlayingState() { let engine = AudioEngine() // Even a failed load should leave isPlaying = false let track = Track(title: "Missing", filePath: "does/not/exist.mp3") try? engine.loadTrack(track) XCTAssertFalse(engine.isPlaying) } // MARK: - Stop func testStopResetsState() { let engine = AudioEngine() engine.stop() XCTAssertFalse(engine.isPlaying) XCTAssertEqual(engine.currentTime, 0) } // MARK: - Pause Without File func testPauseWithoutFile() { let engine = AudioEngine() engine.pause() XCTAssertFalse(engine.isPlaying) } // MARK: - Toggle Play/Pause Without File func testTogglePlayPauseWithNoFile() { let engine = AudioEngine() // Should not crash engine.togglePlayPause() XCTAssertFalse(engine.isPlaying) } // MARK: - EQ func testSetEQValidBand() { let engine = AudioEngine() // Should not crash engine.setEQ(band: 0, gain: 5.0) engine.setEQ(band: 1, gain: -3.0) engine.setEQ(band: 2, gain: 0.0) } func testSetEQOutOfRangeBand() { let engine = AudioEngine() // Should not crash — guard protects against out-of-range indices engine.setEQ(band: 99, gain: 10.0) engine.setEQ(band: -1, gain: 10.0) engine.setEQ(band: 3, gain: 10.0) engine.setEQ(band: 1000, gain: 10.0) } // MARK: - Seek Without File func testSeekToWithoutFile() { let engine = AudioEngine() engine.seek(to: 30.0) // Should not crash; currentTime should be clamped XCTAssertEqual(engine.currentTime, 0, accuracy: 0.01) } func testSeekByWithoutFile() { let engine = AudioEngine() engine.seek(by: 10.0) XCTAssertEqual(engine.currentTime, 0, accuracy: 0.01) } // MARK: - Update Current Time func testUpdateCurrentTimeNotPlaying() { let engine = AudioEngine() engine.updateCurrentTime() // Should not crash or change time XCTAssertEqual(engine.currentTime, 0) } // MARK: - Playback Callback func testOnPlaybackFinishedCallback() { let engine = AudioEngine() var callbackCalled = false engine.onPlaybackFinished = { callbackCalled = true } XCTAssertFalse(callbackCalled) // The callback should be set but not called until playback actually finishes XCTAssertNotNil(engine.onPlaybackFinished) } }