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
|
@@ -2,395 +2,99 @@ import Testing
|
|
|
2
2
|
|
|
3
3
|
@testable import AudioBrowserTestable
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
displayScale: 2.0,
|
|
16
|
-
urlResolver: nil,
|
|
5
|
+
@Suite("CarPlayArtworkResolver")
|
|
6
|
+
struct CarPlayArtworkResolverTests {
|
|
7
|
+
private func action(
|
|
8
|
+
artwork: String? = nil,
|
|
9
|
+
artworkSource: ArtworkResolvedImage? = nil,
|
|
10
|
+
resolved: ArtworkResolvedImage? = nil,
|
|
11
|
+
stencil: Bool = false,
|
|
12
|
+
) -> ArtworkLoadAction {
|
|
13
|
+
CarPlayArtworkResolver.action(
|
|
14
|
+
artwork: artwork, artworkSource: artworkSource, resolved: resolved, stencil: stencil, width: 40, height: 40,
|
|
17
15
|
)
|
|
18
|
-
#expect(action == .sfSymbol(artwork: "sf:heart.fill", width: 40, height: 40))
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
artwork: "sf:play.circle?bg=#000&fg=#fff",
|
|
24
|
-
artworkSourceUri: nil,
|
|
25
|
-
stencil: false,
|
|
26
|
-
targetWidth: 24, targetHeight: 24,
|
|
27
|
-
displayScale: 3.0,
|
|
28
|
-
urlResolver: nil,
|
|
29
|
-
)
|
|
30
|
-
#expect(action == .sfSymbol(artwork: "sf:play.circle?bg=#000&fg=#fff", width: 24, height: 24))
|
|
18
|
+
private func image(_ uri: String, headers: [String: String]? = nil) -> ArtworkResolvedImage {
|
|
19
|
+
ArtworkResolvedImage(uri: uri, headers: headers)
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
@Test func
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
displayScale: 2.0,
|
|
41
|
-
urlResolver: { _, _ in
|
|
42
|
-
resolverCalled = true
|
|
43
|
-
return ArtworkResolvedImage(uri: "https://example.com/img.jpg", headers: nil)
|
|
44
|
-
},
|
|
22
|
+
@Test func anSFSymbolRendersAndIsNotResolved() {
|
|
23
|
+
#expect(!CarPlayArtworkResolver.needsResolve(artwork: "sf:star.fill?fg=#fff"))
|
|
24
|
+
#expect(CarPlayArtworkResolver.needsResolve(artwork: "https://example.com/a.jpg"))
|
|
25
|
+
#expect(CarPlayArtworkResolver.needsResolve(artwork: nil))
|
|
26
|
+
#expect(
|
|
27
|
+
action(artwork: "sf:star.fill?fg=#fff", resolved: image("https://example.com/r.jpg"))
|
|
28
|
+
== .sfSymbol(artwork: "sf:star.fill?fg=#fff", width: 40, height: 40),
|
|
45
29
|
)
|
|
46
|
-
#expect(action == .sfSymbol(artwork: "sf:music.note", width: 40, height: 40))
|
|
47
|
-
#expect(!resolverCalled)
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
@Test func
|
|
51
|
-
|
|
52
|
-
artwork: nil,
|
|
53
|
-
artworkSourceUri: "sf:star.fill",
|
|
54
|
-
stencil: false,
|
|
55
|
-
targetWidth: 20, targetHeight: 20,
|
|
56
|
-
displayScale: 2.0,
|
|
57
|
-
urlResolver: nil,
|
|
58
|
-
)
|
|
59
|
-
#expect(action == .sfSymbol(artwork: "sf:star.fill", width: 20, height: 20))
|
|
32
|
+
@Test func anSFSymbolArtworkSourceRendersAsTheFallback() {
|
|
33
|
+
#expect(action(artworkSource: image("sf:star.fill")) == .sfSymbol(artwork: "sf:star.fill", width: 40, height: 40))
|
|
60
34
|
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// MARK: - No Artwork
|
|
64
35
|
|
|
65
|
-
@
|
|
66
|
-
|
|
67
|
-
@Test func noArtworkOrSource_returnsNone() async {
|
|
68
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
69
|
-
artwork: nil,
|
|
70
|
-
artworkSourceUri: nil,
|
|
71
|
-
stencil: false,
|
|
72
|
-
targetWidth: 40, targetHeight: 40,
|
|
73
|
-
displayScale: 2.0,
|
|
74
|
-
urlResolver: nil,
|
|
75
|
-
)
|
|
76
|
-
#expect(action == .none)
|
|
36
|
+
@Test func nothingToLoadIsNone() {
|
|
37
|
+
#expect(action() == .none)
|
|
77
38
|
}
|
|
78
39
|
|
|
79
|
-
@Test func
|
|
80
|
-
|
|
81
|
-
artwork:
|
|
82
|
-
|
|
83
|
-
stencil: false,
|
|
84
|
-
targetWidth: 40, targetHeight: 40,
|
|
85
|
-
displayScale: 2.0,
|
|
86
|
-
urlResolver: { _, _ in nil },
|
|
40
|
+
@Test func theResolvedURLIsFetchedWithItsHeaders() {
|
|
41
|
+
#expect(
|
|
42
|
+
action(artwork: "https://example.com/a.jpg", resolved: image("https://cdn.example.com/r.jpg", headers: ["A": "1"]))
|
|
43
|
+
== .fetch(uri: "https://cdn.example.com/r.jpg", headers: ["A": "1"], shouldTint: false, isSvg: false),
|
|
87
44
|
)
|
|
88
|
-
#expect(action == .none)
|
|
89
45
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// MARK: - URL Resolution
|
|
93
46
|
|
|
94
|
-
@
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
artwork: "https://example.com/art.jpg",
|
|
99
|
-
artworkSourceUri: nil,
|
|
100
|
-
stencil: false,
|
|
101
|
-
targetWidth: 40, targetHeight: 40,
|
|
102
|
-
displayScale: 2.0,
|
|
103
|
-
urlResolver: { pixelWidth, pixelHeight in
|
|
104
|
-
#expect(pixelWidth == 80)
|
|
105
|
-
#expect(pixelHeight == 80)
|
|
106
|
-
return ArtworkResolvedImage(
|
|
107
|
-
uri: "https://cdn.example.com/art_80x80.jpg",
|
|
108
|
-
headers: ["Authorization": "Bearer token"],
|
|
109
|
-
)
|
|
110
|
-
},
|
|
47
|
+
@Test func withoutAResolvedURLTheArtworkIsFetched() {
|
|
48
|
+
#expect(
|
|
49
|
+
action(artwork: "https://example.com/a.jpg")
|
|
50
|
+
== .fetch(uri: "https://example.com/a.jpg", headers: nil, shouldTint: false, isSvg: false),
|
|
111
51
|
)
|
|
112
|
-
#expect(action == .fetch(
|
|
113
|
-
uri: "https://cdn.example.com/art_80x80.jpg",
|
|
114
|
-
headers: ["Authorization": "Bearer token"],
|
|
115
|
-
shouldTint: false,
|
|
116
|
-
isSvg: false,
|
|
117
|
-
))
|
|
118
52
|
}
|
|
119
53
|
|
|
120
|
-
@Test func
|
|
121
|
-
|
|
122
|
-
artwork: "https://example.com/
|
|
123
|
-
|
|
124
|
-
stencil: false,
|
|
125
|
-
targetWidth: 24, targetHeight: 24,
|
|
126
|
-
displayScale: 3.0,
|
|
127
|
-
urlResolver: { _, _ in
|
|
128
|
-
ArtworkResolvedImage(uri: "https://cdn.example.com/resized.jpg", headers: nil)
|
|
129
|
-
},
|
|
54
|
+
@Test func anInvalidResolvedURLFallsBackToTheArtwork() {
|
|
55
|
+
#expect(
|
|
56
|
+
action(artwork: "https://example.com/a.jpg", resolved: image(""))
|
|
57
|
+
== .fetch(uri: "https://example.com/a.jpg", headers: nil, shouldTint: false, isSvg: false),
|
|
130
58
|
)
|
|
131
|
-
#expect(action == .fetch(
|
|
132
|
-
uri: "https://cdn.example.com/resized.jpg",
|
|
133
|
-
headers: nil,
|
|
134
|
-
shouldTint: false,
|
|
135
|
-
isSvg: false,
|
|
136
|
-
))
|
|
137
59
|
}
|
|
138
60
|
|
|
139
|
-
@Test func
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
stencil: false,
|
|
144
|
-
targetWidth: 40, targetHeight: 40,
|
|
145
|
-
displayScale: 2.0,
|
|
146
|
-
urlResolver: { _, _ in nil },
|
|
61
|
+
@Test func withoutArtworkTheArtworkSourceIsFetchedWithItsHeaders() {
|
|
62
|
+
#expect(
|
|
63
|
+
action(artworkSource: image("https://example.com/s.jpg", headers: ["Authorization": "Bearer t"]))
|
|
64
|
+
== .fetch(uri: "https://example.com/s.jpg", headers: ["Authorization": "Bearer t"], shouldTint: false, isSvg: false),
|
|
147
65
|
)
|
|
148
|
-
#expect(action == .fetch(
|
|
149
|
-
uri: "https://example.com/art.jpg",
|
|
150
|
-
headers: nil,
|
|
151
|
-
shouldTint: false,
|
|
152
|
-
isSvg: false,
|
|
153
|
-
))
|
|
154
66
|
}
|
|
155
67
|
|
|
156
|
-
@Test func
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
displayScale: 2.0,
|
|
163
|
-
urlResolver: { _, _ in
|
|
164
|
-
// Return an invalid URI that URL(string:) would reject
|
|
165
|
-
ArtworkResolvedImage(uri: "", headers: nil)
|
|
166
|
-
},
|
|
68
|
+
@Test func artworkWinsOverArtworkSourceWithoutItsHeaders() {
|
|
69
|
+
#expect(
|
|
70
|
+
action(
|
|
71
|
+
artwork: "https://example.com/a.jpg",
|
|
72
|
+
artworkSource: image("https://example.com/s.jpg", headers: ["Authorization": "Bearer t"]),
|
|
73
|
+
) == .fetch(uri: "https://example.com/a.jpg", headers: nil, shouldTint: false, isSvg: false),
|
|
167
74
|
)
|
|
168
|
-
// Should fall back to direct artwork URL
|
|
169
|
-
#expect(action == .fetch(
|
|
170
|
-
uri: "https://example.com/fallback.jpg",
|
|
171
|
-
headers: nil,
|
|
172
|
-
shouldTint: false,
|
|
173
|
-
isSvg: false,
|
|
174
|
-
))
|
|
175
75
|
}
|
|
176
76
|
|
|
177
|
-
@Test func
|
|
178
|
-
|
|
179
|
-
var receivedHeight: Double?
|
|
180
|
-
_ = await CarPlayArtworkResolver.resolve(
|
|
181
|
-
artwork: "https://example.com/art.jpg",
|
|
182
|
-
artworkSourceUri: nil,
|
|
183
|
-
stencil: false,
|
|
184
|
-
targetWidth: 50, targetHeight: 30,
|
|
185
|
-
displayScale: 2.5,
|
|
186
|
-
urlResolver: { w, h in
|
|
187
|
-
receivedWidth = w
|
|
188
|
-
receivedHeight = h
|
|
189
|
-
return ArtworkResolvedImage(uri: "https://cdn.example.com/art.jpg", headers: nil)
|
|
190
|
-
},
|
|
191
|
-
)
|
|
192
|
-
#expect(receivedWidth == 125) // 50 * 2.5
|
|
193
|
-
#expect(receivedHeight == 75) // 30 * 2.5
|
|
77
|
+
@Test func anInvalidDirectURLIsNone() {
|
|
78
|
+
#expect(action(artwork: "") == .none)
|
|
194
79
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// MARK: - Direct Fallback
|
|
198
80
|
|
|
199
|
-
@
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
stencil: false,
|
|
206
|
-
targetWidth: 40, targetHeight: 40,
|
|
207
|
-
displayScale: 2.0,
|
|
208
|
-
urlResolver: nil,
|
|
81
|
+
@Test func anSVGPathIsMarkedCaseInsensitively() {
|
|
82
|
+
#expect(action(artwork: "https://example.com/i.svg") == .fetch(uri: "https://example.com/i.svg", headers: nil, shouldTint: false, isSvg: true))
|
|
83
|
+
#expect(action(artwork: "https://example.com/i.SVG") == .fetch(uri: "https://example.com/i.SVG", headers: nil, shouldTint: false, isSvg: true))
|
|
84
|
+
#expect(
|
|
85
|
+
action(resolved: image("https://cdn.example.com/i.svg"))
|
|
86
|
+
== .fetch(uri: "https://cdn.example.com/i.svg", headers: nil, shouldTint: false, isSvg: true),
|
|
209
87
|
)
|
|
210
|
-
#expect(action == .fetch(
|
|
211
|
-
uri: "https://example.com/artwork.png",
|
|
212
|
-
headers: nil,
|
|
213
|
-
shouldTint: false,
|
|
214
|
-
isSvg: false,
|
|
215
|
-
))
|
|
216
88
|
}
|
|
217
89
|
|
|
218
|
-
@Test func
|
|
219
|
-
|
|
220
|
-
artwork:
|
|
221
|
-
|
|
222
|
-
stencil: false,
|
|
223
|
-
targetWidth: 40, targetHeight: 40,
|
|
224
|
-
displayScale: 2.0,
|
|
225
|
-
urlResolver: nil,
|
|
90
|
+
@Test func aStencilTints() {
|
|
91
|
+
#expect(
|
|
92
|
+
action(artwork: "https://example.com/a.png", stencil: true)
|
|
93
|
+
== .fetch(uri: "https://example.com/a.png", headers: nil, shouldTint: true, isSvg: false),
|
|
226
94
|
)
|
|
227
|
-
#expect(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
shouldTint: false,
|
|
231
|
-
isSvg: false,
|
|
232
|
-
))
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
@Test func artworkPreferredOverArtworkSource() async {
|
|
236
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
237
|
-
artwork: "https://example.com/artwork.jpg",
|
|
238
|
-
artworkSourceUri: "https://example.com/source.jpg",
|
|
239
|
-
stencil: false,
|
|
240
|
-
targetWidth: 40, targetHeight: 40,
|
|
241
|
-
displayScale: 2.0,
|
|
242
|
-
urlResolver: nil,
|
|
243
|
-
)
|
|
244
|
-
#expect(action == .fetch(
|
|
245
|
-
uri: "https://example.com/artwork.jpg",
|
|
246
|
-
headers: nil,
|
|
247
|
-
shouldTint: false,
|
|
248
|
-
isSvg: false,
|
|
249
|
-
))
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
@Test func invalidDirectUrl_returnsNone() async {
|
|
253
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
254
|
-
artwork: "",
|
|
255
|
-
artworkSourceUri: nil,
|
|
256
|
-
stencil: false,
|
|
257
|
-
targetWidth: 40, targetHeight: 40,
|
|
258
|
-
displayScale: 2.0,
|
|
259
|
-
urlResolver: nil,
|
|
260
|
-
)
|
|
261
|
-
#expect(action == .none)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// MARK: - SVG Detection
|
|
266
|
-
|
|
267
|
-
@Suite("SVG detection")
|
|
268
|
-
struct SVGDetectionTests {
|
|
269
|
-
@Test func svgExtension_setsIsSvgTrue() async {
|
|
270
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
271
|
-
artwork: "https://example.com/icon.svg",
|
|
272
|
-
artworkSourceUri: nil,
|
|
273
|
-
stencil: false,
|
|
274
|
-
targetWidth: 40, targetHeight: 40,
|
|
275
|
-
displayScale: 2.0,
|
|
276
|
-
urlResolver: nil,
|
|
277
|
-
)
|
|
278
|
-
#expect(action == .fetch(
|
|
279
|
-
uri: "https://example.com/icon.svg",
|
|
280
|
-
headers: nil,
|
|
281
|
-
shouldTint: false,
|
|
282
|
-
isSvg: true,
|
|
283
|
-
))
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
@Test func svgUppercase_setsIsSvgTrue() async {
|
|
287
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
288
|
-
artwork: "https://example.com/icon.SVG",
|
|
289
|
-
artworkSourceUri: nil,
|
|
290
|
-
stencil: false,
|
|
291
|
-
targetWidth: 40, targetHeight: 40,
|
|
292
|
-
displayScale: 2.0,
|
|
293
|
-
urlResolver: nil,
|
|
294
|
-
)
|
|
295
|
-
#expect(action == .fetch(
|
|
296
|
-
uri: "https://example.com/icon.SVG",
|
|
297
|
-
headers: nil,
|
|
298
|
-
shouldTint: false,
|
|
299
|
-
isSvg: true,
|
|
300
|
-
))
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
@Test func resolvedSvgUrl_setsIsSvgTrue() async {
|
|
304
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
305
|
-
artwork: "https://example.com/art.jpg",
|
|
306
|
-
artworkSourceUri: nil,
|
|
307
|
-
stencil: false,
|
|
308
|
-
targetWidth: 40, targetHeight: 40,
|
|
309
|
-
displayScale: 2.0,
|
|
310
|
-
urlResolver: { _, _ in
|
|
311
|
-
ArtworkResolvedImage(uri: "https://cdn.example.com/icon.svg", headers: nil)
|
|
312
|
-
},
|
|
313
|
-
)
|
|
314
|
-
#expect(action == .fetch(
|
|
315
|
-
uri: "https://cdn.example.com/icon.svg",
|
|
316
|
-
headers: nil,
|
|
317
|
-
shouldTint: false,
|
|
318
|
-
isSvg: true,
|
|
319
|
-
))
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
@Test func nonSvg_setsIsSvgFalse() async {
|
|
323
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
324
|
-
artwork: "https://example.com/photo.jpg",
|
|
325
|
-
artworkSourceUri: nil,
|
|
326
|
-
stencil: false,
|
|
327
|
-
targetWidth: 40, targetHeight: 40,
|
|
328
|
-
displayScale: 2.0,
|
|
329
|
-
urlResolver: nil,
|
|
330
|
-
)
|
|
331
|
-
#expect(action == .fetch(
|
|
332
|
-
uri: "https://example.com/photo.jpg",
|
|
333
|
-
headers: nil,
|
|
334
|
-
shouldTint: false,
|
|
335
|
-
isSvg: false,
|
|
336
|
-
))
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// MARK: - Tinting
|
|
341
|
-
|
|
342
|
-
@Suite("tinting")
|
|
343
|
-
struct TintingTests {
|
|
344
|
-
@Test func stencilTrue_setsShouldTint() async {
|
|
345
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
346
|
-
artwork: "https://example.com/icon.png",
|
|
347
|
-
artworkSourceUri: nil,
|
|
348
|
-
stencil: true,
|
|
349
|
-
targetWidth: 40, targetHeight: 40,
|
|
350
|
-
displayScale: 2.0,
|
|
351
|
-
urlResolver: nil,
|
|
352
|
-
)
|
|
353
|
-
#expect(action == .fetch(
|
|
354
|
-
uri: "https://example.com/icon.png",
|
|
355
|
-
headers: nil,
|
|
356
|
-
shouldTint: true,
|
|
357
|
-
isSvg: false,
|
|
358
|
-
))
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
@Test func stencilFalse_doesNotTint() async {
|
|
362
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
363
|
-
artwork: "https://example.com/photo.jpg",
|
|
364
|
-
artworkSourceUri: nil,
|
|
365
|
-
stencil: false,
|
|
366
|
-
targetWidth: 40, targetHeight: 40,
|
|
367
|
-
displayScale: 2.0,
|
|
368
|
-
urlResolver: nil,
|
|
369
|
-
)
|
|
370
|
-
#expect(action == .fetch(
|
|
371
|
-
uri: "https://example.com/photo.jpg",
|
|
372
|
-
headers: nil,
|
|
373
|
-
shouldTint: false,
|
|
374
|
-
isSvg: false,
|
|
375
|
-
))
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
@Test func tintingPreservedThroughResolver() async {
|
|
379
|
-
let action = await CarPlayArtworkResolver.resolve(
|
|
380
|
-
artwork: "https://example.com/icon.png",
|
|
381
|
-
artworkSourceUri: nil,
|
|
382
|
-
stencil: true,
|
|
383
|
-
targetWidth: 40, targetHeight: 40,
|
|
384
|
-
displayScale: 2.0,
|
|
385
|
-
urlResolver: { _, _ in
|
|
386
|
-
ArtworkResolvedImage(uri: "https://cdn.example.com/icon.png", headers: nil)
|
|
387
|
-
},
|
|
95
|
+
#expect(
|
|
96
|
+
action(resolved: image("https://cdn.example.com/r.png"), stencil: true)
|
|
97
|
+
== .fetch(uri: "https://cdn.example.com/r.png", headers: nil, shouldTint: true, isSvg: false),
|
|
388
98
|
)
|
|
389
|
-
#expect(action == .fetch(
|
|
390
|
-
uri: "https://cdn.example.com/icon.png",
|
|
391
|
-
headers: nil,
|
|
392
|
-
shouldTint: true,
|
|
393
|
-
isSvg: false,
|
|
394
|
-
))
|
|
395
99
|
}
|
|
396
100
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
|
|
3
|
+
@testable import AudioBrowserTestable
|
|
4
|
+
|
|
5
|
+
@Suite("DeferredSeek")
|
|
6
|
+
struct DeferredSeekTests {
|
|
7
|
+
@Test func startsIdle() {
|
|
8
|
+
let seek = DeferredSeek()
|
|
9
|
+
#expect(seek.state == .idle)
|
|
10
|
+
#expect(seek.target == nil)
|
|
11
|
+
#expect(!seek.defersReady)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test func aCaptureDefersReadyUntilItLands() {
|
|
15
|
+
var seek = DeferredSeek()
|
|
16
|
+
seek.capture(30)
|
|
17
|
+
#expect(seek.defersReady)
|
|
18
|
+
#expect(seek.begin() == 30)
|
|
19
|
+
#expect(seek.state == .inFlight(30))
|
|
20
|
+
#expect(seek.defersReady)
|
|
21
|
+
#expect(seek.land() == .done)
|
|
22
|
+
#expect(seek.state == .idle)
|
|
23
|
+
#expect(!seek.defersReady)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test func theLatestCaptureWins() {
|
|
27
|
+
var seek = DeferredSeek()
|
|
28
|
+
seek.capture(10)
|
|
29
|
+
seek.capture(20)
|
|
30
|
+
#expect(seek.target == 20)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test func beginHandsOutOnlyAPendingTarget() {
|
|
34
|
+
var seek = DeferredSeek()
|
|
35
|
+
#expect(seek.begin() == nil)
|
|
36
|
+
seek.capture(15)
|
|
37
|
+
#expect(seek.begin() == 15)
|
|
38
|
+
#expect(seek.begin() == nil)
|
|
39
|
+
#expect(seek.target == 15)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A target captured while a seek is in flight is issued when that seek lands, and only its
|
|
43
|
+
/// own landing makes the load ready.
|
|
44
|
+
@Test func aCaptureInFlightIsReissuedOnLanding() {
|
|
45
|
+
var seek = DeferredSeek()
|
|
46
|
+
seek.capture(10)
|
|
47
|
+
_ = seek.begin()
|
|
48
|
+
seek.capture(50)
|
|
49
|
+
#expect(seek.state == .pending(50))
|
|
50
|
+
#expect(seek.land() == .reseek(50))
|
|
51
|
+
#expect(seek.state == .inFlight(50))
|
|
52
|
+
#expect(seek.land() == .done)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// A seek issued while playing, not deferred, lands as nothing to do.
|
|
56
|
+
@Test func aLandingWithNothingInFlightIsNone() {
|
|
57
|
+
var seek = DeferredSeek()
|
|
58
|
+
#expect(seek.land() == .none)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test func resetDropsTheTargetAndInvalidatesSeeksInFlight() {
|
|
62
|
+
var seek = DeferredSeek()
|
|
63
|
+
seek.capture(10)
|
|
64
|
+
_ = seek.begin()
|
|
65
|
+
let issuedAgainst = seek.generation
|
|
66
|
+
seek.reset()
|
|
67
|
+
#expect(seek.state == .idle)
|
|
68
|
+
#expect(seek.generation != issuedAgainst)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -120,3 +120,32 @@ struct ListenerCountTests {
|
|
|
120
120
|
#expect(emitter.listenerCount == 0)
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
// MARK: - subscribe
|
|
125
|
+
|
|
126
|
+
@Suite("subscribe")
|
|
127
|
+
struct SubscribeTests {
|
|
128
|
+
@Test func receivesUntilCancelled() {
|
|
129
|
+
let emitter = Emitter<Int>()
|
|
130
|
+
var received: [Int] = []
|
|
131
|
+
let subscription = emitter.subscribe { received.append($0) }
|
|
132
|
+
emitter.emit(1)
|
|
133
|
+
subscription.cancel()
|
|
134
|
+
emitter.emit(2)
|
|
135
|
+
#expect(received == [1])
|
|
136
|
+
#expect(emitter.listenerCount == 0)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Test func releasingTheSubscriptionRemovesTheListener() {
|
|
140
|
+
let emitter = Emitter<Int>()
|
|
141
|
+
var received: [Int] = []
|
|
142
|
+
do {
|
|
143
|
+
let subscription = emitter.subscribe { received.append($0) }
|
|
144
|
+
emitter.emit(1)
|
|
145
|
+
_ = subscription
|
|
146
|
+
}
|
|
147
|
+
emitter.emit(2)
|
|
148
|
+
#expect(received == [1])
|
|
149
|
+
#expect(emitter.listenerCount == 0)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
|
|
3
|
+
@testable import AudioBrowserTestable
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
@Suite("GenerationMemo")
|
|
7
|
+
struct GenerationMemoTests {
|
|
8
|
+
struct Failure: Error {}
|
|
9
|
+
|
|
10
|
+
/// A computation that suspends until `release()`, counting its runs.
|
|
11
|
+
@MainActor
|
|
12
|
+
final class Gate {
|
|
13
|
+
var runs = 0
|
|
14
|
+
var input = 0
|
|
15
|
+
private var waiters: [CheckedContinuation<Void, Never>] = []
|
|
16
|
+
|
|
17
|
+
func run() async -> Int {
|
|
18
|
+
runs += 1
|
|
19
|
+
let input = input
|
|
20
|
+
await withCheckedContinuation { waiters.append($0) }
|
|
21
|
+
return input
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func release() {
|
|
25
|
+
let pending = waiters
|
|
26
|
+
waiters = []
|
|
27
|
+
for waiter in pending {
|
|
28
|
+
waiter.resume()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Yields until `count` runs have started, recording an issue rather than hanging when they
|
|
33
|
+
/// never do.
|
|
34
|
+
func waitForRuns(_ count: Int, sourceLocation: SourceLocation = #_sourceLocation) async {
|
|
35
|
+
for _ in 0 ..< 1000 where runs < count {
|
|
36
|
+
await Task.yield()
|
|
37
|
+
}
|
|
38
|
+
if runs < count {
|
|
39
|
+
Issue.record("expected \(count) runs, saw \(runs)", sourceLocation: sourceLocation)
|
|
40
|
+
release()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test func concurrentCallersShareOneComputation() async throws {
|
|
46
|
+
let memo = GenerationMemo<Int>()
|
|
47
|
+
let gate = Gate()
|
|
48
|
+
gate.input = 1
|
|
49
|
+
let first = Task { try await memo.value { await gate.run() } }
|
|
50
|
+
let second = Task { try await memo.value { await gate.run() } }
|
|
51
|
+
await gate.waitForRuns(1)
|
|
52
|
+
gate.release()
|
|
53
|
+
#expect(try await first.value == 1)
|
|
54
|
+
#expect(try await second.value == 1)
|
|
55
|
+
#expect(gate.runs == 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test func keepsTheValueWithinAGeneration() async throws {
|
|
59
|
+
let memo = GenerationMemo<Int>()
|
|
60
|
+
_ = try await memo.value { 1 }
|
|
61
|
+
#expect(try await memo.value { 2 } == 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test func invalidateComputesAgain() async throws {
|
|
65
|
+
let memo = GenerationMemo<Int>()
|
|
66
|
+
_ = try await memo.value { 1 }
|
|
67
|
+
memo.invalidate()
|
|
68
|
+
#expect(try await memo.value { 2 } == 2)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test func aResultThatLandsAfterAnInvalidationIsRecomputed() async throws {
|
|
72
|
+
let memo = GenerationMemo<Int>()
|
|
73
|
+
let gate = Gate()
|
|
74
|
+
gate.input = 1
|
|
75
|
+
let result = Task { try await memo.value { await gate.run() } }
|
|
76
|
+
await gate.waitForRuns(1)
|
|
77
|
+
gate.input = 2
|
|
78
|
+
memo.invalidate()
|
|
79
|
+
gate.release()
|
|
80
|
+
await gate.waitForRuns(2)
|
|
81
|
+
gate.release()
|
|
82
|
+
#expect(try await result.value == 2)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test func aFailureIsNotKept() async throws {
|
|
86
|
+
let memo = GenerationMemo<Int>()
|
|
87
|
+
await #expect(throws: Failure.self) { try await memo.value { throw Failure() } }
|
|
88
|
+
#expect(try await memo.value { 3 } == 3)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test func aFailureThatLandsAfterAnInvalidationIsRetried() async throws {
|
|
92
|
+
let memo = GenerationMemo<Int>()
|
|
93
|
+
let gate = Gate()
|
|
94
|
+
let result = Task {
|
|
95
|
+
try await memo.value {
|
|
96
|
+
let input = await gate.run()
|
|
97
|
+
if input == 0 { throw Failure() }
|
|
98
|
+
return input
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
await gate.waitForRuns(1)
|
|
102
|
+
gate.input = 4
|
|
103
|
+
memo.invalidate()
|
|
104
|
+
gate.release()
|
|
105
|
+
await gate.waitForRuns(2)
|
|
106
|
+
gate.release()
|
|
107
|
+
#expect(try await result.value == 4)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test func aCancelledCallerStopsRetrying() async throws {
|
|
111
|
+
let memo = GenerationMemo<Int>()
|
|
112
|
+
let gate = Gate()
|
|
113
|
+
let result = Task { try await memo.value { await gate.run() } }
|
|
114
|
+
await gate.waitForRuns(1)
|
|
115
|
+
memo.invalidate()
|
|
116
|
+
result.cancel()
|
|
117
|
+
gate.release()
|
|
118
|
+
await #expect(throws: CancellationError.self) { try await result.value }
|
|
119
|
+
#expect(gate.runs == 1)
|
|
120
|
+
}
|
|
121
|
+
}
|