velody/packages/apple/VelodyPlayback/Tests/VelodyPlaybackTests/PlaybackControllerTests.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)
}
}