react-native-audio-browser 0.2.0-next.28 → 0.2.0-next.29
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/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +3 -4
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +2 -2
- package/ios/AVPlayerEngine.swift +334 -0
- package/ios/AVPlayerEngineCallbacks.swift +15 -0
- package/ios/Browser/BrowseGate.swift +58 -0
- package/ios/Browser/BrowserConfig.swift +31 -89
- package/ios/Browser/BrowserManager.swift +68 -487
- package/ios/Browser/NavigationErrorState.swift +41 -0
- package/ios/Browser/PageResolver.swift +227 -0
- package/ios/Browser/RequestLayer.swift +90 -0
- package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +175 -107
- package/ios/CarPlay/CPInterfaceController+Safe.swift +11 -24
- package/ios/CarPlay/CarPlayArtworkResolver.swift +35 -83
- package/ios/CarPlay/CarPlayController.swift +42 -1442
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -83
- package/ios/CarPlay/CarPlayListItemFactory.swift +9 -23
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +119 -129
- package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
- package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
- package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
- package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
- package/ios/CarPlay/CarPlaySession.swift +209 -0
- package/ios/CarPlay/CarPlayTemplates.swift +63 -0
- package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
- package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +2 -18
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +5 -21
- package/ios/CarPlay/RNABMediaIntentHandler.swift +2 -3
- package/ios/Extension/NavigationError+Conversion.swift +7 -4
- package/ios/Extension/UIImage+Adaptive.swift +12 -0
- package/ios/Extension/URLError+Connectivity.swift +15 -0
- package/ios/Http/HttpClient.swift +63 -145
- package/ios/HybridAudioBrowser+Browser.swift +153 -0
- package/ios/HybridAudioBrowser+Options.swift +144 -0
- package/ios/HybridAudioBrowser+Player.swift +397 -0
- package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
- package/ios/HybridAudioBrowser+Stubs.swift +46 -0
- package/ios/HybridAudioBrowser.swift +74 -1687
- package/ios/Model/NitroTypeStubs.swift +18 -1
- package/ios/Model/PlayerUpdateOptions.swift +1 -1
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
- package/ios/Observer/ItemObserver.swift +90 -0
- package/ios/Observer/PlayerObserver.swift +59 -0
- package/ios/Option/SessionCategories.swift +41 -92
- package/ios/Player/AudioSessionController.swift +177 -0
- package/ios/Player/DeferredSeek.swift +70 -0
- package/ios/Player/ErrorStatus.swift +27 -0
- package/ios/Player/MediaLoader.swift +24 -11
- package/ios/Player/NowPlayingUpdater.swift +157 -58
- package/ios/Player/PlaybackFailure.swift +125 -0
- package/ios/Player/PlaybackPersistence.swift +73 -0
- package/ios/Player/Player.swift +1083 -0
- package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
- package/ios/Player/PlayerEngine.swift +89 -0
- package/ios/Player/PlayerStatusTypes.swift +2 -6
- package/ios/Player/QueueManager.swift +52 -59
- package/ios/Player/RetryLoop.swift +261 -0
- package/ios/Player/SleepTimerManager.swift +1 -1
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +2 -3
- package/ios/RemoteCommand/RemoteCommandController.swift +85 -367
- package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -356
- package/ios/Tests/DeferredSeekTests.swift +70 -0
- package/ios/Tests/EmitterTests.swift +29 -0
- package/ios/Tests/GenerationMemoTests.swift +121 -0
- package/ios/Tests/LRUCacheTests.swift +4 -5
- package/ios/Tests/MediaLoaderTests.swift +12 -33
- package/ios/Tests/ObserverTests.swift +88 -0
- package/ios/Tests/PlaybackErrorTests.swift +76 -0
- package/ios/Tests/PlaybackFailureTests.swift +189 -0
- package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
- package/ios/Tests/PlayerPolicyTests.swift +405 -0
- package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +298 -248
- package/ios/Tests/PlayingStateTests.swift +42 -0
- package/ios/Tests/QueueManagerTests.swift +76 -107
- package/ios/Tests/ReadyValueTests.swift +73 -0
- package/ios/Tests/RemoteCommandControllerTests.swift +10 -30
- package/ios/Tests/RemoteCommandTests.swift +2 -4
- package/ios/Tests/RequestLayerTests.swift +112 -0
- package/ios/Tests/RetryLoopTests.swift +270 -0
- package/ios/Tests/Support/TestTypes.swift +111 -112
- package/ios/Tests/TrackIdTemplateTests.swift +1 -1
- package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
- package/ios/TrackSelector.swift +1 -1
- package/ios/Util/ArtworkImageFetcher.swift +42 -0
- package/ios/Util/Emitter.swift +42 -0
- package/ios/Util/GenerationMemo.swift +36 -0
- package/ios/Util/LRUCache.swift +23 -162
- package/ios/Util/NetworkMonitor.swift +1 -1
- package/ios/Util/ReadyValue.swift +56 -0
- package/ios/Util/SFSymbolRenderer.swift +35 -29
- package/ios/Util/SVGProcessor.swift +0 -10
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/typescript/src/types/browser.d.ts +3 -3
- package/package.json +1 -1
- package/src/types/browser.ts +3 -3
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
- package/ios/Bridge.h +0 -8
- package/ios/Browser/MediaResolveComposer.swift +0 -114
- package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
- package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
- package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
- package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
- package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
- package/ios/Observer/PlayerStateObserver.swift +0 -50
- package/ios/Observer/PlayerTimeObserver.swift +0 -55
- package/ios/Option/PitchAlgorithms.swift +0 -17
- package/ios/Option/TimeEventFrequency.swift +0 -18
- package/ios/Player/LoadSeekCoordinator.swift +0 -99
- package/ios/Player/MediaLoaderDelegate.swift +0 -11
- package/ios/Player/PlaybackCoordinator.swift +0 -761
- package/ios/Player/PlaybackEffectHandler.swift +0 -54
- package/ios/Player/PlaybackErrorHandler.swift +0 -175
- package/ios/Player/PlaybackStateStore.swift +0 -27
- package/ios/Player/PlayingStateManager.swift +0 -33
- package/ios/Player/RetryManager.swift +0 -384
- package/ios/Player/SeekCompletionHandler.swift +0 -5
- package/ios/Player/SleepTimerHandling.swift +0 -28
- package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
- package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -327
- package/ios/Tests/MediaResolveTests.swift +0 -126
- package/ios/Tests/OnceValueTests.swift +0 -79
- package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -549
- package/ios/Tests/PlaybackStateStoreTests.swift +0 -49
- package/ios/Tests/PlayerTimeObserverTests.swift +0 -37
- package/ios/Tests/PlayingStateManagerTests.swift +0 -57
- package/ios/Tests/RetryManagerTests.swift +0 -178
- package/ios/TrackPlayer.swift +0 -1042
- package/ios/TrackPlayerCallbacks.swift +0 -53
- package/ios/Util/MetadataAdapter.swift +0 -131
- package/ios/Util/OnceValue.swift +0 -104
- /package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +0 -0
- /package/ios/{Browser → CarPlay}/SectionPresentation.swift +0 -0
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// Protocol abstracting sleep timer for testability.
|
|
4
|
-
/// PlaybackCoordinator uses this instead of SleepTimerManager directly.
|
|
5
|
-
@MainActor protocol SleepTimerHandling: AnyObject {
|
|
6
|
-
var onComplete: (() -> Void)? { get set }
|
|
7
|
-
/// Invoked when the fade-out window of a time-based timer begins.
|
|
8
|
-
var onFadeStart: ((_ duration: TimeInterval) -> Void)? { get set }
|
|
9
|
-
/// Invoked when a timer is cancelled/replaced while its fade may be running —
|
|
10
|
-
/// the listener restores the pre-fade volume.
|
|
11
|
-
var onFadeCancel: (() -> Void)? { get set }
|
|
12
|
-
@discardableResult func clear() -> Bool
|
|
13
|
-
func onTrackChanged()
|
|
14
|
-
func onTrackPlayedToEnd()
|
|
15
|
-
|
|
16
|
-
// Consumer-facing surface — HybridAudioBrowser drives these through
|
|
17
|
-
// TrackPlayer, so they belong here rather than behind a concrete downcast.
|
|
18
|
-
|
|
19
|
-
/// Invoked when the timer state changes (set, cleared, completed).
|
|
20
|
-
var onChanged: ((SleepTimerState) -> Void)? { get set }
|
|
21
|
-
/// The current timer state, or nil when no timer is active.
|
|
22
|
-
func get() -> SleepTimerState?
|
|
23
|
-
/// Sets a time-based timer, optionally fading out over the last
|
|
24
|
-
/// `fadeDuration` seconds.
|
|
25
|
-
func set(seconds: TimeInterval, fadeDuration: TimeInterval?)
|
|
26
|
-
/// Sets the timer to stop playback when the current track ends.
|
|
27
|
-
func setToEndOfTrack()
|
|
28
|
-
}
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import AVFoundation
|
|
2
|
-
import Testing
|
|
3
|
-
|
|
4
|
-
@testable import AudioBrowserTestable
|
|
5
|
-
|
|
6
|
-
// MARK: - Helpers
|
|
7
|
-
|
|
8
|
-
@MainActor
|
|
9
|
-
private final class SeekCompletionSpy: SeekCompletionHandler {
|
|
10
|
-
var calls: [(seconds: Double, didFinish: Bool)] = []
|
|
11
|
-
func handleSeekCompleted(to seconds: Double, didFinish: Bool) {
|
|
12
|
-
calls.append((seconds, didFinish))
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
@MainActor
|
|
17
|
-
private func makeCoordinator() -> LoadSeekCoordinator {
|
|
18
|
-
LoadSeekCoordinator()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// MARK: - Initial State
|
|
22
|
-
|
|
23
|
-
@Suite("initial state")
|
|
24
|
-
@MainActor
|
|
25
|
-
struct InitialStateTests {
|
|
26
|
-
@Test func startsIdle() {
|
|
27
|
-
let c = makeCoordinator()
|
|
28
|
-
guard case .idle = c.state else {
|
|
29
|
-
Issue.record("expected .idle, got \(c.state)")
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@Test func pendingTimeIsNil() {
|
|
35
|
-
let c = makeCoordinator()
|
|
36
|
-
#expect(c.pendingTime == nil)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@Test func shouldDeferReadyTransitionIsFalse() {
|
|
40
|
-
let c = makeCoordinator()
|
|
41
|
-
#expect(c.shouldDeferReadyTransition == false)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// MARK: - Capture
|
|
46
|
-
|
|
47
|
-
@Suite("capture")
|
|
48
|
-
@MainActor
|
|
49
|
-
struct CaptureTests {
|
|
50
|
-
@Test func setsStateToPendingSeek() {
|
|
51
|
-
let c = makeCoordinator()
|
|
52
|
-
c.capture(position: 42.0)
|
|
53
|
-
guard case let .pendingSeek(time) = c.state else {
|
|
54
|
-
Issue.record("expected .pendingSeek, got \(c.state)")
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
#expect(time == 42.0)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@Test func updatesPendingTime() {
|
|
61
|
-
let c = makeCoordinator()
|
|
62
|
-
c.capture(position: 10.0)
|
|
63
|
-
#expect(c.pendingTime == 10.0)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@Test func shouldDeferReadyTransitionBecomesTrue() {
|
|
67
|
-
let c = makeCoordinator()
|
|
68
|
-
c.capture(position: 5.0)
|
|
69
|
-
#expect(c.shouldDeferReadyTransition == true)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@Test func supersedesPreviousPendingSeek() {
|
|
73
|
-
let c = makeCoordinator()
|
|
74
|
-
c.capture(position: 10.0)
|
|
75
|
-
c.capture(position: 20.0)
|
|
76
|
-
#expect(c.pendingTime == 20.0)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@Test func overridesSeekInFlight() {
|
|
80
|
-
let c = makeCoordinator()
|
|
81
|
-
let player = AVPlayer()
|
|
82
|
-
c.capture(position: 10.0)
|
|
83
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
84
|
-
// Now in seekInFlight
|
|
85
|
-
guard case .seekInFlight = c.state else {
|
|
86
|
-
Issue.record("expected .seekInFlight, got \(c.state)")
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
c.capture(position: 30.0)
|
|
90
|
-
guard case let .pendingSeek(time) = c.state else {
|
|
91
|
-
Issue.record("expected .pendingSeek, got \(c.state)")
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
#expect(time == 30.0)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// MARK: - executeIfPending
|
|
99
|
-
|
|
100
|
-
@Suite("executeIfPending")
|
|
101
|
-
@MainActor
|
|
102
|
-
struct ExecuteIfPendingTests {
|
|
103
|
-
@Test func returnsFalseWhenIdle() {
|
|
104
|
-
let c = makeCoordinator()
|
|
105
|
-
let player = AVPlayer()
|
|
106
|
-
#expect(c.executeIfPending(on: player, delegate: nil) == false)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@Test func returnsTrueWhenPendingSeek() {
|
|
110
|
-
let c = makeCoordinator()
|
|
111
|
-
let player = AVPlayer()
|
|
112
|
-
c.capture(position: 15.0)
|
|
113
|
-
#expect(c.executeIfPending(on: player, delegate: nil) == true)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
@Test func transitionsToSeekInFlight() {
|
|
117
|
-
let c = makeCoordinator()
|
|
118
|
-
let player = AVPlayer()
|
|
119
|
-
c.capture(position: 15.0)
|
|
120
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
121
|
-
guard case let .seekInFlight(time) = c.state else {
|
|
122
|
-
Issue.record("expected .seekInFlight, got \(c.state)")
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
#expect(time == 15.0)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@Test func preservesTimeValueThroughTransition() {
|
|
129
|
-
let c = makeCoordinator()
|
|
130
|
-
let player = AVPlayer()
|
|
131
|
-
c.capture(position: 99.5)
|
|
132
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
133
|
-
#expect(c.pendingTime == 99.5)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@Test func returnsFalseWhenSeekInFlight() {
|
|
137
|
-
let c = makeCoordinator()
|
|
138
|
-
let player = AVPlayer()
|
|
139
|
-
c.capture(position: 10.0)
|
|
140
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
141
|
-
// Now in seekInFlight — calling again should return false
|
|
142
|
-
#expect(c.executeIfPending(on: player, delegate: nil) == false)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// MARK: - seekDidComplete
|
|
147
|
-
|
|
148
|
-
@Suite("seekDidComplete")
|
|
149
|
-
@MainActor
|
|
150
|
-
struct SeekDidCompleteTests {
|
|
151
|
-
@Test func fromSeekInFlight_returnsTrue_resetsToIdle() {
|
|
152
|
-
let c = makeCoordinator()
|
|
153
|
-
let player = AVPlayer()
|
|
154
|
-
c.capture(position: 10.0)
|
|
155
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
156
|
-
#expect(c.seekDidComplete(on: player, delegate: nil) == true)
|
|
157
|
-
guard case .idle = c.state else {
|
|
158
|
-
Issue.record("expected .idle, got \(c.state)")
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
@Test func fromPendingSeek_returnsFalse_reExecutes() {
|
|
164
|
-
let c = makeCoordinator()
|
|
165
|
-
let player = AVPlayer()
|
|
166
|
-
// Start a seek
|
|
167
|
-
c.capture(position: 10.0)
|
|
168
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
169
|
-
// Queue a new seek while in-flight
|
|
170
|
-
c.capture(position: 20.0)
|
|
171
|
-
// Complete the first seek — should re-execute
|
|
172
|
-
#expect(c.seekDidComplete(on: player, delegate: nil) == false)
|
|
173
|
-
guard case let .seekInFlight(time) = c.state else {
|
|
174
|
-
Issue.record("expected .seekInFlight, got \(c.state)")
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
#expect(time == 20.0)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@Test func fromIdle_returnsFalse() {
|
|
181
|
-
let c = makeCoordinator()
|
|
182
|
-
let player = AVPlayer()
|
|
183
|
-
#expect(c.seekDidComplete(on: player, delegate: nil) == false)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// MARK: - Reset
|
|
188
|
-
|
|
189
|
-
@Suite("reset")
|
|
190
|
-
@MainActor
|
|
191
|
-
struct ResetTests {
|
|
192
|
-
@Test func clearsFromPendingSeekToIdle() {
|
|
193
|
-
let c = makeCoordinator()
|
|
194
|
-
c.capture(position: 10.0)
|
|
195
|
-
c.reset()
|
|
196
|
-
guard case .idle = c.state else {
|
|
197
|
-
Issue.record("expected .idle, got \(c.state)")
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
#expect(c.pendingTime == nil)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
@Test func clearsFromSeekInFlightToIdle() {
|
|
204
|
-
let c = makeCoordinator()
|
|
205
|
-
let player = AVPlayer()
|
|
206
|
-
c.capture(position: 10.0)
|
|
207
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
208
|
-
c.reset()
|
|
209
|
-
guard case .idle = c.state else {
|
|
210
|
-
Issue.record("expected .idle, got \(c.state)")
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
@Test func noopWhenAlreadyIdle() {
|
|
216
|
-
let c = makeCoordinator()
|
|
217
|
-
c.reset()
|
|
218
|
-
guard case .idle = c.state else {
|
|
219
|
-
Issue.record("expected .idle, got \(c.state)")
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
@Test func invalidatesTheGenerationOfSeeksInFlight() {
|
|
225
|
-
let c = makeCoordinator()
|
|
226
|
-
let issuedAgainst = c.generation
|
|
227
|
-
#expect(c.isCurrentGeneration(issuedAgainst))
|
|
228
|
-
c.reset()
|
|
229
|
-
#expect(!c.isCurrentGeneration(issuedAgainst))
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/// The cross-item race: a seek is in flight when a queue change resets the
|
|
233
|
-
/// coordinator and captures the new track's start position. The old item's
|
|
234
|
-
/// cancelled completion (now a stale generation) must be dropped — not
|
|
235
|
-
/// delivered as a completion that would consume the fresh pending seek.
|
|
236
|
-
@Test func staleCompletionAfterResetIsDroppedAndKeepsThePendingSeek() async {
|
|
237
|
-
let c = makeCoordinator()
|
|
238
|
-
let player = AVPlayer()
|
|
239
|
-
let spy = SeekCompletionSpy()
|
|
240
|
-
|
|
241
|
-
c.capture(position: 10.0)
|
|
242
|
-
_ = c.executeIfPending(on: player, delegate: spy)
|
|
243
|
-
|
|
244
|
-
// Queue change: reset, then capture the new track's start position.
|
|
245
|
-
c.reset()
|
|
246
|
-
c.capture(position: 25.0)
|
|
247
|
-
|
|
248
|
-
// Let the (immediately-cancelled, itemless) seek completion drain.
|
|
249
|
-
for _ in 0 ..< 20 {
|
|
250
|
-
await Task.yield()
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
#expect(spy.calls.isEmpty)
|
|
254
|
-
#expect(c.pendingTime == 25.0)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// MARK: - Integration
|
|
259
|
-
|
|
260
|
-
@Suite("integration")
|
|
261
|
-
@MainActor
|
|
262
|
-
struct IntegrationTests {
|
|
263
|
-
@Test func fullCycle_capture_execute_complete() {
|
|
264
|
-
let c = makeCoordinator()
|
|
265
|
-
let player = AVPlayer()
|
|
266
|
-
|
|
267
|
-
// Capture
|
|
268
|
-
c.capture(position: 30.0)
|
|
269
|
-
#expect(c.shouldDeferReadyTransition == true)
|
|
270
|
-
|
|
271
|
-
// Execute
|
|
272
|
-
let executed = c.executeIfPending(on: player, delegate: nil)
|
|
273
|
-
#expect(executed == true)
|
|
274
|
-
guard case .seekInFlight = c.state else {
|
|
275
|
-
Issue.record("expected .seekInFlight")
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Complete
|
|
280
|
-
let done = c.seekDidComplete(on: player, delegate: nil)
|
|
281
|
-
#expect(done == true)
|
|
282
|
-
guard case .idle = c.state else {
|
|
283
|
-
Issue.record("expected .idle")
|
|
284
|
-
return
|
|
285
|
-
}
|
|
286
|
-
#expect(c.shouldDeferReadyTransition == false)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
@Test func overrideDuringInFlight_capture_execute_capture_complete_reExecute() {
|
|
290
|
-
let c = makeCoordinator()
|
|
291
|
-
let player = AVPlayer()
|
|
292
|
-
|
|
293
|
-
// First seek
|
|
294
|
-
c.capture(position: 10.0)
|
|
295
|
-
_ = c.executeIfPending(on: player, delegate: nil)
|
|
296
|
-
guard case let .seekInFlight(t1) = c.state else {
|
|
297
|
-
Issue.record("expected .seekInFlight")
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
#expect(t1 == 10.0)
|
|
301
|
-
|
|
302
|
-
// Override while in-flight
|
|
303
|
-
c.capture(position: 50.0)
|
|
304
|
-
guard case let .pendingSeek(t2) = c.state else {
|
|
305
|
-
Issue.record("expected .pendingSeek")
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
#expect(t2 == 50.0)
|
|
309
|
-
|
|
310
|
-
// First seek "completes" — should auto re-execute
|
|
311
|
-
let done = c.seekDidComplete(on: player, delegate: nil)
|
|
312
|
-
#expect(done == false)
|
|
313
|
-
guard case let .seekInFlight(t3) = c.state else {
|
|
314
|
-
Issue.record("expected .seekInFlight after re-execute")
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
#expect(t3 == 50.0)
|
|
318
|
-
|
|
319
|
-
// Second seek completes
|
|
320
|
-
let done2 = c.seekDidComplete(on: player, delegate: nil)
|
|
321
|
-
#expect(done2 == true)
|
|
322
|
-
guard case .idle = c.state else {
|
|
323
|
-
Issue.record("expected .idle")
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import Testing
|
|
2
|
-
|
|
3
|
-
@testable import AudioBrowserTestable
|
|
4
|
-
|
|
5
|
-
/// Tests for the media `resolve` layer composition: the variant unwrap (sync vs
|
|
6
|
-
/// async arm) and the override-wins merge that layers the resolved config over
|
|
7
|
-
/// the base. These mirror what `BrowserManager.resolveMediaTrackConfig` +
|
|
8
|
-
/// `mergeRequestConfig` do in production, exercised here against the pure
|
|
9
|
-
/// `MediaResolveComposer` (which the Nitro path delegates to / duplicates).
|
|
10
|
-
@Suite("MediaResolve")
|
|
11
|
-
struct MediaResolveTests {
|
|
12
|
-
typealias Config = MediaResolveComposer.RequestConfigLike
|
|
13
|
-
|
|
14
|
-
// MARK: - Variant unwrap
|
|
15
|
-
|
|
16
|
-
@Test("sync (.first) arm yields its config directly")
|
|
17
|
-
func unwrapSync() async throws {
|
|
18
|
-
let config = Config(userAgent: "RESOLVED")
|
|
19
|
-
let result = try await MediaResolveComposer.unwrap(.sync(config))
|
|
20
|
-
#expect(result == config)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@Test("async (.second) arm awaits and yields its config")
|
|
24
|
-
func unwrapAsync() async throws {
|
|
25
|
-
let config = Config(userAgent: "RESOLVED-ASYNC")
|
|
26
|
-
let result = try await MediaResolveComposer.unwrap(
|
|
27
|
-
.async {
|
|
28
|
-
// Simulate the Promise<RequestConfig> resolving after a hop.
|
|
29
|
-
await Task.yield()
|
|
30
|
-
return config
|
|
31
|
-
},
|
|
32
|
-
)
|
|
33
|
-
#expect(result == config)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@Test("async arm propagates errors (rejected promise)")
|
|
37
|
-
func unwrapAsyncThrows() async {
|
|
38
|
-
struct Boom: Error {}
|
|
39
|
-
await #expect(throws: Boom.self) {
|
|
40
|
-
_ = try await MediaResolveComposer.unwrap(
|
|
41
|
-
.async { throw Boom() } as MediaResolveComposer.ResolveVariant<Config>,
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// MARK: - Merge as the final layer
|
|
47
|
-
|
|
48
|
-
@Test("resolve userAgent overrides base while keeping baseUrl and path")
|
|
49
|
-
func mergeOverridesUserAgent() {
|
|
50
|
-
let base = Config(path: "/audio.mp3", baseUrl: "https://base.example.com", userAgent: "BASE")
|
|
51
|
-
let resolve = Config(userAgent: "RESOLVED")
|
|
52
|
-
|
|
53
|
-
let merged = MediaResolveComposer.merge(base: base, override: resolve)
|
|
54
|
-
|
|
55
|
-
#expect(merged.userAgent == "RESOLVED")
|
|
56
|
-
#expect(merged.baseUrl == "https://base.example.com")
|
|
57
|
-
#expect(merged.path == "/audio.mp3")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
@Test("async resolve result merges identically once unwrapped")
|
|
61
|
-
func mergeAfterAsyncUnwrap() async throws {
|
|
62
|
-
let base = Config(path: "/audio.mp3", baseUrl: "https://base.example.com", userAgent: "BASE")
|
|
63
|
-
let resolved = try await MediaResolveComposer.unwrap(
|
|
64
|
-
.async { Config(userAgent: "RESOLVED-ASYNC") },
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
let merged = MediaResolveComposer.merge(base: base, override: resolved)
|
|
68
|
-
|
|
69
|
-
#expect(merged.userAgent == "RESOLVED-ASYNC")
|
|
70
|
-
#expect(merged.baseUrl == "https://base.example.com")
|
|
71
|
-
#expect(merged.path == "/audio.mp3")
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@Test("absent resolve is a no-op (base is returned unchanged)")
|
|
75
|
-
func absentResolveIsNoOp() {
|
|
76
|
-
let base = Config(path: "/audio.mp3", baseUrl: "https://base.example.com", userAgent: "BASE")
|
|
77
|
-
|
|
78
|
-
// Production guards `resolve == nil` and returns base untouched; modelled as
|
|
79
|
-
// merging an all-nil override, which must leave every field intact.
|
|
80
|
-
let merged = MediaResolveComposer.merge(base: base, override: Config())
|
|
81
|
-
|
|
82
|
-
#expect(merged == base)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@Test("headers and query merge per-key with override winning")
|
|
86
|
-
func mergeDictsPerKey() {
|
|
87
|
-
let base = Config(
|
|
88
|
-
headers: ["User-Agent": "BASE", "X-Base": "1"],
|
|
89
|
-
query: ["a": "1"],
|
|
90
|
-
)
|
|
91
|
-
let resolve = Config(
|
|
92
|
-
headers: ["User-Agent": "RESOLVED", "X-Resolve": "2"],
|
|
93
|
-
query: ["b": "2"],
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
let merged = MediaResolveComposer.merge(base: base, override: resolve)
|
|
97
|
-
|
|
98
|
-
#expect(merged.headers == ["User-Agent": "RESOLVED", "X-Base": "1", "X-Resolve": "2"])
|
|
99
|
-
#expect(merged.query == ["a": "1", "b": "2"])
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@Test("mergeDicts is nil-safe on either side")
|
|
103
|
-
func mergeDictsNilSafe() {
|
|
104
|
-
#expect(MediaResolveComposer.mergeDicts(nil, nil) == nil)
|
|
105
|
-
#expect(MediaResolveComposer.mergeDicts(["a": "1"], nil) == ["a": "1"])
|
|
106
|
-
#expect(MediaResolveComposer.mergeDicts(nil, ["b": "2"]) == ["b": "2"])
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// MARK: - Run-both resolve composition
|
|
110
|
-
|
|
111
|
-
@Test("resolve composition merges async then sync (sync wins); nil when neither")
|
|
112
|
-
func composeResolved() {
|
|
113
|
-
// Same `combine` production uses (mergeRequestConfig), here over the test type.
|
|
114
|
-
let combine: (Config, Config) -> Config = { MediaResolveComposer.merge(base: $0, override: $1) }
|
|
115
|
-
#expect(MediaResolveComposer.composeResolved(async: nil, sync: nil, combine: combine) == nil)
|
|
116
|
-
|
|
117
|
-
let a = Config(baseUrl: "https://async.example.com", userAgent: "ASYNC")
|
|
118
|
-
let s = Config(userAgent: "SYNC")
|
|
119
|
-
let merged = MediaResolveComposer.composeResolved(async: a, sync: s, combine: combine)
|
|
120
|
-
#expect(merged?.userAgent == "SYNC") // sync wins
|
|
121
|
-
#expect(merged?.baseUrl == "https://async.example.com") // async's baseUrl survives
|
|
122
|
-
|
|
123
|
-
#expect(MediaResolveComposer.composeResolved(async: a, sync: nil, combine: combine)?.userAgent == "ASYNC")
|
|
124
|
-
#expect(MediaResolveComposer.composeResolved(async: nil, sync: s, combine: combine)?.userAgent == "SYNC")
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import Testing
|
|
3
|
-
|
|
4
|
-
@testable import AudioBrowserTestable
|
|
5
|
-
|
|
6
|
-
@Suite("OnceValue")
|
|
7
|
-
struct OnceValueTests {
|
|
8
|
-
@Test func waitReturnsImmediatelyWhenAlreadyResolved() async {
|
|
9
|
-
let once = OnceValue<Int>()
|
|
10
|
-
once.resolve(7)
|
|
11
|
-
let value = await once.wait()
|
|
12
|
-
#expect(value == 7)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@Test func waitResumesAllWaitersOnResolve() async {
|
|
16
|
-
let once = OnceValue<String>()
|
|
17
|
-
async let first = once.wait()
|
|
18
|
-
async let second = once.wait()
|
|
19
|
-
once.resolve("ready")
|
|
20
|
-
let values = await [first, second]
|
|
21
|
-
#expect(values == ["ready", "ready"])
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@Test func checkResolvesViaProvider() async {
|
|
25
|
-
nonisolated(unsafe) var ready: Int?
|
|
26
|
-
let once = OnceValue<Int> { ready }
|
|
27
|
-
once.check()
|
|
28
|
-
ready = 3
|
|
29
|
-
once.check()
|
|
30
|
-
let value = await once.wait()
|
|
31
|
-
#expect(value == 3)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@Test func firstResolveWins() async {
|
|
35
|
-
let once = OnceValue<Int>()
|
|
36
|
-
once.resolve(1)
|
|
37
|
-
once.resolve(2)
|
|
38
|
-
let value = await once.wait()
|
|
39
|
-
#expect(value == 1)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@Suite("OnceValue.wait(timeout:)")
|
|
44
|
-
struct OnceValueTimeoutTests {
|
|
45
|
-
@Test func returnsValueWhenAlreadyResolved() async {
|
|
46
|
-
let once = OnceValue<Int>()
|
|
47
|
-
once.resolve(42)
|
|
48
|
-
let value = await once.wait(timeout: .milliseconds(50))
|
|
49
|
-
#expect(value == 42)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@Test func returnsValueWhenResolvedDuringWait() async {
|
|
53
|
-
let once = OnceValue<Int>()
|
|
54
|
-
Task {
|
|
55
|
-
try? await Task.sleep(for: .milliseconds(10))
|
|
56
|
-
once.resolve(42)
|
|
57
|
-
}
|
|
58
|
-
let value = await once.wait(timeout: .seconds(5))
|
|
59
|
-
#expect(value == 42)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@Test func returnsNilOnTimeout() async {
|
|
63
|
-
let once = OnceValue<Int>()
|
|
64
|
-
let value = await once.wait(timeout: .milliseconds(50))
|
|
65
|
-
#expect(value == nil)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@Test func lateResolveAfterTimeoutIsHarmless() async {
|
|
69
|
-
let once = OnceValue<Int>()
|
|
70
|
-
let value = await once.wait(timeout: .milliseconds(20))
|
|
71
|
-
#expect(value == nil)
|
|
72
|
-
// The timed-out waiter's continuation is still registered; resolving must
|
|
73
|
-
// not double-resume it, and fresh waits must see the value.
|
|
74
|
-
once.resolve(9)
|
|
75
|
-
try? await Task.sleep(for: .milliseconds(20))
|
|
76
|
-
let late = await once.wait(timeout: .milliseconds(50))
|
|
77
|
-
#expect(late == 9)
|
|
78
|
-
}
|
|
79
|
-
}
|