react-native-audio-browser 0.2.0-next.22 → 0.2.0-next.24
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/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +9 -1
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +6 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +14 -0
- package/ios/Browser/BrowserPathHelper.swift +6 -2
- package/ios/CarPlay/CPInterfaceController+Safe.swift +93 -23
- package/ios/CarPlay/CarPlayController.swift +5 -1
- package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
- package/ios/Extension/NavigationError+Conversion.swift +7 -0
- package/ios/HybridAudioBrowser.swift +4 -1
- package/ios/Tests/BrowserPathHelperTests.swift +11 -0
- package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
- package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +6 -0
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/web/browser/NavigationErrorManager.js +3 -1
- package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +6 -0
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +8 -1
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +9 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +2 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +2 -0
- package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +3 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
- package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
- package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
- package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
- package/package.json +1 -1
- package/src/types/browser.ts +9 -1
- package/src/web/browser/NavigationErrorManager.ts +3 -1
- package/src/web/util/BrowserPathHelper.test.ts +16 -0
- package/src/web/util/BrowserPathHelper.ts +9 -0
- package/website/guide/carplay.md +30 -0
|
@@ -519,7 +519,15 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
|
|
|
519
519
|
val default = defaultFormattedError(error)
|
|
520
520
|
val formatter = _configuration.formatNavigationError ?: return default
|
|
521
521
|
return formattedOrDefault(mainScope, default) {
|
|
522
|
-
formatter(
|
|
522
|
+
formatter(
|
|
523
|
+
FormatNavigationErrorParams(
|
|
524
|
+
error,
|
|
525
|
+
default,
|
|
526
|
+
path,
|
|
527
|
+
BrowserPathHelper.navigationErrorKind(path),
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
.await()
|
|
523
531
|
}
|
|
524
532
|
}
|
|
525
533
|
|
|
@@ -2,6 +2,7 @@ package com.audiobrowser.util
|
|
|
2
2
|
|
|
3
3
|
import android.net.Uri
|
|
4
4
|
import androidx.core.net.toUri
|
|
5
|
+
import com.margelo.nitro.audiobrowser.NavigationErrorKind
|
|
5
6
|
import java.net.URLEncoder
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -49,6 +50,11 @@ object BrowserPathHelper {
|
|
|
49
50
|
// Query parameter name for the tapped child's page position (tie-breaker)
|
|
50
51
|
private const val CONTEXTUAL_INDEX_PARAM = "__index"
|
|
51
52
|
|
|
53
|
+
/** Whether a navigation error on this path came from a browse or a search */
|
|
54
|
+
fun navigationErrorKind(path: String): NavigationErrorKind =
|
|
55
|
+
if (path.startsWith("$SEARCH_PATH_PREFIX?")) NavigationErrorKind.SEARCH
|
|
56
|
+
else NavigationErrorKind.BROWSE
|
|
57
|
+
|
|
52
58
|
/** Check if a path is a special system path (not a regular navigation path) */
|
|
53
59
|
fun isSpecialPath(path: String): Boolean {
|
|
54
60
|
return path == ROOT_PATH || path.startsWith("$SEARCH_PATH_PREFIX?")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.audiobrowser.util
|
|
2
2
|
|
|
3
|
+
import com.margelo.nitro.audiobrowser.NavigationErrorKind
|
|
3
4
|
import org.junit.Assert.assertEquals
|
|
4
5
|
import org.junit.Assert.assertNull
|
|
5
6
|
import org.junit.Test
|
|
@@ -50,4 +51,17 @@ class BrowserPathHelperTest {
|
|
|
50
51
|
assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x"))
|
|
51
52
|
assertNull(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1"))
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `a search path is a search, anything else a browse`() {
|
|
57
|
+
assertEquals(
|
|
58
|
+
NavigationErrorKind.SEARCH,
|
|
59
|
+
BrowserPathHelper.navigationErrorKind(BrowserPathHelper.createSearchPath("jazz")),
|
|
60
|
+
)
|
|
61
|
+
assertEquals(
|
|
62
|
+
NavigationErrorKind.BROWSE,
|
|
63
|
+
BrowserPathHelper.navigationErrorKind("/search?q=jazz"),
|
|
64
|
+
)
|
|
65
|
+
assertEquals(NavigationErrorKind.BROWSE, BrowserPathHelper.navigationErrorKind("/__searchable"))
|
|
66
|
+
}
|
|
53
67
|
}
|
|
@@ -44,8 +44,12 @@ enum BrowserPathHelper {
|
|
|
44
44
|
|
|
45
45
|
/// Check if a path is a special system path (not a regular navigation path)
|
|
46
46
|
static func isSpecialPath(_ path: String) -> Bool {
|
|
47
|
-
path == rootPath ||
|
|
48
|
-
|
|
47
|
+
path == rootPath || isSearchPath(path)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Check if a path is a search path (`/__search?q=…`)
|
|
51
|
+
static func isSearchPath(_ path: String) -> Bool {
|
|
52
|
+
path.hasPrefix("\(searchPathPrefix)?")
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/// Create a search path for a given query
|
|
@@ -12,6 +12,10 @@ import os.log
|
|
|
12
12
|
/// preconditions are checked up front, the underlying call always gets a
|
|
13
13
|
/// completion, and every failure is logged, with an optional caller completion
|
|
14
14
|
/// for call sites that chain on the result.
|
|
15
|
+
///
|
|
16
|
+
/// A push onto a full stack replaces the top template (`TopTemplateReplacer`).
|
|
17
|
+
/// The stack mutations wait for that replacement to settle before they run,
|
|
18
|
+
/// so none of them lands between its pop and its push.
|
|
15
19
|
@MainActor
|
|
16
20
|
extension CPInterfaceController {
|
|
17
21
|
private static let stackLogger = Logger(subsystem: "com.audiobrowser", category: "TemplateStack")
|
|
@@ -26,9 +30,43 @@ extension CPInterfaceController {
|
|
|
26
30
|
/// push — taking down the whole app, not just the CarPlay scene.
|
|
27
31
|
private static let maximumStackDepth = 5
|
|
28
32
|
|
|
33
|
+
private static var topReplacerKey: UInt8 = 0
|
|
34
|
+
|
|
35
|
+
private var topReplacer: TopTemplateReplacer {
|
|
36
|
+
if let replacer = objc_getAssociatedObject(self, &Self.topReplacerKey) as? TopTemplateReplacer {
|
|
37
|
+
return replacer
|
|
38
|
+
}
|
|
39
|
+
let replacer = TopTemplateReplacer()
|
|
40
|
+
objc_setAssociatedObject(self, &Self.topReplacerKey, replacer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
41
|
+
return replacer
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func whenStackSettled(_ mutation: @escaping (CPInterfaceController) -> Void) {
|
|
45
|
+
topReplacer.whenSettled { [weak self] in
|
|
46
|
+
guard let self else { return }
|
|
47
|
+
mutation(self)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Pushes `template`, skipping the raise case (already in the hierarchy). At
|
|
52
|
+
/// the depth limit it replaces the top template.
|
|
53
|
+
func safePush(_ template: CPTemplate, animated: Bool, completion: ((Bool, Error?) -> Void)? = nil) {
|
|
54
|
+
whenStackSettled { controller in
|
|
55
|
+
let templates = controller.templates
|
|
56
|
+
guard templates.count >= Self.maximumStackDepth, !templates.contains(where: { $0 === template }) else {
|
|
57
|
+
controller.guardedPush(template, animated: animated, completion: completion)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
controller.topReplacer.push(
|
|
61
|
+
template, replacingTopOf: GuardedTemplateStack(controller: controller), animated: animated,
|
|
62
|
+
completion: completion,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/// Pushes `template`, skipping the raise cases (already in the hierarchy,
|
|
30
68
|
/// stack at the depth limit).
|
|
31
|
-
func
|
|
69
|
+
fileprivate func guardedPush(_ template: CPTemplate, animated: Bool, completion: ((Bool, Error?) -> Void)?) {
|
|
32
70
|
guard !templates.contains(where: { $0 === template }) else {
|
|
33
71
|
Self.stackLogger.warning("push skipped: \(Self.name(of: template)) is already in the stack")
|
|
34
72
|
completion?(false, nil)
|
|
@@ -48,7 +86,7 @@ extension CPInterfaceController {
|
|
|
48
86
|
}
|
|
49
87
|
|
|
50
88
|
/// Pops the top template, skipping the raise case (nothing above the root).
|
|
51
|
-
func
|
|
89
|
+
fileprivate func guardedPop(animated: Bool, completion: ((Bool, Error?) -> Void)?) {
|
|
52
90
|
guard templates.count > 1 else {
|
|
53
91
|
Self.stackLogger.warning("pop skipped: already at the root template")
|
|
54
92
|
completion?(false, nil)
|
|
@@ -62,41 +100,54 @@ extension CPInterfaceController {
|
|
|
62
100
|
}
|
|
63
101
|
}
|
|
64
102
|
|
|
103
|
+
/// Pops the top template, skipping the raise case (nothing above the root).
|
|
104
|
+
func safePop(animated: Bool, completion: ((Bool, Error?) -> Void)? = nil) {
|
|
105
|
+
whenStackSettled { controller in
|
|
106
|
+
controller.guardedPop(animated: animated, completion: completion)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
/// Pops back to `template`, skipping the raise case (not in the hierarchy).
|
|
66
111
|
/// A no-op success when it is already the top template.
|
|
67
112
|
func safePop(to template: CPTemplate, animated: Bool, completion: ((Bool, Error?) -> Void)? = nil) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
113
|
+
whenStackSettled { controller in
|
|
114
|
+
guard controller.templates.contains(where: { $0 === template }) else {
|
|
115
|
+
Self.stackLogger.warning("pop(to:) skipped: \(Self.name(of: template)) is not in the stack")
|
|
116
|
+
completion?(false, nil)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
guard controller.topTemplate !== template else {
|
|
120
|
+
completion?(true, nil)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
controller.pop(to: template, animated: animated) { popped, error in
|
|
124
|
+
if let error {
|
|
125
|
+
Self.stackLogger.error("pop(to:) \(Self.name(of: template)) failed: \(error.localizedDescription)")
|
|
126
|
+
}
|
|
127
|
+
completion?(popped, error)
|
|
80
128
|
}
|
|
81
|
-
completion?(popped, error)
|
|
82
129
|
}
|
|
83
130
|
}
|
|
84
131
|
|
|
85
132
|
func safePopToRoot(animated: Bool, completion: ((Bool, Error?) -> Void)? = nil) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
whenStackSettled { controller in
|
|
134
|
+
controller.popToRootTemplate(animated: animated) { popped, error in
|
|
135
|
+
if let error {
|
|
136
|
+
Self.stackLogger.error("popToRootTemplate failed: \(error.localizedDescription)")
|
|
137
|
+
}
|
|
138
|
+
completion?(popped, error)
|
|
89
139
|
}
|
|
90
|
-
completion?(popped, error)
|
|
91
140
|
}
|
|
92
141
|
}
|
|
93
142
|
|
|
94
143
|
func safeSetRoot(_ template: CPTemplate, animated: Bool, completion: ((Bool, Error?) -> Void)? = nil) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
whenStackSettled { controller in
|
|
145
|
+
controller.setRootTemplate(template, animated: animated) { set, error in
|
|
146
|
+
if let error {
|
|
147
|
+
Self.stackLogger.error("setRootTemplate \(Self.name(of: template)) failed: \(error.localizedDescription)")
|
|
148
|
+
}
|
|
149
|
+
completion?(set, error)
|
|
98
150
|
}
|
|
99
|
-
completion?(set, error)
|
|
100
151
|
}
|
|
101
152
|
}
|
|
102
153
|
|
|
@@ -124,3 +175,22 @@ extension CPInterfaceController {
|
|
|
124
175
|
}
|
|
125
176
|
}
|
|
126
177
|
}
|
|
178
|
+
|
|
179
|
+
/// The guarded pop and push a replacement is made of, so a push that still
|
|
180
|
+
/// cannot fit skips and logs rather than raising.
|
|
181
|
+
@MainActor
|
|
182
|
+
private struct GuardedTemplateStack: TemplateStack {
|
|
183
|
+
let controller: CPInterfaceController
|
|
184
|
+
|
|
185
|
+
var templates: [CPTemplate] {
|
|
186
|
+
controller.templates
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func popTop(completion: @escaping (Bool, Error?) -> Void) {
|
|
190
|
+
controller.guardedPop(animated: false, completion: completion)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func push(_ template: CPTemplate, animated: Bool, completion: @escaping (Bool, Error?) -> Void) {
|
|
194
|
+
controller.guardedPush(template, animated: animated, completion: completion)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -784,6 +784,7 @@ public final class RNABCarPlayController: NSObject {
|
|
|
784
784
|
guard let formatter = config.formatNavigationError else { return fallback }
|
|
785
785
|
let params = FormatNavigationErrorParams(
|
|
786
786
|
error: navError, defaultFormatted: fallback, path: path,
|
|
787
|
+
kind: NavigationErrorKind(path: path),
|
|
787
788
|
)
|
|
788
789
|
do {
|
|
789
790
|
let custom = try await formatter(params).await()
|
|
@@ -1317,7 +1318,10 @@ public final class RNABCarPlayController: NSObject {
|
|
|
1317
1318
|
if let formatter = config.formatNavigationError {
|
|
1318
1319
|
logger.debug("Calling formatNavigationError callback...")
|
|
1319
1320
|
// Call the JS callback and handle result
|
|
1320
|
-
let params = FormatNavigationErrorParams(
|
|
1321
|
+
let params = FormatNavigationErrorParams(
|
|
1322
|
+
error: error, defaultFormatted: defaultFormatted, path: path,
|
|
1323
|
+
kind: NavigationErrorKind(path: path),
|
|
1324
|
+
)
|
|
1321
1325
|
// The result callbacks are @Sendable and hop back explicitly: Nitro resolves
|
|
1322
1326
|
// its promises synchronously on the JS thread, so a plain closure would
|
|
1323
1327
|
// inherit this type's MainActor isolation and trap on entry under Swift 6's
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os.log
|
|
3
|
+
|
|
4
|
+
/// The stack mutations a top-template replacement is made of:
|
|
5
|
+
/// `CPInterfaceController` in the app, a fake in tests.
|
|
6
|
+
@MainActor
|
|
7
|
+
protocol TemplateStack {
|
|
8
|
+
associatedtype Template: AnyObject
|
|
9
|
+
|
|
10
|
+
/// Root first.
|
|
11
|
+
var templates: [Template] { get }
|
|
12
|
+
|
|
13
|
+
/// Unanimated.
|
|
14
|
+
func popTop(completion: @escaping (Bool, Error?) -> Void)
|
|
15
|
+
func push(_ template: Template, animated: Bool, completion: @escaping (Bool, Error?) -> Void)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Pushes onto a full template stack by replacing its top template — an
|
|
19
|
+
/// unanimated pop, then the push — and holds back every other stack mutation
|
|
20
|
+
/// until both have completed. See "Template Depth" in
|
|
21
|
+
/// `website/guide/carplay.md`.
|
|
22
|
+
@MainActor
|
|
23
|
+
final class TopTemplateReplacer {
|
|
24
|
+
private let logger = Logger(subsystem: "com.audiobrowser", category: "TemplateStack")
|
|
25
|
+
private var deferred: [() -> Void] = []
|
|
26
|
+
|
|
27
|
+
/// True from the pop until the push completes.
|
|
28
|
+
private(set) var isReplacing = false
|
|
29
|
+
|
|
30
|
+
/// Runs `mutation` now, or — while a replacement is in flight — once it has
|
|
31
|
+
/// settled, after the mutations that arrived before it.
|
|
32
|
+
func whenSettled(_ mutation: @escaping () -> Void) {
|
|
33
|
+
guard isReplacing else {
|
|
34
|
+
mutation()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
deferred.append(mutation)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Pushes `template` onto `stack` in place of its top template. When the
|
|
41
|
+
/// push fails, the popped template is pushed back, so the user stays where
|
|
42
|
+
/// they were. `completion` reports the push of `template` itself.
|
|
43
|
+
///
|
|
44
|
+
/// Refuses a stack with nothing above the root to replace, and a call made
|
|
45
|
+
/// while a replacement is in flight: route it through `whenSettled`.
|
|
46
|
+
func push<Stack: TemplateStack>(
|
|
47
|
+
_ template: Stack.Template,
|
|
48
|
+
replacingTopOf stack: Stack,
|
|
49
|
+
animated: Bool,
|
|
50
|
+
completion: ((Bool, Error?) -> Void)? = nil,
|
|
51
|
+
) {
|
|
52
|
+
let templates = stack.templates
|
|
53
|
+
guard !isReplacing, templates.count > 1, let top = templates.last else {
|
|
54
|
+
logger.error("push skipped: \(Self.name(of: template)) has no template to replace (\(templates.count) templates)")
|
|
55
|
+
completion?(false, nil)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
isReplacing = true
|
|
59
|
+
logger.info("push at the depth limit: replacing the top, \(Self.name(of: top)), with \(Self.name(of: template))")
|
|
60
|
+
|
|
61
|
+
stack.popTop { [self] popped, error in
|
|
62
|
+
guard popped else {
|
|
63
|
+
logger.error("replace abandoned: pop failed, \(Self.name(of: template)) not pushed (\(stack.templates.count) templates)")
|
|
64
|
+
settle(completion, pushed: false, error: error)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
stack.push(template, animated: animated) { [self] pushed, error in
|
|
68
|
+
guard !pushed else {
|
|
69
|
+
settle(completion, pushed: true, error: error)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
logger.error("replace failed: \(Self.name(of: template)) not pushed, restoring \(Self.name(of: top))")
|
|
73
|
+
stack.push(top, animated: false) { [self] restored, _ in
|
|
74
|
+
if !restored {
|
|
75
|
+
logger.error("replace failed: \(Self.name(of: top)) not restored (\(stack.templates.count) templates)")
|
|
76
|
+
}
|
|
77
|
+
settle(completion, pushed: false, error: error)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private func settle(_ completion: ((Bool, Error?) -> Void)?, pushed: Bool, error: Error?) {
|
|
84
|
+
// Still replacing here, so a mutation the completion issues queues behind
|
|
85
|
+
// the ones that arrived during the replacement.
|
|
86
|
+
completion?(pushed, error)
|
|
87
|
+
isReplacing = false
|
|
88
|
+
while !isReplacing, !deferred.isEmpty {
|
|
89
|
+
deferred.removeFirst()()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private static func name(of template: AnyObject) -> String {
|
|
94
|
+
String(describing: type(of: template))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -54,3 +54,10 @@ extension NavigationError {
|
|
|
54
54
|
return FormattedNavigationError(title: title, message: message.isEmpty ? nil : message)
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
extension NavigationErrorKind {
|
|
59
|
+
/// The kind of request a navigation error on `path` came from.
|
|
60
|
+
init(path: String) {
|
|
61
|
+
self = BrowserPathHelper.isSearchPath(path) ? .search : .browse
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -465,7 +465,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
465
465
|
// Format the error (async if using JS callback, sync for defaults)
|
|
466
466
|
let defaultFormatted = navError.defaultFormatted()
|
|
467
467
|
if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
|
|
468
|
-
let params = FormatNavigationErrorParams(
|
|
468
|
+
let params = FormatNavigationErrorParams(
|
|
469
|
+
error: navError, defaultFormatted: defaultFormatted, path: path,
|
|
470
|
+
kind: NavigationErrorKind(path: path),
|
|
471
|
+
)
|
|
469
472
|
// Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes.
|
|
470
473
|
// The result callbacks are @Sendable: a plain closure would inherit MainActor
|
|
471
474
|
// isolation from this block, and Nitro resolves its promises synchronously on the
|
|
@@ -166,3 +166,14 @@ import Testing
|
|
|
166
166
|
@Test func isContextualReturnsTrueWithAmpersandSeparator() {
|
|
167
167
|
#expect(BrowserPathHelper.isContextual("/lib?q=1&__trackId=x") == true)
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
// MARK: - isSearchPath
|
|
171
|
+
|
|
172
|
+
@Test func isSearchPathMatchesCreatedSearchPaths() {
|
|
173
|
+
#expect(BrowserPathHelper.isSearchPath(BrowserPathHelper.createSearchPath("jazz")) == true)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test func isSearchPathRejectsBrowsePaths() {
|
|
177
|
+
#expect(BrowserPathHelper.isSearchPath("/search?q=jazz") == false)
|
|
178
|
+
#expect(BrowserPathHelper.isSearchPath("/__searchable") == false)
|
|
179
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
|
|
4
|
+
@testable import AudioBrowserTestable
|
|
5
|
+
|
|
6
|
+
private final class Page {
|
|
7
|
+
let name: String
|
|
8
|
+
init(_ name: String) { self.name = name }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// A template stack whose mutations complete only when the test steps them,
|
|
12
|
+
/// the way CarPlay's complete on a later run-loop turn.
|
|
13
|
+
@MainActor
|
|
14
|
+
private final class FakeStack: TemplateStack {
|
|
15
|
+
private(set) var templates: [Page]
|
|
16
|
+
private(set) var calls: [String] = []
|
|
17
|
+
var failingPushes: Set<String> = []
|
|
18
|
+
var failsPop = false
|
|
19
|
+
private var pending: [() -> Void] = []
|
|
20
|
+
|
|
21
|
+
init(_ names: String...) {
|
|
22
|
+
templates = names.map(Page.init)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var names: [String] {
|
|
26
|
+
templates.map(\.name)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func popTop(completion: @escaping (Bool, Error?) -> Void) {
|
|
30
|
+
calls.append("pop")
|
|
31
|
+
pending.append { [self] in
|
|
32
|
+
guard !failsPop else {
|
|
33
|
+
completion(false, nil)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
templates.removeLast()
|
|
37
|
+
completion(true, nil)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func push(_ template: Page, animated: Bool, completion: @escaping (Bool, Error?) -> Void) {
|
|
42
|
+
calls.append("push \(template.name)\(animated ? " animated" : "")")
|
|
43
|
+
pending.append { [self] in
|
|
44
|
+
guard !failingPushes.contains(template.name) else {
|
|
45
|
+
completion(false, nil)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
templates.append(template)
|
|
49
|
+
completion(true, nil)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func step() {
|
|
54
|
+
pending.removeFirst()()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func run() {
|
|
58
|
+
while !pending.isEmpty {
|
|
59
|
+
step()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Suite("TopTemplateReplacer")
|
|
65
|
+
@MainActor
|
|
66
|
+
struct TopTemplateReplacerTests {
|
|
67
|
+
private let replacer = TopTemplateReplacer()
|
|
68
|
+
private let stack = FakeStack("tabs", "A", "B", "C", "D")
|
|
69
|
+
|
|
70
|
+
@Test func replacesTheTopPageAndKeepsTheRest() {
|
|
71
|
+
var result: Bool?
|
|
72
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true) { pushed, _ in result = pushed }
|
|
73
|
+
stack.run()
|
|
74
|
+
|
|
75
|
+
#expect(stack.names == ["tabs", "A", "B", "C", "E"])
|
|
76
|
+
#expect(stack.calls == ["pop", "push E animated"])
|
|
77
|
+
#expect(result == true)
|
|
78
|
+
#expect(!replacer.isReplacing)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test func thePushWaitsForThePopToComplete() {
|
|
82
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true)
|
|
83
|
+
#expect(stack.calls == ["pop"])
|
|
84
|
+
stack.step()
|
|
85
|
+
#expect(stack.calls == ["pop", "push E animated"])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test func aMutationArrivingMidReplacementRunsOnceItHasSettled() {
|
|
89
|
+
var seen: [[String]] = []
|
|
90
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true)
|
|
91
|
+
stack.step()
|
|
92
|
+
replacer.whenSettled { seen.append(stack.names) }
|
|
93
|
+
#expect(seen.isEmpty)
|
|
94
|
+
stack.run()
|
|
95
|
+
|
|
96
|
+
#expect(seen == [["tabs", "A", "B", "C", "E"]])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test func deferredMutationsRunInArrivalOrderBehindASecondReplacement() {
|
|
100
|
+
var order: [String] = []
|
|
101
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true)
|
|
102
|
+
replacer.whenSettled { [replacer, stack] in
|
|
103
|
+
order.append("F")
|
|
104
|
+
replacer.push(Page("F"), replacingTopOf: stack, animated: true)
|
|
105
|
+
}
|
|
106
|
+
replacer.whenSettled { order.append("after F: \(stack.names.last ?? "")") }
|
|
107
|
+
stack.run()
|
|
108
|
+
|
|
109
|
+
#expect(stack.names == ["tabs", "A", "B", "C", "F"])
|
|
110
|
+
#expect(order == ["F", "after F: F"])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test func aMutationIssuedByTheCompletionQueuesBehindEarlierArrivals() {
|
|
114
|
+
var order: [String] = []
|
|
115
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true) { [replacer] _, _ in
|
|
116
|
+
replacer.whenSettled { order.append("from completion") }
|
|
117
|
+
}
|
|
118
|
+
replacer.whenSettled { order.append("arrived mid-replacement") }
|
|
119
|
+
stack.run()
|
|
120
|
+
|
|
121
|
+
#expect(order == ["arrived mid-replacement", "from completion"])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test func aFailedPopLeavesTheStackAsItWas() {
|
|
125
|
+
stack.failsPop = true
|
|
126
|
+
var result: Bool?
|
|
127
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true) { pushed, _ in result = pushed }
|
|
128
|
+
stack.run()
|
|
129
|
+
|
|
130
|
+
#expect(stack.names == ["tabs", "A", "B", "C", "D"])
|
|
131
|
+
#expect(stack.calls == ["pop"])
|
|
132
|
+
#expect(result == false)
|
|
133
|
+
#expect(!replacer.isReplacing)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test func aFailedPushRestoresThePoppedPage() {
|
|
137
|
+
stack.failingPushes = ["E"]
|
|
138
|
+
let top = stack.templates[4]
|
|
139
|
+
var result: Bool?
|
|
140
|
+
var ranAfter = false
|
|
141
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true) { pushed, _ in result = pushed }
|
|
142
|
+
replacer.whenSettled { ranAfter = true }
|
|
143
|
+
stack.run()
|
|
144
|
+
|
|
145
|
+
#expect(stack.names == ["tabs", "A", "B", "C", "D"])
|
|
146
|
+
#expect(stack.templates[4] === top)
|
|
147
|
+
#expect(stack.calls == ["pop", "push E animated", "push D"])
|
|
148
|
+
#expect(result == false)
|
|
149
|
+
#expect(ranAfter)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Test func aFailedRestoreStillSettles() {
|
|
153
|
+
stack.failingPushes = ["E", "D"]
|
|
154
|
+
var result: Bool?
|
|
155
|
+
replacer.push(Page("E"), replacingTopOf: stack, animated: true) { pushed, _ in result = pushed }
|
|
156
|
+
stack.run()
|
|
157
|
+
|
|
158
|
+
#expect(stack.names == ["tabs", "A", "B", "C"])
|
|
159
|
+
#expect(result == false)
|
|
160
|
+
#expect(!replacer.isReplacing)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Test func refusesAStackWithNothingAboveTheRoot() {
|
|
164
|
+
let rootOnly = FakeStack("tabs")
|
|
165
|
+
var result: Bool?
|
|
166
|
+
replacer.push(Page("E"), replacingTopOf: rootOnly, animated: true) { pushed, _ in result = pushed }
|
|
167
|
+
|
|
168
|
+
#expect(rootOnly.calls.isEmpty)
|
|
169
|
+
#expect(result == false)
|
|
170
|
+
#expect(!replacer.isReplacing)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.NavigationErrorManager = void 0;
|
|
7
|
+
var _BrowserPathHelper = require("../util/BrowserPathHelper");
|
|
7
8
|
const ERROR_TITLES = {
|
|
8
9
|
'network-error': 'Network Error',
|
|
9
10
|
'http-error': 'Server Error',
|
|
@@ -77,7 +78,8 @@ class NavigationErrorManager {
|
|
|
77
78
|
const customFormatted = this.formatCallback({
|
|
78
79
|
error: navError,
|
|
79
80
|
defaultFormatted,
|
|
80
|
-
path
|
|
81
|
+
path,
|
|
82
|
+
kind: _BrowserPathHelper.BrowserPathHelper.navigationErrorKind(path)
|
|
81
83
|
});
|
|
82
84
|
this._formattedNavigationError = customFormatted || defaultFormatted;
|
|
83
85
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["ERROR_TITLES","NavigationErrorManager","onNavigationError","onFormattedNavigationError","setFormatCallback","callback","formatCallback","clearNavigationError","_navigationError","undefined","_formattedNavigationError","error","setNavigationError","code","message","path","statusCode","navError","statusCodeSuccess","defaultFormatted","title","customFormatted","console","getNavigationError","getFormattedNavigationError","exports"],"sourceRoot":"../../../../src","sources":["web/browser/NavigationErrorManager.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"names":["_BrowserPathHelper","require","ERROR_TITLES","NavigationErrorManager","onNavigationError","onFormattedNavigationError","setFormatCallback","callback","formatCallback","clearNavigationError","_navigationError","undefined","_formattedNavigationError","error","setNavigationError","code","message","path","statusCode","navError","statusCodeSuccess","defaultFormatted","title","customFormatted","kind","BrowserPathHelper","navigationErrorKind","console","getNavigationError","getFormattedNavigationError","exports"],"sourceRoot":"../../../../src","sources":["web/browser/NavigationErrorManager.ts"],"mappings":";;;;;;AAOA,IAAAA,kBAAA,GAAAC,OAAA;AAEA,MAAMC,YAAiD,GAAG;EACxD,eAAe,EAAE,eAAe;EAChC,YAAY,EAAE,cAAc;EAC5B,mBAAmB,EAAE,mBAAmB;EACxC,gBAAgB,EAAE,OAAO;EACzB,eAAe,EAAE,OAAO;EACxB,eAAe,EAAE,cAAc;EAC/B,SAAS,EAAE;AACb,CAAC;;AAED;AACA;AACA;AACA;AACO,MAAMC,sBAAsB,CAAC;EAKlC;EACAC,iBAAiB,GAAyCA,CAAA,KAAM,CAAC,CAAC;EAClEC,0BAA0B,GAEdA,CAAA,KAAM,CAAC,CAAC;;EAEpB;AACF;AACA;EACEC,iBAAiBA,CACfC,QAA6D,EACvD;IACN,IAAI,CAACC,cAAc,GAAGD,QAAQ;EAChC;;EAEA;AACF;AACA;EACEE,oBAAoBA,CAAA,EAAS;IAC3B,IACE,IAAI,CAACC,gBAAgB,KAAKC,SAAS,IACnC,IAAI,CAACC,yBAAyB,KAAKD,SAAS,EAC5C;MACA,IAAI,CAACD,gBAAgB,GAAGC,SAAS;MACjC,IAAI,CAACC,yBAAyB,GAAGD,SAAS;MAC1C,IAAI,CAACP,iBAAiB,CAAC;QAAES,KAAK,EAAEF;MAAU,CAAC,CAAC;MAC5C,IAAI,CAACN,0BAA0B,CAACM,SAAS,CAAC;IAC5C;EACF;;EAEA;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACEG,kBAAkBA,CAChBC,IAAyB,EACzBC,OAAe,EACfC,IAAa,EACbC,UAAmB,EACb;IACN,MAAMC,QAAyB,GAAG;MAChCJ,IAAI;MACJC,OAAO;MACPE,UAAU;MACVE,iBAAiB,EAAET;IACrB,CAAC;IACD,IAAI,CAACD,gBAAgB,GAAGS,QAAQ;IAChC,IAAI,CAACf,iBAAiB,CAAC;MAAES,KAAK,EAAEM;IAAS,CAAC,CAAC;;IAE3C;IACA,MAAME,gBAA0C,GAAG;MACjDC,KAAK,EAAEpB,YAAY,CAACa,IAAI,CAAC;MACzBC;IACF,CAAC;;IAED;IACA,IAAI,IAAI,CAACR,cAAc,IAAIS,IAAI,EAAE;MAC/B,IAAI;QACF,MAAMM,eAAe,GAAG,IAAI,CAACf,cAAc,CAAC;UAC1CK,KAAK,EAAEM,QAAQ;UACfE,gBAAgB;UAChBJ,IAAI;UACJO,IAAI,EAAEC,oCAAiB,CAACC,mBAAmB,CAACT,IAAI;QAClD,CAAC,CAAC;QACF,IAAI,CAACL,yBAAyB,GAAGW,eAAe,IAAIF,gBAAgB;MACtE,CAAC,CAAC,OAAOR,KAAK,EAAE;QACdc,OAAO,CAACd,KAAK,CAAC,0CAA0C,EAAEA,KAAK,CAAC;QAChE,IAAI,CAACD,yBAAyB,GAAGS,gBAAgB;MACnD;IACF,CAAC,MAAM;MACL,IAAI,CAACT,yBAAyB,GAAGS,gBAAgB;IACnD;IAEA,IAAI,CAAChB,0BAA0B,CAAC,IAAI,CAACO,yBAAyB,CAAC;EACjE;;EAEA;AACF;AACA;EACEgB,kBAAkBA,CAAA,EAAgC;IAChD,OAAO,IAAI,CAAClB,gBAAgB;EAC9B;;EAEA;AACF;AACA;EACEmB,2BAA2BA,CAAA,EAAyC;IAClE,OAAO,IAAI,CAACjB,yBAAyB;EACvC;AACF;AAACkB,OAAA,CAAA3B,sBAAA,GAAAA,sBAAA","ignoreList":[]}
|
|
@@ -31,6 +31,12 @@ const BrowserPathHelper = exports.BrowserPathHelper = {
|
|
|
31
31
|
CONTEXTUAL_TRACK_PARAM: '__trackId',
|
|
32
32
|
/** Query parameter name for the tapped child's page position (tie-breaker) */
|
|
33
33
|
CONTEXTUAL_INDEX_PARAM: '__index',
|
|
34
|
+
/**
|
|
35
|
+
* Whether a navigation error on this path came from a browse or a search
|
|
36
|
+
*/
|
|
37
|
+
navigationErrorKind(path) {
|
|
38
|
+
return path.startsWith(`${this.SEARCH_PATH_PREFIX}?`) ? 'search' : 'browse';
|
|
39
|
+
},
|
|
34
40
|
/**
|
|
35
41
|
* Create a search path for a given query
|
|
36
42
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["BrowserPathHelper","exports","SEARCH_PATH_PREFIX","CONTEXTUAL_TRACK_PARAM","CONTEXTUAL_INDEX_PARAM","createSearchPath","query","encodedQuery","encodeURIComponent","isContextual","
|
|
1
|
+
{"version":3,"names":["BrowserPathHelper","exports","SEARCH_PATH_PREFIX","CONTEXTUAL_TRACK_PARAM","CONTEXTUAL_INDEX_PARAM","navigationErrorKind","path","startsWith","createSearchPath","query","encodedQuery","encodeURIComponent","isContextual","includes","stripTrackId","url","isFullUrl","urlObj","URL","undefined","searchParams","delete","toString","search","pathname","paramPattern","RegExp","result","replace","build","parentPath","trackId","index","separator","indexParam","extractTrackId","extractParam","extractIndex","raw","test","Number","parseInt","param","get","match","decodeURIComponent","buildUrl","baseUrl","normalizedBase","normalizedPath"],"sourceRoot":"../../../../src","sources":["web/util/BrowserPathHelper.ts"],"mappings":"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","ignoreList":[]}
|