Files
Amphlify/Packages/AmphlifyKit/Tests/PlaybackTests/PlaybackSessionTests.swift
T

186 lines
6.9 KiB
Swift

import Domain
import Foundation
@testable import Playback
import Testing
actor RecordingEngine: OutputEngine {
nonisolated let events: AsyncStream<OutputEvent>
nonisolated let continuation: AsyncStream<OutputEvent>.Continuation
private(set) var loaded: [QueueItem.ID] = []
private(set) var prepared: [QueueItem.ID?] = []
init() {
(events, continuation) = AsyncStream<OutputEvent>.makeStream()
}
func load(_ item: QueueItem, at _: TimeInterval) {
loaded.append(item.id)
}
func prepare(_ item: QueueItem?) {
prepared.append(item?.id)
}
func play() {}
func pause() {}
func seek(to _: TimeInterval) {}
func stop() {}
}
final class FakeClock: @unchecked Sendable {
private let lock = NSLock()
private var instant = ContinuousClock.now
var now: ContinuousClock.Instant {
lock.withLock { instant }
}
func advance(_ seconds: Double) {
lock.withLock { instant += .seconds(seconds) }
}
}
private func tracks(_ count: Int) -> [Track] {
let source = SourceID()
return (0 ..< count).map {
Track(
id: EntityID(source: source, remote: "\($0)"),
title: "Track \($0)",
albumID: nil,
albumTitle: "",
artistID: nil,
artistName: "",
duration: 200
)
}
}
@Test func queueStartsAtIndexAndAnchorsPosition() async {
let clock = FakeClock()
let session = PlaybackSession(engine: RecordingEngine()) { clock.now }
await session.send(.setQueue(tracks: tracks(5), startIndex: 2, shuffled: false))
clock.advance(10)
let state = await session.snapshot
#expect(state.current?.track.title == "Track 2")
#expect(state.history.count == 2 && state.upcoming.count == 2)
#expect(state.status == .playing)
#expect(abs(state.anchor.position(at: clock.now) - 10) < 0.001)
}
@Test func pauseFreezesAnchor() async {
let clock = FakeClock()
let session = PlaybackSession(engine: RecordingEngine()) { clock.now }
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
clock.advance(30)
await session.send(.pause)
clock.advance(30)
let state = await session.snapshot
#expect(abs(state.anchor.position(at: clock.now) - 30) < 0.001)
}
@Test func previousRestartsAfterThresholdAndGoesBackBefore() async {
let clock = FakeClock()
let session = PlaybackSession(engine: RecordingEngine()) { clock.now }
await session.send(.setQueue(tracks: tracks(3), startIndex: 1, shuffled: false))
clock.advance(10)
await session.send(.previous)
#expect(await session.snapshot.current?.track.title == "Track 1")
await session.send(.previous)
#expect(await session.snapshot.current?.track.title == "Track 0")
}
@Test func queueEditsAddressItemsByID() async {
let session = PlaybackSession(engine: RecordingEngine())
await session.send(.setQueue(tracks: tracks(4), startIndex: 0, shuffled: false))
let upcoming = await session.snapshot.upcoming
await session.send(.move(item: upcoming[2].id, after: nil))
await session.send(.remove(upcoming[0].id))
#expect(await session.snapshot.upcoming.map(\.track.title) == ["Track 3", "Track 2"])
await session.send(.jump(to: upcoming[1].id))
let state = await session.snapshot
#expect(state.current?.track.title == "Track 2" && state.upcoming.isEmpty)
}
@Test func finishedTrackAdvancesAndStopsAtEndWithoutRepeat() async throws {
let engine = RecordingEngine()
let session = PlaybackSession(engine: engine)
let states = await session.states()
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
for expected in [("Track 1", PlaybackStatus.playing), ("Track 0", .paused)] {
let current = try #require(await session.snapshot.current)
engine.continuation.yield(.finished(current.id))
for await state in states where state.current?.id != current.id {
#expect(state.current?.track.title == expected.0 && state.status == expected.1)
break
}
}
}
@Test func unshuffleRestoresOriginalOrder() async {
let session = PlaybackSession(engine: RecordingEngine())
await session.send(.setQueue(tracks: tracks(8), startIndex: 0, shuffled: false))
await session.send(.setShuffle(true))
await session.send(.setShuffle(false))
#expect(await session.snapshot.upcoming.map(\.track.title) == (1 ..< 8).map { "Track \($0)" })
}
@Test func prepareFollowsHeadOfUpcoming() async {
let engine = RecordingEngine()
let session = PlaybackSession(engine: engine)
await session.send(.setQueue(tracks: tracks(3), startIndex: 0, shuffled: false))
var state = await session.snapshot
#expect(await engine.prepared == [state.upcoming[0].id])
await session.send(.next)
state = await session.snapshot
#expect(await engine.prepared.last == state.upcoming[0].id)
await session.send(.next)
#expect(await engine.prepared.last == .some(nil))
}
@Test func anchorEventReanchorsCurrentAndIgnoresStaleID() async throws {
let clock = FakeClock()
let engine = RecordingEngine()
let session = PlaybackSession(engine: engine) { clock.now }
let states = await session.states()
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
let current = try #require(await session.snapshot.current)
engine.continuation.yield(.anchor(UUID(), position: 50, rate: 1))
engine.continuation.yield(.anchor(current.id, position: 70, rate: 0))
for await state in states where state.anchor.rate == 0 {
#expect(state.status == .playing && state.current?.id == current.id)
#expect(abs(state.anchor.position(at: clock.now) - 70) < 0.001)
break
}
}
@Test func failedTrackAdvancesToNext() async throws {
let engine = RecordingEngine()
let session = PlaybackSession(engine: engine)
let states = await session.states()
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
let current = try #require(await session.snapshot.current)
engine.continuation.yield(.failed(current.id))
for await state in states where state.current?.id != current.id {
#expect(state.current?.track.title == "Track 1" && state.status == .playing)
break
}
}
@Test func everyTrackFailingStopsInsteadOfLooping() async throws {
let engine = RecordingEngine()
let session = PlaybackSession(engine: engine)
let states = await session.states()
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
await session.send(.setRepeat(.all))
for _ in 0 ..< 3 {
let current = try #require(await session.snapshot.current)
engine.continuation.yield(.failed(current.id))
for await state in states where state.current?.id != current.id || state.status == .paused {
break
}
}
let state = await session.snapshot
#expect(state.status == .paused && state.anchor.rate == 0)
#expect(await engine.loaded.count == 2)
}