react-native-audio-browser 0.2.0-next.31 → 0.2.0-next.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/AVPlayerEngine.swift +4 -8
- package/ios/Browser/BrowserConfig.swift +0 -5
- package/ios/Browser/BrowserError.swift +16 -0
- package/ios/Browser/BrowserManager.swift +36 -141
- package/ios/Browser/BrowserPathHelper.swift +25 -12
- package/ios/Browser/FavoriteSet.swift +46 -0
- package/ios/Browser/JsonModels.swift +172 -381
- package/ios/Browser/PageResolver.swift +16 -96
- package/ios/Browser/RequestLayer.swift +97 -2
- package/ios/Browser/RequestPipeline.swift +78 -227
- package/ios/Browser/SectionScope.swift +25 -0
- package/ios/Browser/SimpleRouter.swift +73 -144
- package/ios/Extension/SearchParams+Query.swift +28 -0
- package/ios/Extension/Track+Copying.swift +3 -1
- package/ios/Extension/Track+Identity.swift +7 -0
- package/ios/Http/HttpClient.swift +12 -30
- package/ios/HybridAudioBrowser+Browser.swift +5 -21
- package/ios/HybridAudioBrowser+Player.swift +3 -3
- package/ios/Model/NitroTypeStubs.swift +78 -0
- package/ios/Player/AudioSessionController.swift +2 -0
- package/ios/Player/ErrorStatus.swift +25 -2
- package/ios/Player/MediaLoader.swift +23 -71
- package/ios/Player/NowPlayingLines.swift +36 -0
- package/ios/Player/NowPlayingUpdater.swift +27 -85
- package/ios/Player/Player.swift +108 -261
- package/ios/Player/RetryLoop.swift +26 -0
- package/ios/Player/SleepTimerManager.swift +13 -15
- package/ios/Player/VolumeFader.swift +5 -0
- package/ios/Tests/BrowserPathHelperTests.swift +33 -0
- package/ios/Tests/ErrorStatusTests.swift +50 -0
- package/ios/Tests/FavoriteSetTests.swift +60 -0
- package/ios/Tests/JsonModelsTests.swift +30 -0
- package/ios/Tests/MediaLoaderTests.swift +26 -21
- package/ios/Tests/NowPlayingLinesTests.swift +43 -0
- package/ios/Tests/PlayerPolicyTests.swift +19 -3
- package/ios/Tests/PlayerTests.swift +81 -74
- package/ios/Tests/RequestLayerTests.swift +116 -0
- package/ios/Tests/RetryLoopTests.swift +35 -0
- package/ios/Tests/SearchParamsQueryTests.swift +48 -0
- package/ios/Tests/SectionScopeTests.swift +53 -0
- package/ios/Tests/SimpleRouterTests.swift +38 -3
- package/ios/Tests/SleepTimerManagerTests.swift +65 -41
- package/ios/Tests/Support/TestTypes.swift +2 -2
- package/package.json +1 -1
|
@@ -8,14 +8,14 @@ private func makeCoordinator() -> (
|
|
|
8
8
|
player: Player,
|
|
9
9
|
engine: MockPlayerEngine,
|
|
10
10
|
callbacks: MockPlayerCallbacks,
|
|
11
|
-
|
|
11
|
+
clock: ManualSleepTimerScheduler,
|
|
12
12
|
) {
|
|
13
|
-
let
|
|
13
|
+
let clock = ManualSleepTimerScheduler()
|
|
14
14
|
let engine = MockPlayerEngine()
|
|
15
|
-
let player = makePlayer(engine: engine, retries: false,
|
|
15
|
+
let player = makePlayer(engine: engine, retries: false, sleepTimerScheduler: clock)
|
|
16
16
|
let callbacks = MockPlayerCallbacks()
|
|
17
17
|
player.callbacks = callbacks
|
|
18
|
-
return (player, engine, callbacks,
|
|
18
|
+
return (player, engine, callbacks, clock)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/// Helper to load a track into the player's queue so it has a currentTrack.
|
|
@@ -275,7 +275,7 @@ struct ObserverGuardTests {
|
|
|
275
275
|
eh.currentTime = 99.8
|
|
276
276
|
eh.duration = 100
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
279
279
|
|
|
280
280
|
// State should remain .playing — the pause near track end is ignored
|
|
281
281
|
#expect(c.state == .playing)
|
|
@@ -289,7 +289,7 @@ struct ObserverGuardTests {
|
|
|
289
289
|
eh.currentTime = 0
|
|
290
290
|
eh.duration = 0
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
293
293
|
|
|
294
294
|
#expect(c.state == .none)
|
|
295
295
|
}
|
|
@@ -303,7 +303,7 @@ struct ObserverGuardTests {
|
|
|
303
303
|
eh.duration = 100
|
|
304
304
|
c.setPlayWhenReady(false)
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
307
307
|
|
|
308
308
|
#expect(c.state == .paused)
|
|
309
309
|
}
|
|
@@ -317,7 +317,7 @@ struct ObserverGuardTests {
|
|
|
317
317
|
eh.duration = 100
|
|
318
318
|
c.setPlayWhenReady(true)
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
321
321
|
|
|
322
322
|
// playWhenReady is true — pause is ignored (likely buffering/seeking)
|
|
323
323
|
#expect(c.state == .playing)
|
|
@@ -325,11 +325,11 @@ struct ObserverGuardTests {
|
|
|
325
325
|
|
|
326
326
|
@Test @MainActor
|
|
327
327
|
func timeControlStatus_ignored_duringLoading() {
|
|
328
|
-
let (c,
|
|
328
|
+
let (c, eh, cb, _) = makeCoordinator()
|
|
329
329
|
c.transition(.trackLoading)
|
|
330
330
|
cb.playbackChanges.removeAll()
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
333
333
|
|
|
334
334
|
#expect(c.state == .loading)
|
|
335
335
|
#expect(cb.playbackChanges.isEmpty)
|
|
@@ -341,7 +341,7 @@ struct ObserverGuardTests {
|
|
|
341
341
|
c.transition(.avPlayerPlaying)
|
|
342
342
|
eh.hasLoadedAsset = false
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
eh.emit(.timeControlStatus(.waitingToPlayAtSpecifiedRate, waitingForNoItem: false))
|
|
345
345
|
|
|
346
346
|
#expect(c.state == .playing) // unchanged
|
|
347
347
|
}
|
|
@@ -352,18 +352,18 @@ struct ObserverGuardTests {
|
|
|
352
352
|
c.transition(.avPlayerPlaying)
|
|
353
353
|
eh.hasLoadedAsset = true
|
|
354
354
|
|
|
355
|
-
|
|
355
|
+
eh.emit(.timeControlStatus(.waitingToPlayAtSpecifiedRate, waitingForNoItem: false))
|
|
356
356
|
|
|
357
357
|
#expect(c.state == .buffering)
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
@Test @MainActor
|
|
361
361
|
func playbackLikelyToKeepUp_false_ignored() {
|
|
362
|
-
let (c,
|
|
362
|
+
let (c, eh, cb, _) = makeCoordinator()
|
|
363
363
|
c.transition(.trackLoading)
|
|
364
364
|
cb.playbackChanges.removeAll()
|
|
365
365
|
|
|
366
|
-
|
|
366
|
+
eh.emit(.likelyToKeepUp(false))
|
|
367
367
|
|
|
368
368
|
#expect(c.state == .loading)
|
|
369
369
|
#expect(cb.playbackChanges.isEmpty)
|
|
@@ -374,28 +374,29 @@ struct ObserverGuardTests {
|
|
|
374
374
|
let (c, eh, _, _) = makeCoordinator()
|
|
375
375
|
c.transition(.trackLoading)
|
|
376
376
|
eh.hasLoadedAsset = true
|
|
377
|
+
eh.hasItem = true
|
|
377
378
|
|
|
378
|
-
|
|
379
|
+
eh.emit(.likelyToKeepUp(true))
|
|
379
380
|
|
|
380
381
|
#expect(c.state == .ready)
|
|
381
382
|
}
|
|
382
383
|
|
|
383
384
|
@Test @MainActor
|
|
384
385
|
func audioDidStart_ignored_duringLoading() {
|
|
385
|
-
let (c,
|
|
386
|
+
let (c, eh, _, _) = makeCoordinator()
|
|
386
387
|
c.transition(.trackLoading)
|
|
387
388
|
|
|
388
|
-
|
|
389
|
+
eh.emit(.audioDidStart)
|
|
389
390
|
|
|
390
391
|
#expect(c.state == .loading)
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
@Test @MainActor
|
|
394
395
|
func audioDidStart_transitionsToPlaying() {
|
|
395
|
-
let (c,
|
|
396
|
+
let (c, eh, _, _) = makeCoordinator()
|
|
396
397
|
c.transition(.bufferingSufficient)
|
|
397
398
|
|
|
398
|
-
|
|
399
|
+
eh.emit(.audioDidStart)
|
|
399
400
|
|
|
400
401
|
#expect(c.state == .playing)
|
|
401
402
|
}
|
|
@@ -604,7 +605,7 @@ struct HandleTrackDidPlayToEndTimeTests {
|
|
|
604
605
|
c.repeatMode = .track
|
|
605
606
|
eh.startPlaybackCallCount = 0
|
|
606
607
|
|
|
607
|
-
|
|
608
|
+
eh.emit(.playedToEnd)
|
|
608
609
|
|
|
609
610
|
#expect(eh.seekCalls == [0])
|
|
610
611
|
#expect(eh.startPlaybackCallCount == 1)
|
|
@@ -622,7 +623,7 @@ struct HandleTrackDidPlayToEndTimeTests {
|
|
|
622
623
|
c.repeatMode = .track
|
|
623
624
|
c.setPlayWhenReady(false)
|
|
624
625
|
|
|
625
|
-
|
|
626
|
+
eh.emit(.playedToEnd)
|
|
626
627
|
|
|
627
628
|
#expect(cb.queueEndedEvents.isEmpty)
|
|
628
629
|
#expect(eh.seekCalls.isEmpty)
|
|
@@ -635,7 +636,7 @@ struct HandleTrackDidPlayToEndTimeTests {
|
|
|
635
636
|
loadTrack(c)
|
|
636
637
|
c.repeatMode = .off
|
|
637
638
|
|
|
638
|
-
|
|
639
|
+
eh.emit(.playedToEnd)
|
|
639
640
|
|
|
640
641
|
#expect(c.state == .ended)
|
|
641
642
|
#expect(cb.queueEndedEvents.count >= 1)
|
|
@@ -643,7 +644,7 @@ struct HandleTrackDidPlayToEndTimeTests {
|
|
|
643
644
|
|
|
644
645
|
@Test @MainActor
|
|
645
646
|
func midQueue_advancesToNext() {
|
|
646
|
-
let (c,
|
|
647
|
+
let (c, eh, cb, _) = makeCoordinator()
|
|
647
648
|
let tracks = [
|
|
648
649
|
Track(id: "t1", src: "https://example.com/1.mp3", title: "Track 1"),
|
|
649
650
|
Track(id: "t2", src: "https://example.com/2.mp3", title: "Track 2"),
|
|
@@ -651,9 +652,9 @@ struct HandleTrackDidPlayToEndTimeTests {
|
|
|
651
652
|
c.setQueue(tracks)
|
|
652
653
|
cb.activeTrackChanges.removeAll()
|
|
653
654
|
|
|
654
|
-
|
|
655
|
+
eh.emit(.playedToEnd)
|
|
655
656
|
|
|
656
|
-
#expect(c.currentIndex == 1)
|
|
657
|
+
#expect(c.queue.currentIndex == 1)
|
|
657
658
|
#expect(cb.activeTrackChanges.count == 1)
|
|
658
659
|
#expect(cb.activeTrackChanges.first?.track?.id == "t2")
|
|
659
660
|
}
|
|
@@ -672,7 +673,7 @@ struct NaturalEndPlayIntentTests {
|
|
|
672
673
|
let (c, eh, _, _) = makeCoordinator()
|
|
673
674
|
startPlaying(c, eh)
|
|
674
675
|
|
|
675
|
-
|
|
676
|
+
eh.emit(.playedToEnd)
|
|
676
677
|
|
|
677
678
|
#expect(c.state == .ended)
|
|
678
679
|
#expect(c.playWhenReady == false)
|
|
@@ -684,7 +685,7 @@ struct NaturalEndPlayIntentTests {
|
|
|
684
685
|
startPlaying(c, eh)
|
|
685
686
|
callbacks.releaseSessionCount = 0
|
|
686
687
|
|
|
687
|
-
|
|
688
|
+
eh.emit(.playedToEnd)
|
|
688
689
|
|
|
689
690
|
#expect(callbacks.releaseSessionCount == 1)
|
|
690
691
|
}
|
|
@@ -695,7 +696,7 @@ struct NaturalEndPlayIntentTests {
|
|
|
695
696
|
func playAfterNaturalEnd_reloadsFromStart() {
|
|
696
697
|
let (c, eh, _, _) = makeCoordinator()
|
|
697
698
|
startPlaying(c, eh)
|
|
698
|
-
|
|
699
|
+
eh.emit(.playedToEnd)
|
|
699
700
|
eh.resumePosition = 300
|
|
700
701
|
let resolvesBefore = eh.resolveAndLoadCalls.count
|
|
701
702
|
|
|
@@ -712,10 +713,10 @@ struct NaturalEndPlayIntentTests {
|
|
|
712
713
|
func strayPauseAfterNaturalEnd_staysEnded() {
|
|
713
714
|
let (c, eh, _, _) = makeCoordinator()
|
|
714
715
|
startPlaying(c, eh)
|
|
715
|
-
|
|
716
|
+
eh.emit(.playedToEnd)
|
|
716
717
|
#expect(c.state == .ended)
|
|
717
718
|
|
|
718
|
-
|
|
719
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
719
720
|
|
|
720
721
|
#expect(c.state == .ended)
|
|
721
722
|
}
|
|
@@ -724,7 +725,7 @@ struct NaturalEndPlayIntentTests {
|
|
|
724
725
|
func interruptionAfterNaturalEnd_doesNotArmResume() {
|
|
725
726
|
let (c, eh, _, _) = makeCoordinator()
|
|
726
727
|
startPlaying(c, eh)
|
|
727
|
-
|
|
728
|
+
eh.emit(.playedToEnd)
|
|
728
729
|
let resolvesBefore = eh.resolveAndLoadCalls.count
|
|
729
730
|
|
|
730
731
|
c.handleInterruptionBegan()
|
|
@@ -760,10 +761,10 @@ struct HealthyPlaybackRetryResetTests {
|
|
|
760
761
|
func sustainedPlayback_resetsRetryBudget() async {
|
|
761
762
|
let (player, retry, _) = await makeAfterOneRetry()
|
|
762
763
|
#expect(retry.attempts == 1)
|
|
763
|
-
|
|
764
|
+
retry.healthyPlaybackDuration = .milliseconds(10)
|
|
764
765
|
|
|
765
766
|
player.transition(.avPlayerPlaying)
|
|
766
|
-
await
|
|
767
|
+
await retry.healthyPlaybackTask?.value
|
|
767
768
|
|
|
768
769
|
#expect(retry.attempts == 0)
|
|
769
770
|
}
|
|
@@ -773,11 +774,11 @@ struct HealthyPlaybackRetryResetTests {
|
|
|
773
774
|
@Test @MainActor
|
|
774
775
|
func refill_skipsWhileRetryIsPending() async {
|
|
775
776
|
let (player, retry, _) = await makeAfterOneRetry(delay: 1)
|
|
776
|
-
|
|
777
|
+
retry.healthyPlaybackDuration = .milliseconds(10)
|
|
777
778
|
|
|
778
779
|
player.transition(.avPlayerPlaying)
|
|
779
780
|
retry.handleError(URLError(.timedOut), context: .playback)
|
|
780
|
-
await
|
|
781
|
+
await retry.healthyPlaybackTask?.value
|
|
781
782
|
|
|
782
783
|
#expect(retry.pendingTask != nil)
|
|
783
784
|
retry.cancel()
|
|
@@ -787,10 +788,10 @@ struct HealthyPlaybackRetryResetTests {
|
|
|
787
788
|
func playbackInterruptedBeforeThreshold_doesNotReset() async {
|
|
788
789
|
let (player, retry, engine) = await makeAfterOneRetry()
|
|
789
790
|
engine.hasLoadedAsset = true
|
|
790
|
-
|
|
791
|
+
retry.healthyPlaybackDuration = .milliseconds(500)
|
|
791
792
|
|
|
792
793
|
player.transition(.avPlayerPlaying)
|
|
793
|
-
let refill =
|
|
794
|
+
let refill = retry.healthyPlaybackTask
|
|
794
795
|
player.transition(.avPlayerWaiting) // stalls before the threshold
|
|
795
796
|
|
|
796
797
|
// Leaving .playing cancels the refill; awaiting the cancelled task is the
|
|
@@ -805,44 +806,54 @@ struct HealthyPlaybackRetryResetTests {
|
|
|
805
806
|
|
|
806
807
|
@Suite("Player - sleep timer fade")
|
|
807
808
|
struct SleepTimerFadeTests {
|
|
809
|
+
/// A 20 s timer whose 10 s fade has begun.
|
|
810
|
+
@MainActor
|
|
811
|
+
private func startFade(_ c: Player, _ clock: ManualSleepTimerScheduler) {
|
|
812
|
+
c.sleepTimerManager.set(seconds: 20, fadeDuration: 10)
|
|
813
|
+
clock.advance(to: 10)
|
|
814
|
+
}
|
|
815
|
+
|
|
808
816
|
@Test @MainActor
|
|
809
817
|
func fadeStart_capturesPreFadeVolume() {
|
|
810
|
-
let (c, eh, _,
|
|
818
|
+
let (c, eh, _, clock) = makeCoordinator()
|
|
811
819
|
eh.volume = 0.8
|
|
812
820
|
|
|
813
|
-
|
|
821
|
+
startFade(c, clock)
|
|
822
|
+
eh.volume = 0.1 // mid-fade
|
|
814
823
|
|
|
815
|
-
#expect(c.
|
|
816
|
-
|
|
824
|
+
#expect(c.sleepTimerManager.isFading)
|
|
825
|
+
c.sleepTimerManager.clear()
|
|
826
|
+
#expect(eh.volume == 0.8)
|
|
817
827
|
}
|
|
818
828
|
|
|
819
829
|
@Test @MainActor
|
|
820
830
|
func completion_pausesThenRestoresPreFadeVolume() {
|
|
821
|
-
let (c, eh, _,
|
|
831
|
+
let (c, eh, _, clock) = makeCoordinator()
|
|
822
832
|
loadTrack(c)
|
|
823
833
|
c.setPlayWhenReady(true)
|
|
824
834
|
c.transition(.bufferingSufficient) // leave .loading so pause reaches the player
|
|
825
835
|
eh.pausePlaybackCallCount = 0
|
|
826
836
|
|
|
827
|
-
|
|
837
|
+
startFade(c, clock)
|
|
828
838
|
eh.volume = 0.2 // mid-fade
|
|
829
839
|
|
|
830
|
-
|
|
840
|
+
clock.advance(to: 20)
|
|
831
841
|
|
|
832
842
|
#expect(c.playWhenReady == false)
|
|
833
843
|
#expect(eh.pausePlaybackCallCount == 1)
|
|
834
844
|
#expect(eh.volume == 1.0)
|
|
835
|
-
#expect(!c.
|
|
845
|
+
#expect(!c.sleepTimerManager.isFading)
|
|
836
846
|
}
|
|
837
847
|
|
|
838
848
|
@Test @MainActor
|
|
839
849
|
func completion_withoutFade_justPauses() {
|
|
840
|
-
let (c, eh, _,
|
|
850
|
+
let (c, eh, _, clock) = makeCoordinator()
|
|
841
851
|
loadTrack(c)
|
|
842
852
|
c.setPlayWhenReady(true)
|
|
843
853
|
eh.volume = 0.8
|
|
854
|
+
c.sleepTimerManager.set(seconds: 20)
|
|
844
855
|
|
|
845
|
-
|
|
856
|
+
clock.advance(to: 20)
|
|
846
857
|
|
|
847
858
|
#expect(c.playWhenReady == false)
|
|
848
859
|
#expect(eh.volume == 0.8) // untouched — no fade ran
|
|
@@ -850,41 +861,40 @@ struct SleepTimerFadeTests {
|
|
|
850
861
|
|
|
851
862
|
@Test @MainActor
|
|
852
863
|
func fadeCancel_restoresPreFadeVolume() {
|
|
853
|
-
let (c, eh, _,
|
|
864
|
+
let (c, eh, _, clock) = makeCoordinator()
|
|
854
865
|
eh.volume = 0.8
|
|
855
866
|
|
|
856
|
-
|
|
867
|
+
startFade(c, clock)
|
|
857
868
|
eh.volume = 0.1 // mid-fade
|
|
858
869
|
|
|
859
|
-
|
|
870
|
+
c.sleepTimerManager.set(seconds: 60)
|
|
860
871
|
|
|
861
872
|
#expect(eh.volume == 0.8)
|
|
862
|
-
#expect(!c.
|
|
873
|
+
#expect(!c.sleepTimerManager.isFading)
|
|
863
874
|
}
|
|
864
875
|
|
|
865
876
|
@Test @MainActor
|
|
866
877
|
func pauseDuringFade_clearsSleepTimer() {
|
|
867
|
-
let (c, _, _,
|
|
878
|
+
let (c, _, _, clock) = makeCoordinator()
|
|
868
879
|
loadTrack(c)
|
|
869
880
|
c.setPlayWhenReady(true)
|
|
870
|
-
st.set(seconds: 60, fadeDuration: 10)
|
|
871
881
|
|
|
872
|
-
|
|
882
|
+
startFade(c, clock)
|
|
873
883
|
c.pause()
|
|
874
884
|
|
|
875
|
-
#expect(
|
|
885
|
+
#expect(c.sleepTimerManager.get() == nil)
|
|
876
886
|
}
|
|
877
887
|
|
|
878
888
|
@Test @MainActor
|
|
879
889
|
func pauseWithoutFade_leavesSleepTimerAlone() {
|
|
880
|
-
let (c, _, _,
|
|
890
|
+
let (c, _, _, _) = makeCoordinator()
|
|
881
891
|
loadTrack(c)
|
|
882
892
|
c.setPlayWhenReady(true)
|
|
883
|
-
|
|
893
|
+
c.sleepTimerManager.set(seconds: 60, fadeDuration: 10)
|
|
884
894
|
|
|
885
895
|
c.pause()
|
|
886
896
|
|
|
887
|
-
#expect(
|
|
897
|
+
#expect(c.sleepTimerManager.get() != nil)
|
|
888
898
|
}
|
|
889
899
|
}
|
|
890
900
|
|
|
@@ -922,7 +932,7 @@ struct InterruptionTests {
|
|
|
922
932
|
let (c, eh, _, _) = makeCoordinator()
|
|
923
933
|
startPlaying(c, eh)
|
|
924
934
|
|
|
925
|
-
|
|
935
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
926
936
|
#expect(c.state == .playing) // swallowed — the symptom this fix targets
|
|
927
937
|
|
|
928
938
|
c.handleInterruptionBegan()
|
|
@@ -1041,7 +1051,7 @@ struct InterruptionTests {
|
|
|
1041
1051
|
startPlaying(c, eh)
|
|
1042
1052
|
// Pause before the interruption (e.g. user already paused).
|
|
1043
1053
|
c.setPlayWhenReady(false)
|
|
1044
|
-
|
|
1054
|
+
eh.emit(.timeControlStatus(.paused, waitingForNoItem: false))
|
|
1045
1055
|
#expect(c.state == .paused)
|
|
1046
1056
|
|
|
1047
1057
|
c.handleInterruptionBegan()
|
|
@@ -1144,10 +1154,10 @@ struct LoadTests {
|
|
|
1144
1154
|
|
|
1145
1155
|
@Test @MainActor
|
|
1146
1156
|
func aNewTrackStartsAnUnprovenLoad() {
|
|
1147
|
-
let (c,
|
|
1157
|
+
let (c, eh, _, _) = makeCoordinator()
|
|
1148
1158
|
c.setQueue([track("a"), track("b")])
|
|
1149
1159
|
let first = c.load
|
|
1150
|
-
|
|
1160
|
+
eh.emit(.audioDidStart)
|
|
1151
1161
|
#expect(c.load?.hasPlayed == true)
|
|
1152
1162
|
|
|
1153
1163
|
c.next()
|
|
@@ -1160,9 +1170,9 @@ struct LoadTests {
|
|
|
1160
1170
|
/// recovery budget, must survive it.
|
|
1161
1171
|
@Test @MainActor
|
|
1162
1172
|
func aRetryResetKeepsTheLoad() {
|
|
1163
|
-
let (c,
|
|
1173
|
+
let (c, eh, _, _) = makeCoordinator()
|
|
1164
1174
|
c.setQueue([track("a")])
|
|
1165
|
-
|
|
1175
|
+
eh.emit(.audioDidStart)
|
|
1166
1176
|
let load = c.load
|
|
1167
1177
|
|
|
1168
1178
|
c.retry.reset()
|
|
@@ -1171,16 +1181,13 @@ struct LoadTests {
|
|
|
1171
1181
|
#expect(c.load?.hasPlayed == true)
|
|
1172
1182
|
}
|
|
1173
1183
|
|
|
1174
|
-
@Test
|
|
1175
|
-
func
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
#expect(
|
|
1180
|
-
|
|
1181
|
-
c.beginLoad()
|
|
1182
|
-
|
|
1183
|
-
#expect(c.claimNowPlayingPrime())
|
|
1184
|
+
@Test
|
|
1185
|
+
func theNowPlayingPrimeIsClaimedOnce() {
|
|
1186
|
+
var load = Player.Load(id: 1)
|
|
1187
|
+
let first = load.claimNowPlayingPrime()
|
|
1188
|
+
let second = load.claimNowPlayingPrime()
|
|
1189
|
+
#expect(first)
|
|
1190
|
+
#expect(!second)
|
|
1184
1191
|
}
|
|
1185
1192
|
}
|
|
1186
1193
|
|
|
@@ -109,4 +109,120 @@ struct RequestLayerTests {
|
|
|
109
109
|
#expect(RequestLayer.mergeDicts(["a": "1"], nil) == ["a": "1"])
|
|
110
110
|
#expect(RequestLayer.mergeDicts(nil, ["b": "2"]) == ["b": "2"])
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
@Test func baseSetsOnlyThePath() {
|
|
114
|
+
#expect(Config.base(path: "/a") == Config(path: "/a"))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MARK: - artwork
|
|
118
|
+
|
|
119
|
+
private func artwork(
|
|
120
|
+
_ artwork: Config = Config(),
|
|
121
|
+
over base: Config = Config(path: "/art.png"),
|
|
122
|
+
resolve: (nonisolated(nonsending)() async throws -> Config?)? = nil,
|
|
123
|
+
trackHasArtwork: Bool = true,
|
|
124
|
+
sizeQuery: [String: String] = [:],
|
|
125
|
+
transforms: [nonisolated(nonsending)(Config) async throws -> Config] = [],
|
|
126
|
+
id: String? = nil,
|
|
127
|
+
) async throws -> Config? {
|
|
128
|
+
try await RequestLayer.artwork(
|
|
129
|
+
artwork, over: base, resolve: resolve, trackHasArtwork: trackHasArtwork,
|
|
130
|
+
sizeQuery: sizeQuery, transforms: transforms, id: id,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Test func artworkStaticFieldsMergeOverTheBasePathIncluded() async throws {
|
|
135
|
+
let result = try await artwork(
|
|
136
|
+
Config(path: "/artwork/{id}", baseUrl: "https://img", headers: ["B": "2"]),
|
|
137
|
+
over: Config(path: "/art.png", baseUrl: "https://api", headers: ["A": "1"]),
|
|
138
|
+
id: "42",
|
|
139
|
+
)
|
|
140
|
+
#expect(result == Config(path: "/artwork/42", baseUrl: "https://img", headers: ["A": "1", "B": "2"]))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Test func theResolvedConfigWinsOverTheStaticFields() async throws {
|
|
144
|
+
let result = try await artwork(
|
|
145
|
+
Config(baseUrl: "https://static", userAgent: "static"),
|
|
146
|
+
resolve: { Config(baseUrl: "https://resolved") },
|
|
147
|
+
)
|
|
148
|
+
#expect(result == Config(path: "/art.png", baseUrl: "https://resolved", userAgent: "static"))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Test func aResolverThatProducesNothingKeepsTheTracksArtwork() async throws {
|
|
152
|
+
let result = try await artwork(Config(baseUrl: "https://img"), resolve: { nil })
|
|
153
|
+
#expect(result == Config(path: "/art.png", baseUrl: "https://img"))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test func aResolverThatProducesNothingForATrackWithoutArtworkIsNoArtwork() async throws {
|
|
157
|
+
let result = try await artwork(
|
|
158
|
+
Config(path: "/fallback.png"), over: Config(), resolve: { nil }, trackHasArtwork: false,
|
|
159
|
+
)
|
|
160
|
+
#expect(result == nil)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Test func theSizeQueryMergesBeforeTheTransformsRun() async throws {
|
|
164
|
+
var seen: Config?
|
|
165
|
+
let result = try await artwork(
|
|
166
|
+
over: Config(path: "/art.png", query: ["v": "1"]),
|
|
167
|
+
sizeQuery: ["w": "300"],
|
|
168
|
+
transforms: [{ seen = $0; return $0 }],
|
|
169
|
+
)
|
|
170
|
+
#expect(seen?.query == ["v": "1", "w": "300"])
|
|
171
|
+
#expect(result?.query == ["v": "1", "w": "300"])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Test func anEmptySizeQueryLeavesNoQuery() async throws {
|
|
175
|
+
#expect(try await artwork()?.query == nil)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test func transformsRunInOrderBeforeTheIdIsFilled() async throws {
|
|
179
|
+
let result = try await artwork(
|
|
180
|
+
transforms: [
|
|
181
|
+
{ Config(path: "/t/{id}", baseUrl: $0.baseUrl) },
|
|
182
|
+
{ Config(path: $0.path, baseUrl: "https://second", headers: ["X-Id": "{id}"], query: ["id": "{id}"]) },
|
|
183
|
+
],
|
|
184
|
+
id: "7",
|
|
185
|
+
)
|
|
186
|
+
#expect(result == Config(path: "/t/7", baseUrl: "https://second", headers: ["X-Id": "7"], query: ["id": "7"]))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Test func withoutAnIdTheTokenStays() async throws {
|
|
190
|
+
let result = try await artwork(Config(path: "/artwork/{id}"), id: nil)
|
|
191
|
+
#expect(result?.path == "/artwork/{id}")
|
|
192
|
+
#expect(RequestLayer.hasUnfilledIdToken(result!))
|
|
193
|
+
#expect(!RequestLayer.hasUnfilledIdToken(Config(path: "/artwork/1")))
|
|
194
|
+
#expect(RequestLayer.hasUnfilledIdToken(Config(headers: ["X": "{id}"])))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Test func noPathLeftIsNoArtwork() async throws {
|
|
198
|
+
#expect(try await artwork(over: Config(baseUrl: "https://img")) == nil)
|
|
199
|
+
#expect(try await artwork(transforms: [{ _ in Config(baseUrl: "https://img") }]) == nil)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@Test func sizeQueryNamesTheContextsWholePixels() {
|
|
203
|
+
let params = ImageQueryParams(width: "w", height: "h")
|
|
204
|
+
#expect(RequestLayer.sizeQuery(params, ImageContext(width: 300.7, height: 200)) == ["w": "300", "h": "200"])
|
|
205
|
+
#expect(RequestLayer.sizeQuery(ImageQueryParams(width: "w"), ImageContext(width: 300, height: 200)) == ["w": "300"])
|
|
206
|
+
#expect(RequestLayer.sizeQuery(params, ImageContext(width: nil, height: 200)) == ["h": "200"])
|
|
207
|
+
#expect(RequestLayer.sizeQuery(params, nil) == [:])
|
|
208
|
+
#expect(RequestLayer.sizeQuery(nil, ImageContext(width: 300, height: 200)) == [:])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Test func imageHeadersAddTheUserAgentUnlessAHeaderSetsIt() {
|
|
212
|
+
#expect(RequestLayer.imageHeaders(Config()) == nil)
|
|
213
|
+
#expect(RequestLayer.imageHeaders(Config(userAgent: "UA")) == ["User-Agent": "UA"])
|
|
214
|
+
#expect(RequestLayer.imageHeaders(Config(headers: ["User-Agent": "mine"], userAgent: "UA")) == ["User-Agent": "mine"])
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@Test func imageHeadersAddTheContentTypeUnlessAHeaderSetsIt() {
|
|
218
|
+
#expect(RequestLayer.imageHeaders(Config(contentType: "image/png")) == ["Content-Type": "image/png"])
|
|
219
|
+
#expect(
|
|
220
|
+
RequestLayer.imageHeaders(Config(headers: ["Content-Type": "mine"], contentType: "image/png"))
|
|
221
|
+
== ["Content-Type": "mine"],
|
|
222
|
+
)
|
|
223
|
+
#expect(
|
|
224
|
+
RequestLayer.imageHeaders(Config(contentType: "image/png", userAgent: "UA"))
|
|
225
|
+
== ["Content-Type": "image/png", "User-Agent": "UA"],
|
|
226
|
+
)
|
|
227
|
+
}
|
|
112
228
|
}
|
|
@@ -268,3 +268,38 @@ struct RetryLoopHandleErrorTests {
|
|
|
268
268
|
private final class LoopHolder {
|
|
269
269
|
weak var loop: RetryLoop?
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
@Suite("RetryLoop healthy playback")
|
|
273
|
+
@MainActor
|
|
274
|
+
struct RetryLoopHealthyPlaybackTests {
|
|
275
|
+
private func makeAfterOneRetry() async -> RetryLoop {
|
|
276
|
+
let loop = makeRetryLoop()
|
|
277
|
+
loop.handleError(URLError(.timedOut), context: .playback)
|
|
278
|
+
await loop.pendingTask?.value
|
|
279
|
+
return loop
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@Test func audiblePlaybackForTheWindowRefillsTheBudgets() async {
|
|
283
|
+
let loop = await makeAfterOneRetry()
|
|
284
|
+
#expect(loop.attempts == 1)
|
|
285
|
+
loop.healthyPlaybackDuration = .milliseconds(10)
|
|
286
|
+
|
|
287
|
+
loop.playbackBecameAudible()
|
|
288
|
+
await loop.healthyPlaybackTask?.value
|
|
289
|
+
|
|
290
|
+
#expect(loop.attempts == 0)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@Test func stoppingBeforeTheWindowKeepsTheBudgets() async {
|
|
294
|
+
let loop = await makeAfterOneRetry()
|
|
295
|
+
loop.healthyPlaybackDuration = .milliseconds(500)
|
|
296
|
+
|
|
297
|
+
loop.playbackBecameAudible()
|
|
298
|
+
let refill = loop.healthyPlaybackTask
|
|
299
|
+
loop.playbackStoppedBeingAudible()
|
|
300
|
+
await refill?.value
|
|
301
|
+
|
|
302
|
+
#expect(loop.attempts == 1)
|
|
303
|
+
#expect(loop.healthyPlaybackTask == nil)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
|
|
3
|
+
@testable import AudioBrowserTestable
|
|
4
|
+
|
|
5
|
+
struct SearchParamsQueryTests {
|
|
6
|
+
private func params(
|
|
7
|
+
_ query: String,
|
|
8
|
+
mode: SearchMode? = nil,
|
|
9
|
+
genre: String? = nil,
|
|
10
|
+
artist: String? = nil,
|
|
11
|
+
album: String? = nil,
|
|
12
|
+
title: String? = nil,
|
|
13
|
+
playlist: String? = nil,
|
|
14
|
+
reference: MediaReference = .unknown,
|
|
15
|
+
) -> SearchParams {
|
|
16
|
+
SearchParams(
|
|
17
|
+
mode: mode, query: query, genre: genre, artist: artist, album: album,
|
|
18
|
+
title: title, playlist: playlist, reference: reference,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test func aPlainSearchSendsOnlyQ() {
|
|
23
|
+
#expect(params("jazz").queryParams == ["q": "jazz"])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test func everySetFilterIsSent() {
|
|
27
|
+
let all = params(
|
|
28
|
+
"x", mode: .musicVideo, genre: "g", artist: "a", album: "al", title: "t", playlist: "p", reference: .my,
|
|
29
|
+
)
|
|
30
|
+
#expect(all.queryParams == [
|
|
31
|
+
"q": "x", "mode": "music-video", "genre": "g", "artist": "a", "album": "al",
|
|
32
|
+
"title": "t", "playlist": "p", "reference": "my",
|
|
33
|
+
])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test func anUnknownReferenceIsNotSent() {
|
|
37
|
+
#expect(params("x", reference: .unknown).queryParams["reference"] == nil)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test func theCacheKeyIsSortedAndDelimited() {
|
|
41
|
+
#expect(params("jazz", genre: "jazz").cacheKey == "genre=jazz\u{1}q=jazz")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test func searchesWithTheSameQueryButDifferentFiltersHaveDifferentKeys() {
|
|
45
|
+
#expect(params("jazz").cacheKey != params("jazz", genre: "jazz").cacheKey)
|
|
46
|
+
#expect(params("a", artist: "b").cacheKey != params("a", album: "b").cacheKey)
|
|
47
|
+
}
|
|
48
|
+
}
|