react-native-audio-browser 0.2.0-next.1 → 0.2.0-next.11
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/README.md +22 -18
- package/android/build.gradle +13 -10
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
- package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/ios/Browser/BrowserManager.swift +2 -27
- package/ios/Browser/SearchDrillIn.swift +1 -1
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/HybridAudioBrowser.swift +87 -56
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +4 -4
- package/ios/Player/MediaLoader.swift +3 -1
- package/ios/Player/PlaybackCoordinator.swift +4 -1
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/QueueManager.swift +10 -3
- package/ios/Player/RetryManager.swift +79 -18
- package/ios/Player/ShuffleOrder.swift +10 -0
- package/ios/Player/SleepTimerManager.swift +43 -15
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +10 -13
- package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +50 -4
- package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SectionScopeTests.swift +2 -2
- package/ios/Tests/SleepTimerManagerTests.swift +22 -19
- package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
- package/ios/Tests/Support/TestTypes.swift +1 -1
- package/ios/Tests/TabBarEntriesTests.swift +7 -7
- package/ios/TrackPlayer.swift +6 -1
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/Util/LRUCache.swift +4 -1
- package/lib/commonjs/AudioBrowser.js +0 -11
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser.js +1 -1
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +1 -1
- package/lib/commonjs/features/playback/state.js +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +15 -8
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +21 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +1 -1
- package/lib/commonjs/features/queue/queue.js +2 -2
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +1 -1
- package/lib/module/AudioBrowser.js +1 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser.js +1 -1
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +1 -1
- package/lib/module/features/playback/state.js +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +14 -7
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +17 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +1 -1
- package/lib/module/features/queue/queue.js +2 -2
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +8 -5
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +0 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +87 -37
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +2 -2
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/package.json +12 -4
- package/src/AudioBrowser.ts +1 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser.ts +1 -1
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +1 -1
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +254 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +1 -1
- package/src/features/playback/state.ts +1 -1
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +0 -5
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +99 -57
- package/src/features/player/setup.test.ts +12 -4
- package/src/features/player/setup.ts +19 -9
- package/src/features/player/validateOptions.ts +19 -0
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.ts +2 -2
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +237 -0
- package/src/public-surface.test.ts +107 -0
- package/src/specs/audio-browser.nitro.ts +1 -1
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/utils/LazyNativeEmitter.ts +9 -6
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/browser/BrowserManager.ts +1 -1
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/** @vitest-environment happy-dom */
|
|
2
|
+
|
|
3
|
+
import { renderHook, act, cleanup } from '@testing-library/react'
|
|
4
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The 22 hooks that are a single delegation to `useNativeUpdatedValue`. Their
|
|
8
|
+
* behaviour is covered once in `utils/useNativeUpdatedValue.test.ts`; what is
|
|
9
|
+
* per-hook and untestable there is the wiring — which native callback slot the
|
|
10
|
+
* emitter claims, and which `eventKey` is pulled out of the payload.
|
|
11
|
+
*
|
|
12
|
+
* Both are easy to get wrong and silent when wrong: the hook simply never
|
|
13
|
+
* updates, or yields the whole event where a field was meant. Five slots
|
|
14
|
+
* already differ in name from the emitter that claims them
|
|
15
|
+
* (`onPlaybackPlayWhenReadyChanged` behind `onPlayWhenReadyChanged`, and
|
|
16
|
+
* friends), so the names cannot be derived — they have to be pinned.
|
|
17
|
+
*/
|
|
18
|
+
const slots = vi.hoisted(() => new Map<string, (event: unknown) => void>())
|
|
19
|
+
|
|
20
|
+
vi.mock('../native', () => ({
|
|
21
|
+
nativeBrowser: new Proxy({} as Record<string, unknown>, {
|
|
22
|
+
get(target, prop: string) {
|
|
23
|
+
// Getters are the pre-native fallback; undefined stands in for "native
|
|
24
|
+
// has not answered yet", which is what these tests start from.
|
|
25
|
+
target[prop] ??= vi.fn(() => undefined)
|
|
26
|
+
return target[prop]
|
|
27
|
+
},
|
|
28
|
+
set(target, prop: string, value) {
|
|
29
|
+
if (typeof value === 'function') slots.set(prop, value)
|
|
30
|
+
target[prop] = value
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const { useBatteryOptimizationStatus, useBatteryWarningPending } =
|
|
37
|
+
await import('./battery')
|
|
38
|
+
const { useContent, usePath, useTabs } = await import('./browser')
|
|
39
|
+
const { useCarConnected } = await import('./carConnection')
|
|
40
|
+
const { useEqualizerSettings } = await import('./equalizer')
|
|
41
|
+
const { useFormattedNavigationError, useNavigationError, usePlaybackError } =
|
|
42
|
+
await import('./errors')
|
|
43
|
+
const { useOnline } = await import('./network')
|
|
44
|
+
const { useNowPlaying } = await import('./nowPlaying')
|
|
45
|
+
const { useOutput } = await import('./output')
|
|
46
|
+
const { useOptions } = await import('./player/options')
|
|
47
|
+
const { usePlayingState } = await import('./playback/playing')
|
|
48
|
+
const { usePlayWhenReady } = await import('./playback/playWhenReady')
|
|
49
|
+
const { useProgress } = await import('./playback/progress')
|
|
50
|
+
const { usePlayback } = await import('./playback/state')
|
|
51
|
+
const { useSystemVolume } = await import('./playback/volume')
|
|
52
|
+
const { useActiveTrack } = await import('./queue/activeTrack')
|
|
53
|
+
const { useQueue } = await import('./queue/queue')
|
|
54
|
+
const { useRepeatMode } = await import('./queue/repeatMode')
|
|
55
|
+
const { useShuffle } = await import('./queue/shuffle')
|
|
56
|
+
|
|
57
|
+
afterEach(cleanup)
|
|
58
|
+
|
|
59
|
+
type Wiring = {
|
|
60
|
+
name: string
|
|
61
|
+
hook: () => unknown
|
|
62
|
+
/** The property on the Nitro object the emitter assigns its callback to. */
|
|
63
|
+
slot: string
|
|
64
|
+
event: unknown
|
|
65
|
+
/** What the hook returns — differs from `event` exactly when an eventKey applies. */
|
|
66
|
+
expected: unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const wirings: Wiring[] = [
|
|
70
|
+
// Payload passed through whole (no eventKey).
|
|
71
|
+
{
|
|
72
|
+
name: 'useOutput',
|
|
73
|
+
hook: useOutput,
|
|
74
|
+
slot: 'onOutputChanged',
|
|
75
|
+
event: { type: 'bluetooth' },
|
|
76
|
+
expected: { type: 'bluetooth' }
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'useNowPlaying',
|
|
80
|
+
hook: useNowPlaying,
|
|
81
|
+
slot: 'onNowPlayingChanged',
|
|
82
|
+
event: { title: 'Song' },
|
|
83
|
+
expected: { title: 'Song' }
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'useOnline',
|
|
87
|
+
hook: useOnline,
|
|
88
|
+
slot: 'onOnlineChanged',
|
|
89
|
+
event: true,
|
|
90
|
+
expected: true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'useCarConnected',
|
|
94
|
+
hook: useCarConnected,
|
|
95
|
+
slot: 'onCarConnectedChanged',
|
|
96
|
+
event: true,
|
|
97
|
+
expected: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'useEqualizerSettings',
|
|
101
|
+
hook: useEqualizerSettings,
|
|
102
|
+
slot: 'onEqualizerChanged',
|
|
103
|
+
event: { enabled: true },
|
|
104
|
+
expected: { enabled: true }
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'usePath',
|
|
108
|
+
hook: usePath,
|
|
109
|
+
slot: 'onPathChanged',
|
|
110
|
+
event: '/artists/1',
|
|
111
|
+
expected: '/artists/1'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'useContent',
|
|
115
|
+
hook: useContent,
|
|
116
|
+
slot: 'onContentChanged',
|
|
117
|
+
event: { src: 'a' },
|
|
118
|
+
expected: { src: 'a' }
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'useTabs',
|
|
122
|
+
hook: useTabs,
|
|
123
|
+
slot: 'onTabsChanged',
|
|
124
|
+
event: [{ src: 'a' }],
|
|
125
|
+
expected: [{ src: 'a' }]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'usePlayback',
|
|
129
|
+
hook: usePlayback,
|
|
130
|
+
slot: 'onPlaybackChanged',
|
|
131
|
+
event: { state: 'playing' },
|
|
132
|
+
expected: { state: 'playing' }
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'useSystemVolume',
|
|
136
|
+
hook: useSystemVolume,
|
|
137
|
+
slot: 'onSystemVolumeChanged',
|
|
138
|
+
event: 0.4,
|
|
139
|
+
expected: 0.4
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'useProgress',
|
|
143
|
+
hook: useProgress,
|
|
144
|
+
slot: 'onPlaybackProgressUpdated',
|
|
145
|
+
event: { position: 1, duration: 2, buffered: 3 },
|
|
146
|
+
expected: { position: 1, duration: 2, buffered: 3 }
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'usePlayingState',
|
|
150
|
+
hook: usePlayingState,
|
|
151
|
+
slot: 'onPlaybackPlayingState',
|
|
152
|
+
event: { state: 'ready' },
|
|
153
|
+
expected: { state: 'ready' }
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'useShuffle',
|
|
157
|
+
hook: useShuffle,
|
|
158
|
+
slot: 'onPlaybackShuffleModeChanged',
|
|
159
|
+
event: true,
|
|
160
|
+
expected: true
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'useQueue',
|
|
164
|
+
hook: useQueue,
|
|
165
|
+
slot: 'onPlaybackQueueChanged',
|
|
166
|
+
event: [{ src: 'a' }],
|
|
167
|
+
expected: [{ src: 'a' }]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'useFormattedNavigationError',
|
|
171
|
+
hook: useFormattedNavigationError,
|
|
172
|
+
slot: 'onFormattedNavigationError',
|
|
173
|
+
event: { title: 'Offline' },
|
|
174
|
+
expected: { title: 'Offline' }
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'useOptions',
|
|
178
|
+
hook: useOptions,
|
|
179
|
+
slot: 'onOptionsChanged',
|
|
180
|
+
event: { playbackSpeed: 1.5 },
|
|
181
|
+
expected: { playbackSpeed: 1.5 }
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// A field lifted out of the payload — the eventKey cases.
|
|
185
|
+
{
|
|
186
|
+
name: 'usePlaybackError',
|
|
187
|
+
hook: usePlaybackError,
|
|
188
|
+
slot: 'onPlaybackError',
|
|
189
|
+
event: { error: { code: 'network' } },
|
|
190
|
+
expected: { code: 'network' }
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'useNavigationError',
|
|
194
|
+
hook: useNavigationError,
|
|
195
|
+
slot: 'onNavigationError',
|
|
196
|
+
event: { error: { code: 'notFound' } },
|
|
197
|
+
expected: { code: 'notFound' }
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'usePlayWhenReady',
|
|
201
|
+
hook: usePlayWhenReady,
|
|
202
|
+
slot: 'onPlaybackPlayWhenReadyChanged',
|
|
203
|
+
event: { playWhenReady: true },
|
|
204
|
+
expected: true
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'useRepeatMode',
|
|
208
|
+
hook: useRepeatMode,
|
|
209
|
+
slot: 'onPlaybackRepeatModeChanged',
|
|
210
|
+
event: { repeatMode: 'queue' },
|
|
211
|
+
expected: 'queue'
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'useActiveTrack',
|
|
215
|
+
hook: useActiveTrack,
|
|
216
|
+
slot: 'onPlaybackActiveTrackChanged',
|
|
217
|
+
event: { track: { src: 'a' } },
|
|
218
|
+
expected: { src: 'a' }
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'useBatteryWarningPending',
|
|
222
|
+
hook: useBatteryWarningPending,
|
|
223
|
+
slot: 'onBatteryWarningPendingChanged',
|
|
224
|
+
event: { pending: true },
|
|
225
|
+
expected: true
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'useBatteryOptimizationStatus',
|
|
229
|
+
hook: useBatteryOptimizationStatus,
|
|
230
|
+
slot: 'onBatteryOptimizationStatusChanged',
|
|
231
|
+
event: { status: 'unrestricted' },
|
|
232
|
+
expected: 'unrestricted'
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
describe.each(wirings)('$name', ({ hook, slot, event, expected }) => {
|
|
237
|
+
it(`reads $slot`, () => {
|
|
238
|
+
// A slot nothing claimed means the emitter assigns a different property
|
|
239
|
+
// than the one named here — the hook would never update in production.
|
|
240
|
+
expect(slots.has(slot)).toBe(true)
|
|
241
|
+
|
|
242
|
+
const { result } = renderHook(() => hook())
|
|
243
|
+
act(() => slots.get(slot)!(event))
|
|
244
|
+
expect(result.current).toEqual(expected)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('hook wiring table', () => {
|
|
249
|
+
it('covers every delegating hook exactly once', () => {
|
|
250
|
+
const names = wirings.map((w) => w.name)
|
|
251
|
+
expect(new Set(names).size).toBe(names.length)
|
|
252
|
+
expect(new Set(wirings.map((w) => w.slot)).size).toBe(wirings.length)
|
|
253
|
+
})
|
|
254
|
+
})
|
package/src/features/metadata.ts
CHANGED
|
@@ -98,8 +98,13 @@ export interface ChapterMetadata {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Subscribes to chapter metadata events.
|
|
101
|
+
*
|
|
102
|
+
* @platform ios android
|
|
103
|
+
* Never fires on web — the web player does not read chapter markers. See
|
|
104
|
+
* {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
|
|
105
|
+
*
|
|
101
106
|
* @param callback - Called when chapter metadata is received
|
|
102
|
-
* @returns
|
|
107
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
103
108
|
*/
|
|
104
109
|
export const onChapterMetadata = NativeUpdatedValue.emitterize<
|
|
105
110
|
ChapterMetadata[]
|
|
@@ -107,8 +112,13 @@ export const onChapterMetadata = NativeUpdatedValue.emitterize<
|
|
|
107
112
|
|
|
108
113
|
/**
|
|
109
114
|
* Subscribes to track metadata events.
|
|
115
|
+
*
|
|
116
|
+
* @platform ios android
|
|
117
|
+
* Never fires on web — the web player does not read embedded file tags. See
|
|
118
|
+
* {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
|
|
119
|
+
*
|
|
110
120
|
* @param callback - Called when static track metadata is received
|
|
111
|
-
* @returns
|
|
121
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
112
122
|
*/
|
|
113
123
|
export const onTrackMetadata = NativeUpdatedValue.emitterize<TrackMetadata>(
|
|
114
124
|
(cb) => (nativeBrowser.onTrackMetadata = cb)
|
|
@@ -116,8 +126,15 @@ export const onTrackMetadata = NativeUpdatedValue.emitterize<TrackMetadata>(
|
|
|
116
126
|
|
|
117
127
|
/**
|
|
118
128
|
* Subscribes to timed metadata events (ICY/ID3 from live streams).
|
|
129
|
+
*
|
|
130
|
+
* @platform ios android
|
|
131
|
+
* Never fires on web. ICY metadata is stripped by the browser before the player
|
|
132
|
+
* can see it, and no browser API exposes it; in-band ID3 from HLS is reachable
|
|
133
|
+
* but not yet wired. See
|
|
134
|
+
* {@link https://github.com/radio-garden/react-native-audio-browser/issues/118 | #118}.
|
|
135
|
+
*
|
|
119
136
|
* @param callback - Called when stream metadata is received (title, artist from ICY/ID3 tags)
|
|
120
|
-
* @returns
|
|
137
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
121
138
|
*/
|
|
122
139
|
export const onTimedMetadata = NativeUpdatedValue.emitterize<TimedMetadata>(
|
|
123
140
|
(cb) => (nativeBrowser.onTimedMetadata = cb)
|
package/src/features/network.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function getOnline(): boolean {
|
|
|
17
17
|
/**
|
|
18
18
|
* Subscribes to network connectivity changes.
|
|
19
19
|
* @param callback - Called when network state changes
|
|
20
|
-
* @returns
|
|
20
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
21
21
|
*/
|
|
22
22
|
export const onOnlineChanged = NativeUpdatedValue.emitterize<boolean>(
|
|
23
23
|
(cb) => (nativeBrowser.onOnlineChanged = cb)
|
|
@@ -74,7 +74,7 @@ export function clearNowPlayingFlash(): void {
|
|
|
74
74
|
* Subscribes to now playing metadata change events.
|
|
75
75
|
* Fires when updateNowPlaying is called or when the track changes.
|
|
76
76
|
* @param callback - Called when now playing metadata changes
|
|
77
|
-
* @returns
|
|
77
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
78
78
|
*/
|
|
79
79
|
export const onNowPlayingChanged =
|
|
80
80
|
NativeUpdatedValue.emitterize<NowPlayingMetadata>(
|
package/src/features/output.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function getOutput(): Output | undefined {
|
|
|
46
46
|
/**
|
|
47
47
|
* Subscribes to current-output changes (headphones unplugged, Bluetooth speaker
|
|
48
48
|
* connected, AirPlay/route selected). Fires on iOS and Android 11+; never below.
|
|
49
|
-
* @returns
|
|
49
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
50
50
|
*/
|
|
51
51
|
export const onOutputChanged = NativeUpdatedValue.emitterize<Output>(
|
|
52
52
|
(cb) => (nativeBrowser.onOutputChanged = cb)
|
|
@@ -35,7 +35,7 @@ export function setPlayWhenReady(playWhenReady: boolean): void {
|
|
|
35
35
|
/**
|
|
36
36
|
* Subscribes to play when ready changes.
|
|
37
37
|
* @param callback - Called when playWhenReady changes
|
|
38
|
-
* @returns
|
|
38
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
39
39
|
*/
|
|
40
40
|
export const onPlayWhenReadyChanged =
|
|
41
41
|
NativeUpdatedValue.emitterize<PlaybackPlayWhenReadyChangedEvent>(
|
|
@@ -23,7 +23,7 @@ export function getPlayingState(): PlayingState {
|
|
|
23
23
|
/**
|
|
24
24
|
* Subscribes to playing state changes.
|
|
25
25
|
* @param callback - Called when playing or buffering state changes
|
|
26
|
-
* @returns
|
|
26
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
27
27
|
*/
|
|
28
28
|
export const onPlayingState = NativeUpdatedValue.emitterize<PlayingState>(
|
|
29
29
|
(cb) => (nativeBrowser.onPlaybackPlayingState = cb)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** @vitest-environment happy-dom */
|
|
2
|
+
|
|
3
|
+
import { renderHook, act, cleanup } from '@testing-library/react'
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
5
|
+
import type { Progress } from './progress'
|
|
6
|
+
import {
|
|
7
|
+
appStateListenerCount,
|
|
8
|
+
resetAppState,
|
|
9
|
+
setAppState
|
|
10
|
+
} from '../../test-utils/reactNativeStub'
|
|
11
|
+
|
|
12
|
+
const native = vi.hoisted(() => ({
|
|
13
|
+
getProgress: vi.fn<() => Progress>(() => ({
|
|
14
|
+
position: 0,
|
|
15
|
+
duration: 0,
|
|
16
|
+
buffered: 0
|
|
17
|
+
})),
|
|
18
|
+
onPlaybackChanged: undefined as ((event: unknown) => void) | undefined,
|
|
19
|
+
onPlaybackProgressUpdated: undefined as ((event: unknown) => void) | undefined
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('../../native', () => ({ nativeBrowser: native }))
|
|
23
|
+
|
|
24
|
+
const { usePolledProgress } = await import('./progress')
|
|
25
|
+
|
|
26
|
+
const at = (position: number): Progress => ({
|
|
27
|
+
position,
|
|
28
|
+
duration: 300,
|
|
29
|
+
buffered: position + 10
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.useFakeTimers()
|
|
34
|
+
resetAppState()
|
|
35
|
+
native.getProgress.mockReturnValue(at(0))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
cleanup()
|
|
40
|
+
vi.useRealTimers()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('usePolledProgress', () => {
|
|
44
|
+
it('starts at zero and polls the first value in', () => {
|
|
45
|
+
native.getProgress.mockReturnValue(at(12))
|
|
46
|
+
const { result } = renderHook(() => usePolledProgress())
|
|
47
|
+
// The initial poll runs inside the effect, so the zeroed useState default is
|
|
48
|
+
// never observable to a caller.
|
|
49
|
+
expect(result.current).toEqual(at(12))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('polls on the given interval', () => {
|
|
53
|
+
const { result } = renderHook(() => usePolledProgress(500))
|
|
54
|
+
native.getProgress.mockReturnValue(at(1))
|
|
55
|
+
act(() => {
|
|
56
|
+
vi.advanceTimersByTime(500)
|
|
57
|
+
})
|
|
58
|
+
expect(result.current).toEqual(at(1))
|
|
59
|
+
|
|
60
|
+
native.getProgress.mockReturnValue(at(2))
|
|
61
|
+
act(() => {
|
|
62
|
+
vi.advanceTimersByTime(500)
|
|
63
|
+
})
|
|
64
|
+
expect(result.current).toEqual(at(2))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('does not poll before the interval elapses', () => {
|
|
68
|
+
renderHook(() => usePolledProgress(1000))
|
|
69
|
+
native.getProgress.mockClear()
|
|
70
|
+
act(() => {
|
|
71
|
+
vi.advanceTimersByTime(999)
|
|
72
|
+
})
|
|
73
|
+
expect(native.getProgress).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps the same object when nothing changed', () => {
|
|
77
|
+
const { result } = renderHook(() => usePolledProgress(500))
|
|
78
|
+
const first = result.current
|
|
79
|
+
act(() => {
|
|
80
|
+
vi.advanceTimersByTime(500)
|
|
81
|
+
})
|
|
82
|
+
// Identity, not equality: the hook returns the previous state object so a
|
|
83
|
+
// consumer's useEffect/memo on progress does not re-fire every tick.
|
|
84
|
+
expect(result.current).toBe(first)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('updates immediately when playback changes, without waiting for a tick', () => {
|
|
88
|
+
const { result } = renderHook(() => usePolledProgress(10_000))
|
|
89
|
+
native.getProgress.mockReturnValue(at(7))
|
|
90
|
+
act(() => {
|
|
91
|
+
native.onPlaybackChanged?.({ state: 'playing' })
|
|
92
|
+
})
|
|
93
|
+
expect(result.current).toEqual(at(7))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('survives a getter that throws before setup', () => {
|
|
97
|
+
native.getProgress.mockImplementation(() => {
|
|
98
|
+
throw new Error('not set up')
|
|
99
|
+
})
|
|
100
|
+
const { result } = renderHook(() => usePolledProgress(500))
|
|
101
|
+
expect(result.current).toEqual({ position: 0, duration: 0, buffered: 0 })
|
|
102
|
+
expect(() => act(() => vi.advanceTimersByTime(500))).not.toThrow()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('stops polling in the background and resumes on return', () => {
|
|
106
|
+
const { result } = renderHook(() => usePolledProgress(500))
|
|
107
|
+
|
|
108
|
+
act(() => {
|
|
109
|
+
setAppState('background')
|
|
110
|
+
})
|
|
111
|
+
native.getProgress.mockReturnValue(at(99))
|
|
112
|
+
act(() => {
|
|
113
|
+
vi.advanceTimersByTime(5_000)
|
|
114
|
+
})
|
|
115
|
+
expect(result.current).toEqual(at(0))
|
|
116
|
+
|
|
117
|
+
// Returning to the foreground updates once immediately rather than leaving
|
|
118
|
+
// a stale position on screen until the next interval.
|
|
119
|
+
act(() => {
|
|
120
|
+
setAppState('active')
|
|
121
|
+
})
|
|
122
|
+
expect(result.current).toEqual(at(99))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('ignores playback events while backgrounded', () => {
|
|
126
|
+
const { result } = renderHook(() => usePolledProgress(500))
|
|
127
|
+
act(() => {
|
|
128
|
+
setAppState('background')
|
|
129
|
+
})
|
|
130
|
+
native.getProgress.mockReturnValue(at(42))
|
|
131
|
+
act(() => {
|
|
132
|
+
native.onPlaybackChanged?.({ state: 'playing' })
|
|
133
|
+
})
|
|
134
|
+
expect(result.current).toEqual(at(0))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not stack pollers when backgrounded twice', () => {
|
|
138
|
+
renderHook(() => usePolledProgress(500))
|
|
139
|
+
act(() => {
|
|
140
|
+
setAppState('background')
|
|
141
|
+
})
|
|
142
|
+
act(() => {
|
|
143
|
+
setAppState('active')
|
|
144
|
+
})
|
|
145
|
+
act(() => {
|
|
146
|
+
setAppState('active')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
native.getProgress.mockClear()
|
|
150
|
+
act(() => {
|
|
151
|
+
vi.advanceTimersByTime(500)
|
|
152
|
+
})
|
|
153
|
+
expect(native.getProgress).toHaveBeenCalledTimes(1)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('tears down its timer and subscriptions on unmount', () => {
|
|
157
|
+
const { unmount } = renderHook(() => usePolledProgress(500))
|
|
158
|
+
expect(appStateListenerCount()).toBe(1)
|
|
159
|
+
|
|
160
|
+
unmount()
|
|
161
|
+
expect(appStateListenerCount()).toBe(0)
|
|
162
|
+
|
|
163
|
+
native.getProgress.mockClear()
|
|
164
|
+
act(() => {
|
|
165
|
+
vi.advanceTimersByTime(5_000)
|
|
166
|
+
})
|
|
167
|
+
expect(native.getProgress).not.toHaveBeenCalled()
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -46,7 +46,7 @@ export function getProgress(): Progress {
|
|
|
46
46
|
/**
|
|
47
47
|
* Subscribes to playback progress updates.
|
|
48
48
|
* @param callback - Called periodically with playback progress updates
|
|
49
|
-
* @returns
|
|
49
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
50
50
|
*/
|
|
51
51
|
export const onProgressUpdated =
|
|
52
52
|
NativeUpdatedValue.emitterize<PlaybackProgressUpdatedEvent>(
|
|
@@ -55,7 +55,7 @@ export function getPlayback(): Playback {
|
|
|
55
55
|
/**
|
|
56
56
|
* Subscribes to playback state changes.
|
|
57
57
|
* @param callback - Called when the playback state changes
|
|
58
|
-
* @returns
|
|
58
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
59
59
|
*/
|
|
60
60
|
export const onPlaybackChanged = NativeUpdatedValue.emitterize<Playback>(
|
|
61
61
|
(cb) => (nativeBrowser.onPlaybackChanged = cb)
|
|
@@ -13,12 +13,22 @@ vi.mock('../queue/activeTrack', () => ({
|
|
|
13
13
|
onActiveTrackChanged: { addListener: vi.fn(() => () => {}) }
|
|
14
14
|
}))
|
|
15
15
|
|
|
16
|
-
import { trackPlaybackTime
|
|
16
|
+
import { trackPlaybackTime } from './trackPlaybackTime'
|
|
17
17
|
|
|
18
18
|
beforeEach(() => {
|
|
19
19
|
nativeMock.setPlaybackIntervalEnabled.mockClear()
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
/** Ticks through the slot `install()` assigns, so this covers that wiring too. */
|
|
23
|
+
function tick(times = 1) {
|
|
24
|
+
for (let i = 0; i < times; i++) {
|
|
25
|
+
if (!nativeMock.onPlaybackInterval) {
|
|
26
|
+
throw new Error('trackPlaybackTime never installed onPlaybackInterval')
|
|
27
|
+
}
|
|
28
|
+
nativeMock.onPlaybackInterval()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
describe('trackPlaybackTime', () => {
|
|
23
33
|
it('enables native tick on first subscriber, disables on last', () => {
|
|
24
34
|
const a = trackPlaybackTime(() => {}, 5)
|
|
@@ -38,17 +48,17 @@ describe('trackPlaybackTime', () => {
|
|
|
38
48
|
(e) => calls.push({ total: e.total, sinceLast: e.sinceLast }),
|
|
39
49
|
3
|
|
40
50
|
)
|
|
41
|
-
|
|
51
|
+
tick(2)
|
|
42
52
|
expect(calls).toEqual([])
|
|
43
|
-
|
|
53
|
+
tick(1) // total now 3 → fires
|
|
44
54
|
expect(calls).toEqual([{ total: 3, sinceLast: 3 }])
|
|
45
|
-
|
|
55
|
+
tick(3) // total now 6 → fires again
|
|
46
56
|
expect(calls).toEqual([
|
|
47
57
|
{ total: 3, sinceLast: 3 },
|
|
48
58
|
{ total: 6, sinceLast: 3 }
|
|
49
59
|
])
|
|
50
60
|
cancel()
|
|
51
|
-
|
|
61
|
+
tick(3)
|
|
52
62
|
expect(calls).toHaveLength(2) // cancelled
|
|
53
63
|
})
|
|
54
64
|
|
|
@@ -57,7 +67,7 @@ describe('trackPlaybackTime', () => {
|
|
|
57
67
|
const slow: number[] = []
|
|
58
68
|
const cancelFast = trackPlaybackTime((e) => fast.push(e.total), 1)
|
|
59
69
|
const cancelSlow = trackPlaybackTime((e) => slow.push(e.total), 2)
|
|
60
|
-
|
|
70
|
+
tick(2)
|
|
61
71
|
expect(fast).toEqual([1, 2])
|
|
62
72
|
expect(slow).toEqual([2])
|
|
63
73
|
cancelFast()
|
|
@@ -99,8 +99,3 @@ export function trackPlaybackTime(
|
|
|
99
99
|
if (handles.size === 0) nativeBrowser.setPlaybackIntervalEnabled(false)
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
/** @internal test hook — drives the fan-out without a native tick. */
|
|
104
|
-
export function __emitTickForTests(ticks = 1) {
|
|
105
|
-
for (let i = 0; i < ticks; i++) onTick()
|
|
106
|
-
}
|
|
@@ -42,7 +42,7 @@ export function setSystemVolume(level: number): void {
|
|
|
42
42
|
/**
|
|
43
43
|
* Subscribes to system volume changes.
|
|
44
44
|
* @param callback - Called when the system volume changes
|
|
45
|
-
* @returns
|
|
45
|
+
* @returns An emitter — subscribe with `addListener(callback)`, which returns a cleanup function
|
|
46
46
|
*/
|
|
47
47
|
export const onSystemVolumeChanged = NativeUpdatedValue.emitterize<number>(
|
|
48
48
|
(cb) => (nativeBrowser.onSystemVolumeChanged = cb)
|