Archived
125 lines
4.4 KiB
Swift
125 lines
4.4 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Voice
|
|
|
|
@MainActor
|
|
struct MediaReconcileTests {
|
|
private func makeController() -> MediaPlayerController {
|
|
let config = ServerConfig(
|
|
baseURL: URL(string: "http://127.0.0.1:9")!,
|
|
livekitURL: URL(string: "ws://127.0.0.1:9")!
|
|
)
|
|
let api = MediaAPI(config: config)
|
|
return MediaPlayerController(api: api, settings: SettingsStore())
|
|
}
|
|
|
|
private func makeState(
|
|
desired: String? = "stopped",
|
|
trackID: String? = nil,
|
|
audioURL: String? = nil
|
|
) -> MediaState {
|
|
let track: MediaTrack? = trackID != nil ? MediaTrack(
|
|
id: trackID!,
|
|
kind: nil,
|
|
videoID: nil,
|
|
title: nil,
|
|
channel: nil,
|
|
duration: nil,
|
|
status: nil,
|
|
error: nil,
|
|
audioURL: audioURL,
|
|
artURL: nil
|
|
) : nil
|
|
return MediaState(
|
|
room: "default",
|
|
track: track,
|
|
desired: desired,
|
|
reported: nil,
|
|
seekHint: nil,
|
|
choices: [],
|
|
pending: [],
|
|
version: 1
|
|
)
|
|
}
|
|
|
|
@Test("reconcile updates published desired to playing")
|
|
func reconcileDesiredPlaying() {
|
|
let controller = makeController()
|
|
#expect(controller.desired == "stopped")
|
|
|
|
controller.reconcile(makeState(desired: "playing"))
|
|
#expect(controller.desired == "playing")
|
|
}
|
|
|
|
@Test("reconcile updates published desired to paused")
|
|
func reconcileDesiredPaused() {
|
|
let controller = makeController()
|
|
controller.reconcile(makeState(desired: "playing"))
|
|
#expect(controller.desired == "playing")
|
|
|
|
controller.reconcile(makeState(desired: "paused"))
|
|
#expect(controller.desired == "paused")
|
|
}
|
|
|
|
@Test("reconcile updates published desired to stopped")
|
|
func reconcileDesiredStopped() {
|
|
let controller = makeController()
|
|
controller.reconcile(makeState(desired: "playing"))
|
|
#expect(controller.desired == "playing")
|
|
|
|
controller.reconcile(makeState(desired: "stopped"))
|
|
#expect(controller.desired == "stopped")
|
|
}
|
|
|
|
@Test("reconcile with nil desired defaults to stopped")
|
|
func reconcileNilDesiredDefaultsToStopped() {
|
|
let controller = makeController()
|
|
controller.reconcile(makeState(desired: "playing"))
|
|
#expect(controller.desired == "playing")
|
|
|
|
controller.reconcile(makeState(desired: nil))
|
|
#expect(controller.desired == "stopped")
|
|
}
|
|
|
|
@Test("late audio: desired=playing before audio_url, then audio_url arrives")
|
|
func lateAudioReconciliation() {
|
|
let controller = makeController()
|
|
|
|
// Phase 1: server says playing but the track has no audio_url yet
|
|
// (still downloading). The source is not wired, so play cannot start.
|
|
let stateNoURL = makeState(desired: "playing", trackID: "track-1", audioURL: nil)
|
|
controller.reconcile(stateNoURL)
|
|
#expect(controller.desired == "playing")
|
|
#expect(controller.track?.id == "track-1")
|
|
// Player rate should still be zero because no source was wired.
|
|
#expect(controller.player.rate.isZero)
|
|
|
|
// Phase 2: the same track now has an audio_url. The reconcile path
|
|
// wires the source (wireSource) and then applyDesiredState retries
|
|
// play because desired is still "playing" and rate is still zero.
|
|
let stateWithURL = makeState(desired: "playing", trackID: "track-1", audioURL: "/stream/track-1.mp3")
|
|
controller.reconcile(stateWithURL)
|
|
#expect(controller.desired == "playing")
|
|
// The source should now be wired (loadedAudioPath is set internally).
|
|
// We can't directly observe loadedAudioPath, but we can verify the
|
|
// player has a current item after wireSource ran.
|
|
#expect(controller.player.currentItem != nil)
|
|
}
|
|
|
|
@Test("startDefaultPolling restarts after endConversation keeps scope")
|
|
func pollingRestartsAfterEndConversation() async {
|
|
let controller = makeController()
|
|
controller.startDefaultPolling()
|
|
await controller.endConversation()
|
|
controller.startDefaultPolling()
|
|
try? await Task.sleep(for: .milliseconds(50))
|
|
}
|
|
|
|
@Test("togglePlayPause without a loaded track is a no-op")
|
|
func toggleWithoutTrack() async {
|
|
let controller = makeController()
|
|
await controller.togglePlayPause()
|
|
#expect(controller.track == nil)
|
|
}
|
|
}
|