168 lines
5.3 KiB
Swift
168 lines
5.3 KiB
Swift
import Foundation
|
|
import XCTest
|
|
import VelodyDomain
|
|
@testable import VelodyPlayback
|
|
|
|
@MainActor
|
|
final class PlaybackControllerTests: XCTestCase {
|
|
func testPlayPauseRestartsTrackAfterPlaybackFinishesAtQueueEnd() {
|
|
let engine = FakePlaybackEngine()
|
|
let sessionStore = InMemoryPlaybackSessionStore()
|
|
let controller = PlaybackController(
|
|
engine: engine,
|
|
sessionStore: sessionStore
|
|
)
|
|
let track = LibraryTrack(
|
|
id: "track-1",
|
|
title: "Finished Track",
|
|
artist: "Tester",
|
|
durationSeconds: 120,
|
|
localFilePath: "/tmp/finished-track.mp3"
|
|
)
|
|
|
|
controller.setCatalogTracks([track])
|
|
controller.play(trackID: track.id)
|
|
|
|
XCTAssertEqual(engine.loadTrackCallCount, 1)
|
|
XCTAssertEqual(engine.playCallCount, 1)
|
|
XCTAssertTrue(controller.nowPlayingState.isPlaying)
|
|
|
|
engine.simulatePlaybackFinished()
|
|
|
|
XCTAssertFalse(controller.nowPlayingState.isPlaying)
|
|
XCTAssertEqual(controller.nowPlayingState.currentTime, 120)
|
|
|
|
controller.playPause()
|
|
|
|
XCTAssertEqual(engine.loadTrackCallCount, 2)
|
|
XCTAssertEqual(engine.playCallCount, 2)
|
|
XCTAssertEqual(engine.lastLoadedStartTime, 0)
|
|
XCTAssertTrue(controller.nowPlayingState.isPlaying)
|
|
XCTAssertEqual(controller.nowPlayingState.currentTime, 0)
|
|
}
|
|
|
|
func testPlayMissingFileCapturesPlaybackErrorWithoutStartingPlayback() {
|
|
let engine = FakePlaybackEngine()
|
|
let sessionStore = InMemoryPlaybackSessionStore()
|
|
let controller = PlaybackController(
|
|
engine: engine,
|
|
sessionStore: sessionStore
|
|
)
|
|
let track = LibraryTrack(
|
|
id: "missing-track",
|
|
title: "Missing Track",
|
|
artist: "Tester",
|
|
durationSeconds: 180,
|
|
localFilePath: "/tmp/missing-track.mp3"
|
|
)
|
|
|
|
engine.loadTrackErrorsByPath[track.localFilePath] = .missingLocalFile(path: track.localFilePath)
|
|
|
|
controller.setCatalogTracks([track])
|
|
controller.play(trackID: track.id)
|
|
|
|
XCTAssertEqual(engine.loadTrackCallCount, 1)
|
|
XCTAssertEqual(engine.playCallCount, 0)
|
|
XCTAssertFalse(controller.nowPlayingState.isPlaying)
|
|
XCTAssertEqual(controller.nowPlayingState.currentTrackID, track.id)
|
|
XCTAssertEqual(
|
|
controller.nowPlayingState.error,
|
|
.missingLocalFile(path: track.localFilePath)
|
|
)
|
|
XCTAssertEqual(controller.nowPlayingState.duration, track.durationSeconds)
|
|
}
|
|
|
|
func testMissingFileKeepsQueueStableAndAllowsNextTrackPlayback() {
|
|
let engine = FakePlaybackEngine()
|
|
let sessionStore = InMemoryPlaybackSessionStore()
|
|
let controller = PlaybackController(
|
|
engine: engine,
|
|
sessionStore: sessionStore
|
|
)
|
|
let missingTrack = LibraryTrack(
|
|
id: "missing-track",
|
|
title: "Missing Track",
|
|
artist: "Tester",
|
|
durationSeconds: 180,
|
|
localFilePath: "/tmp/missing-track.mp3"
|
|
)
|
|
let nextTrack = LibraryTrack(
|
|
id: "next-track",
|
|
title: "Next Track",
|
|
artist: "Tester",
|
|
durationSeconds: 90,
|
|
localFilePath: "/tmp/next-track.mp3"
|
|
)
|
|
|
|
engine.loadTrackErrorsByPath[missingTrack.localFilePath] = .missingLocalFile(
|
|
path: missingTrack.localFilePath
|
|
)
|
|
engine.durationByPath[nextTrack.localFilePath] = 90
|
|
|
|
controller.setCatalogTracks([missingTrack, nextTrack])
|
|
controller.play(trackID: missingTrack.id)
|
|
controller.next()
|
|
|
|
XCTAssertEqual(controller.nowPlayingState.currentTrackID, nextTrack.id)
|
|
XCTAssertTrue(controller.nowPlayingState.isPlaying)
|
|
XCTAssertNil(controller.nowPlayingState.error)
|
|
XCTAssertEqual(controller.nowPlayingState.queueTrackIDs, [missingTrack.id, nextTrack.id])
|
|
XCTAssertEqual(engine.loadTrackCallCount, 2)
|
|
XCTAssertEqual(engine.playCallCount, 1)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class FakePlaybackEngine: PlaybackEngine {
|
|
var onEvent: (@MainActor @Sendable (PlaybackEngineEvent) -> Void)?
|
|
var currentTime: Double = 0
|
|
var duration: Double = 120
|
|
var isPlaying = false
|
|
var durationByPath: [String: Double] = [:]
|
|
var loadTrackErrorsByPath: [String: PlaybackError] = [:]
|
|
|
|
private(set) var loadTrackCallCount = 0
|
|
private(set) var playCallCount = 0
|
|
private(set) var lastLoadedStartTime: Double?
|
|
|
|
func loadTrack(at fileURL: URL, startTime: Double) throws {
|
|
loadTrackCallCount += 1
|
|
|
|
if let error = loadTrackErrorsByPath[fileURL.path] {
|
|
currentTime = 0
|
|
duration = 0
|
|
isPlaying = false
|
|
throw error
|
|
}
|
|
|
|
lastLoadedStartTime = startTime
|
|
currentTime = startTime
|
|
duration = durationByPath[fileURL.path] ?? duration
|
|
isPlaying = false
|
|
}
|
|
|
|
func play() throws {
|
|
playCallCount += 1
|
|
isPlaying = true
|
|
}
|
|
|
|
func pause() {
|
|
isPlaying = false
|
|
}
|
|
|
|
func stop() {
|
|
isPlaying = false
|
|
currentTime = 0
|
|
}
|
|
|
|
func seek(to time: Double) throws {
|
|
currentTime = min(max(time, 0), duration)
|
|
}
|
|
|
|
func simulatePlaybackFinished() {
|
|
isPlaying = false
|
|
currentTime = duration
|
|
onEvent?(.finishedPlaying)
|
|
}
|
|
}
|