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.
Files changed (137) hide show
  1. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +3 -4
  2. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +2 -2
  3. package/ios/AVPlayerEngine.swift +334 -0
  4. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  5. package/ios/Browser/BrowseGate.swift +58 -0
  6. package/ios/Browser/BrowserConfig.swift +31 -89
  7. package/ios/Browser/BrowserManager.swift +68 -487
  8. package/ios/Browser/NavigationErrorState.swift +41 -0
  9. package/ios/Browser/PageResolver.swift +227 -0
  10. package/ios/Browser/RequestLayer.swift +90 -0
  11. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +175 -107
  12. package/ios/CarPlay/CPInterfaceController+Safe.swift +11 -24
  13. package/ios/CarPlay/CarPlayArtworkResolver.swift +35 -83
  14. package/ios/CarPlay/CarPlayController.swift +42 -1442
  15. package/ios/CarPlay/CarPlayImageLoader.swift +42 -83
  16. package/ios/CarPlay/CarPlayListItemFactory.swift +9 -23
  17. package/ios/CarPlay/CarPlayNowPlayingManager.swift +119 -129
  18. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  19. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  20. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  21. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  22. package/ios/CarPlay/CarPlaySession.swift +209 -0
  23. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  24. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  25. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  26. package/ios/CarPlay/RNABMediaAddHandler.swift +2 -18
  27. package/ios/CarPlay/RNABMediaAffinityHandler.swift +5 -21
  28. package/ios/CarPlay/RNABMediaIntentHandler.swift +2 -3
  29. package/ios/Extension/NavigationError+Conversion.swift +7 -4
  30. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  31. package/ios/Extension/URLError+Connectivity.swift +15 -0
  32. package/ios/Http/HttpClient.swift +63 -145
  33. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  34. package/ios/HybridAudioBrowser+Options.swift +144 -0
  35. package/ios/HybridAudioBrowser+Player.swift +397 -0
  36. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  37. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  38. package/ios/HybridAudioBrowser.swift +74 -1687
  39. package/ios/Model/NitroTypeStubs.swift +18 -1
  40. package/ios/Model/PlayerUpdateOptions.swift +1 -1
  41. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  42. package/ios/Observer/ItemObserver.swift +90 -0
  43. package/ios/Observer/PlayerObserver.swift +59 -0
  44. package/ios/Option/SessionCategories.swift +41 -92
  45. package/ios/Player/AudioSessionController.swift +177 -0
  46. package/ios/Player/DeferredSeek.swift +70 -0
  47. package/ios/Player/ErrorStatus.swift +27 -0
  48. package/ios/Player/MediaLoader.swift +24 -11
  49. package/ios/Player/NowPlayingUpdater.swift +157 -58
  50. package/ios/Player/PlaybackFailure.swift +125 -0
  51. package/ios/Player/PlaybackPersistence.swift +73 -0
  52. package/ios/Player/Player.swift +1083 -0
  53. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  54. package/ios/Player/PlayerEngine.swift +89 -0
  55. package/ios/Player/PlayerStatusTypes.swift +2 -6
  56. package/ios/Player/QueueManager.swift +52 -59
  57. package/ios/Player/RetryLoop.swift +261 -0
  58. package/ios/Player/SleepTimerManager.swift +1 -1
  59. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +2 -3
  60. package/ios/RemoteCommand/RemoteCommandController.swift +85 -367
  61. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -356
  62. package/ios/Tests/DeferredSeekTests.swift +70 -0
  63. package/ios/Tests/EmitterTests.swift +29 -0
  64. package/ios/Tests/GenerationMemoTests.swift +121 -0
  65. package/ios/Tests/LRUCacheTests.swift +4 -5
  66. package/ios/Tests/MediaLoaderTests.swift +12 -33
  67. package/ios/Tests/ObserverTests.swift +88 -0
  68. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  69. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  70. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  71. package/ios/Tests/PlayerPolicyTests.swift +405 -0
  72. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +298 -248
  73. package/ios/Tests/PlayingStateTests.swift +42 -0
  74. package/ios/Tests/QueueManagerTests.swift +76 -107
  75. package/ios/Tests/ReadyValueTests.swift +73 -0
  76. package/ios/Tests/RemoteCommandControllerTests.swift +10 -30
  77. package/ios/Tests/RemoteCommandTests.swift +2 -4
  78. package/ios/Tests/RequestLayerTests.swift +112 -0
  79. package/ios/Tests/RetryLoopTests.swift +270 -0
  80. package/ios/Tests/Support/TestTypes.swift +111 -112
  81. package/ios/Tests/TrackIdTemplateTests.swift +1 -1
  82. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  83. package/ios/TrackSelector.swift +1 -1
  84. package/ios/Util/ArtworkImageFetcher.swift +42 -0
  85. package/ios/Util/Emitter.swift +42 -0
  86. package/ios/Util/GenerationMemo.swift +36 -0
  87. package/ios/Util/LRUCache.swift +23 -162
  88. package/ios/Util/NetworkMonitor.swift +1 -1
  89. package/ios/Util/ReadyValue.swift +56 -0
  90. package/ios/Util/SFSymbolRenderer.swift +35 -29
  91. package/ios/Util/SVGProcessor.swift +0 -10
  92. package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
  93. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  94. package/lib/module/web/NativeAudioBrowser.js +1 -1
  95. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  96. package/lib/typescript/src/types/browser.d.ts +3 -3
  97. package/package.json +1 -1
  98. package/src/types/browser.ts +3 -3
  99. package/src/web/NativeAudioBrowser.ts +1 -1
  100. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  101. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  102. package/ios/Bridge.h +0 -8
  103. package/ios/Browser/MediaResolveComposer.swift +0 -114
  104. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  105. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  106. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  107. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  108. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  109. package/ios/Observer/PlayerStateObserver.swift +0 -50
  110. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  111. package/ios/Option/PitchAlgorithms.swift +0 -17
  112. package/ios/Option/TimeEventFrequency.swift +0 -18
  113. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  114. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  115. package/ios/Player/PlaybackCoordinator.swift +0 -761
  116. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  117. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  118. package/ios/Player/PlaybackStateStore.swift +0 -27
  119. package/ios/Player/PlayingStateManager.swift +0 -33
  120. package/ios/Player/RetryManager.swift +0 -384
  121. package/ios/Player/SeekCompletionHandler.swift +0 -5
  122. package/ios/Player/SleepTimerHandling.swift +0 -28
  123. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  124. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -327
  125. package/ios/Tests/MediaResolveTests.swift +0 -126
  126. package/ios/Tests/OnceValueTests.swift +0 -79
  127. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -549
  128. package/ios/Tests/PlaybackStateStoreTests.swift +0 -49
  129. package/ios/Tests/PlayerTimeObserverTests.swift +0 -37
  130. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  131. package/ios/Tests/RetryManagerTests.swift +0 -178
  132. package/ios/TrackPlayer.swift +0 -1042
  133. package/ios/TrackPlayerCallbacks.swift +0 -53
  134. package/ios/Util/MetadataAdapter.swift +0 -131
  135. package/ios/Util/OnceValue.swift +0 -104
  136. /package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +0 -0
  137. /package/ios/{Browser → CarPlay}/SectionPresentation.swift +0 -0
@@ -2,395 +2,99 @@ import Testing
2
2
 
3
3
  @testable import AudioBrowserTestable
4
4
 
5
- // MARK: - SF Symbol Detection
6
-
7
- @Suite("SF symbol artwork")
8
- struct SFSymbolTests {
9
- @Test func sfSymbolArtwork_returnsSfSymbolAction() async {
10
- let action = await CarPlayArtworkResolver.resolve(
11
- artwork: "sf:heart.fill",
12
- artworkSourceUri: nil,
13
- stencil: false,
14
- targetWidth: 40, targetHeight: 40,
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
- @Test func sfSymbolWithParams_returnsSfSymbolAction() async {
22
- let action = await CarPlayArtworkResolver.resolve(
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 sfSymbolTakesPriorityOverUrlResolver() async {
34
- var resolverCalled = false
35
- let action = await CarPlayArtworkResolver.resolve(
36
- artwork: "sf:music.note",
37
- artworkSourceUri: nil,
38
- stencil: false,
39
- targetWidth: 40, targetHeight: 40,
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 sfSymbolInArtworkSource_usedAsFallback() async {
51
- let action = await CarPlayArtworkResolver.resolve(
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
- @Suite("no artwork")
66
- struct NoArtworkTests {
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 noArtworkOrSource_withResolver_returnsNone() async {
80
- let action = await CarPlayArtworkResolver.resolve(
81
- artwork: nil,
82
- artworkSourceUri: nil,
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
- @Suite("URL resolution")
95
- struct URLResolutionTests {
96
- @Test func resolvedUrl_returnsFetchWithHeaders() async {
97
- let action = await CarPlayArtworkResolver.resolve(
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 resolvedUrl_noHeaders_returnsFetchWithNilHeaders() async {
121
- let action = await CarPlayArtworkResolver.resolve(
122
- artwork: "https://example.com/art.jpg",
123
- artworkSourceUri: nil,
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 resolverReturnsNil_fallsBackToDirect() async {
140
- let action = await CarPlayArtworkResolver.resolve(
141
- artwork: "https://example.com/art.jpg",
142
- artworkSourceUri: nil,
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 resolvedInvalidUrl_fallsBackToDirect() async {
157
- let action = await CarPlayArtworkResolver.resolve(
158
- artwork: "https://example.com/fallback.jpg",
159
- artworkSourceUri: nil,
160
- stencil: false,
161
- targetWidth: 40, targetHeight: 40,
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 displayScale_appliedToPixelDimensions() async {
178
- var receivedWidth: Double?
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
- @Suite("direct fallback")
200
- struct DirectFallbackTests {
201
- @Test func noResolver_usesArtworkUrl() async {
202
- let action = await CarPlayArtworkResolver.resolve(
203
- artwork: "https://example.com/artwork.png",
204
- artworkSourceUri: nil,
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 noArtwork_usesArtworkSourceUri() async {
219
- let action = await CarPlayArtworkResolver.resolve(
220
- artwork: nil,
221
- artworkSourceUri: "https://example.com/source.jpg",
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(action == .fetch(
228
- uri: "https://example.com/source.jpg",
229
- headers: nil,
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
+ }