Add macOS local playback engine
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class FakePlaybackEngine: PlaybackEngine {
|
||||
var onEvent: (@MainActor @Sendable (PlaybackEngineEvent) -> Void)?
|
||||
var currentTime: Double = 0
|
||||
var duration: Double = 120
|
||||
var isPlaying = false
|
||||
|
||||
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
|
||||
lastLoadedStartTime = startTime
|
||||
currentTime = startTime
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import XCTest
|
||||
@testable import VelodyPlayback
|
||||
|
||||
final class PlaybackQueueTests: XCTestCase {
|
||||
func testQueueStartsWithoutCurrentTrackSelection() {
|
||||
let queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
|
||||
|
||||
XCTAssertEqual(queue.catalogTrackIDs, ["a", "b", "c"])
|
||||
XCTAssertEqual(queue.queuedTrackIDs, ["a", "b", "c"])
|
||||
XCTAssertNil(queue.currentTrackID)
|
||||
XCTAssertEqual(queue.nextTrackID(), "a")
|
||||
}
|
||||
|
||||
func testSelectingTrackAndAdvancingUsesQueueOrder() {
|
||||
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
|
||||
|
||||
queue.selectTrack("b")
|
||||
|
||||
XCTAssertEqual(queue.currentTrackID, "b")
|
||||
XCTAssertEqual(queue.nextTrackID(), "c")
|
||||
XCTAssertEqual(queue.previousTrackID(), "a")
|
||||
|
||||
XCTAssertEqual(queue.advanceToNextTrack(), "c")
|
||||
XCTAssertEqual(queue.currentTrackID, "c")
|
||||
}
|
||||
|
||||
func testShuffleCanUsePersistedQueueOrderWhileKeepingSameTrackSet() {
|
||||
var queue = PlaybackQueue(trackIDs: ["a", "b", "c", "d"])
|
||||
queue.selectTrack("c")
|
||||
|
||||
queue.setShuffleEnabled(true, queuedTrackIDs: ["c", "a", "d", "b"])
|
||||
|
||||
XCTAssertTrue(queue.isShuffleEnabled)
|
||||
XCTAssertEqual(queue.currentTrackID, "c")
|
||||
XCTAssertEqual(queue.queuedTrackIDs, ["c", "a", "d", "b"])
|
||||
|
||||
queue.setShuffleEnabled(false)
|
||||
|
||||
XCTAssertFalse(queue.isShuffleEnabled)
|
||||
XCTAssertEqual(queue.queuedTrackIDs, ["a", "b", "c", "d"])
|
||||
XCTAssertEqual(queue.currentTrackID, "c")
|
||||
}
|
||||
|
||||
func testRepeatAllWrapsAroundQueueBoundaries() {
|
||||
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
|
||||
queue.selectTrack("c")
|
||||
queue.setRepeatMode(.all)
|
||||
|
||||
XCTAssertEqual(queue.nextTrackID(), "a")
|
||||
XCTAssertEqual(queue.advanceToNextTrack(), "a")
|
||||
XCTAssertEqual(queue.currentTrackID, "a")
|
||||
XCTAssertEqual(queue.previousTrackID(), "c")
|
||||
}
|
||||
|
||||
func testRepeatOneKeepsCurrentTrackForNextAndPrevious() {
|
||||
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
|
||||
queue.selectTrack("b")
|
||||
queue.setRepeatMode(.one)
|
||||
|
||||
XCTAssertEqual(queue.nextTrackID(), "b")
|
||||
XCTAssertEqual(queue.previousTrackID(), "b")
|
||||
XCTAssertEqual(queue.advanceToNextTrack(), "b")
|
||||
XCTAssertEqual(queue.moveToPreviousTrack(), "b")
|
||||
}
|
||||
|
||||
func testReplacingTrackIDsDropsRemovedTracksFromQueue() {
|
||||
var queue = PlaybackQueue(
|
||||
trackIDs: ["a", "b", "c"],
|
||||
currentTrackID: "b",
|
||||
queuedTrackIDs: ["b", "c", "a"],
|
||||
isShuffleEnabled: true,
|
||||
repeatMode: .all
|
||||
)
|
||||
|
||||
queue.replaceTrackIDs(
|
||||
["a", "c", "d"],
|
||||
currentTrackID: queue.currentTrackID,
|
||||
queuedTrackIDs: queue.queuedTrackIDs
|
||||
)
|
||||
|
||||
XCTAssertEqual(queue.catalogTrackIDs, ["a", "c", "d"])
|
||||
XCTAssertEqual(queue.queuedTrackIDs, ["c", "a", "d"])
|
||||
XCTAssertNil(queue.currentTrackID)
|
||||
XCTAssertEqual(queue.nextTrackID(), "c")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user