@shortkitsdk/react-native 0.2.35 → 0.2.36
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/build.gradle.kts +8 -0
- package/android/libs/shortkit-release.aar +0 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +94 -46
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +46 -7
- package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +233 -27
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +151 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +135 -6
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedViewManager.kt +15 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +21 -11
- package/android/src/test/java/com/shortkit/reactnative/ReactCarouselOverlayHostEmitTest.kt +134 -0
- package/android/src/test/java/com/shortkit/reactnative/ReactOverlayHostDragTest.kt +45 -0
- package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostDragTest.kt +69 -0
- package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostEmitTest.kt +144 -0
- package/android/src/test/java/com/shortkit/reactnative/ShortKitFeedViewActivePropTest.kt +57 -0
- package/ios/ReactOverlayHost.swift +10 -8
- package/ios/ReactVideoCarouselOverlayHost.swift +6 -5
- package/ios/ShortKitBridge.swift +18 -0
- package/ios/ShortKitModule.mm +5 -0
- package/ios/ShortKitPlayerNativeView.swift +36 -0
- package/ios/ShortKitSDK.xcframework/Info.plist +5 -5
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +932 -84
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +932 -84
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +932 -84
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +26 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
- package/package.json +1 -1
- package/src/ShortKitCommands.ts +20 -0
- package/src/ShortKitFeed.tsx +21 -0
- package/src/specs/NativeShortKitModule.ts +10 -0
- package/src/types.ts +35 -0
|
@@ -30,6 +30,15 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
30
30
|
var feedId: String? = null
|
|
31
31
|
var startAtItemId: String? = null
|
|
32
32
|
var preloadId: String? = null
|
|
33
|
+
/**
|
|
34
|
+
* JSON-encoded `FeedInput[]` from the `feedItems` JS prop. Parsed at
|
|
35
|
+
* fragment-construction time only — post-mount changes are a no-op
|
|
36
|
+
* (matches iOS PR #150). Host apps that need post-mount item updates
|
|
37
|
+
* use the imperative `ShortKit.setFeedItems(feedId, items)` API.
|
|
38
|
+
*
|
|
39
|
+
* `preloadId` takes precedence when both are set.
|
|
40
|
+
*/
|
|
41
|
+
var feedItemsJSON: String? = null
|
|
33
42
|
// Fabric's generated delegate maps an absent boolean prop to the Java
|
|
34
43
|
// primitive false, so Boolean? is always non-null from the bridge. Track
|
|
35
44
|
// whether the prop was explicitly set so we can fall back to the provider
|
|
@@ -37,6 +46,62 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
37
46
|
var debugPanel: Boolean = false
|
|
38
47
|
var debugPanelPropSet: Boolean = false
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* URL of the thumbnail the host app is already rendering for the item that
|
|
51
|
+
* will be the first active cell. Looked up synchronously via Glide's cache
|
|
52
|
+
* (10ms cap; supports expo-image / FastImage clients) and seeded onto the
|
|
53
|
+
* fragment before its first lifecycle event. Falls back to the existing
|
|
54
|
+
* patchMissingThumbnailIfNeeded helper on cache miss or timeout.
|
|
55
|
+
*
|
|
56
|
+
* Mirrors iOS ShortKitFeedView.seedThumbnailUrl
|
|
57
|
+
* (react_native_sdk/ios/ShortKitFeedView.swift:28).
|
|
58
|
+
*/
|
|
59
|
+
var seedThumbnailUrl: String? = null
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Host-driven activation state, set by the `active` JS prop. Defaults to
|
|
63
|
+
* `true`. Mirrors iOS [`ShortKitFeedView.active`](react_native_sdk/ios/ShortKitFeedView.swift:46).
|
|
64
|
+
*/
|
|
65
|
+
var active: Boolean = true
|
|
66
|
+
internal set
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Tracks whether `active` was explicitly set by the host (vs. defaulted).
|
|
70
|
+
* Same purpose as `debugPanelPropSet` — distinguishes "absent" from "false"
|
|
71
|
+
* since Fabric maps absent boolean props to the Java primitive `false`.
|
|
72
|
+
*
|
|
73
|
+
* Used only by the first-mount race fix in [embedFeedFragmentIfNeeded] to
|
|
74
|
+
* decide whether a `false` value is a real host intent or a Fabric default.
|
|
75
|
+
*/
|
|
76
|
+
var activePropSet: Boolean = false
|
|
77
|
+
internal set
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set the `active` prop from the bridge. The canonical entry point for
|
|
81
|
+
* all `active` state transitions — do not mutate the field directly.
|
|
82
|
+
*
|
|
83
|
+
* Behavior:
|
|
84
|
+
* - Always sets `activePropSet = true`.
|
|
85
|
+
* - On transition while a fragment is embedded:
|
|
86
|
+
* - `true → false`: calls `fragment.deactivate()`.
|
|
87
|
+
* - `false → true`: calls `fragment.activate()`.
|
|
88
|
+
* - Returns early if no fragment is yet embedded — the first-mount race
|
|
89
|
+
* in [embedFeedFragmentIfNeeded] handles the deferred deactivate case.
|
|
90
|
+
*
|
|
91
|
+
* Invoked by [ShortKitFeedViewManager.setActive].
|
|
92
|
+
*/
|
|
93
|
+
fun setActiveFromBridge(value: Boolean) {
|
|
94
|
+
val prev = active
|
|
95
|
+
active = value
|
|
96
|
+
activePropSet = true
|
|
97
|
+
val fragment = feedFragment ?: return
|
|
98
|
+
if (prev && !value) {
|
|
99
|
+
fragment.deactivate()
|
|
100
|
+
} else if (!prev && value) {
|
|
101
|
+
fragment.activate()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
40
105
|
// -----------------------------------------------------------------------
|
|
41
106
|
// Fragment management
|
|
42
107
|
// -----------------------------------------------------------------------
|
|
@@ -140,7 +205,15 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
140
205
|
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
141
206
|
ShortKitBridge.staticPendingFeedViews.remove(this)
|
|
142
207
|
}
|
|
143
|
-
|
|
208
|
+
// Mirror iOS willMove(toWindow:) at react_native_sdk/ios/ShortKitFeedView.swift:91-99 —
|
|
209
|
+
// suspend only if leaving the window AND the prop says we should be active.
|
|
210
|
+
// When active=false the host has already driven the fragment into deactivate
|
|
211
|
+
// (or will once setActiveFromBridge runs), and we mustn't double-suspend.
|
|
212
|
+
// Default active=true preserves legacy window-inference behavior for hosts
|
|
213
|
+
// that never set the active prop.
|
|
214
|
+
if (active) {
|
|
215
|
+
suspendFeedFragment()
|
|
216
|
+
}
|
|
144
217
|
super.onDetachedFromWindow()
|
|
145
218
|
}
|
|
146
219
|
|
|
@@ -167,7 +240,11 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
167
240
|
.beginTransaction()
|
|
168
241
|
.show(existingFragment)
|
|
169
242
|
.commitNowAllowingStateLoss()
|
|
170
|
-
|
|
243
|
+
// Mirror iOS: only auto-activate if the host's active prop says so.
|
|
244
|
+
// Default active=true preserves legacy "always activate on un-hide" behavior.
|
|
245
|
+
if (active) {
|
|
246
|
+
existingFragment.activate()
|
|
247
|
+
}
|
|
171
248
|
} catch (e: Exception) {
|
|
172
249
|
Log.e(TAG, "Failed to show feed fragment", e)
|
|
173
250
|
}
|
|
@@ -188,24 +265,58 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
188
265
|
|
|
189
266
|
var feedConfig = ShortKitBridge.parseFeedConfig(config ?: "{}", context)
|
|
190
267
|
|
|
191
|
-
|
|
192
|
-
|
|
268
|
+
val explicitPreloadId = preloadId
|
|
269
|
+
val explicitFeedItemsJSON = feedItemsJSON
|
|
270
|
+
if (explicitPreloadId != null) {
|
|
271
|
+
val preload = ShortKitBridge.shared?.consumePreload(explicitPreloadId)
|
|
193
272
|
if (preload != null) {
|
|
194
273
|
feedConfig = feedConfig.copy(preload = preload)
|
|
195
274
|
}
|
|
275
|
+
} else if (explicitFeedItemsJSON != null) {
|
|
276
|
+
// Mount-time custom-feed fast-path (SHO-29 / iOS PR #150).
|
|
277
|
+
// Parse JSON → FeedInput[] → sdk.preloadFeed(items=) which
|
|
278
|
+
// internally calls FeedPreload.forCustomItems with the SDK's
|
|
279
|
+
// own scope. Assign to feedConfig.preload BEFORE constructing
|
|
280
|
+
// the fragment, so FeedPreload.consume() returns synchronously
|
|
281
|
+
// on the first cell bind. Requires Task 1's sync-payload fix
|
|
282
|
+
// in FeedPreload.kt.
|
|
283
|
+
//
|
|
284
|
+
// Using sdk.preloadFeed(items=) (the public SDK entry point at
|
|
285
|
+
// ShortKit.kt:232) instead of FeedPreload.forCustomItems
|
|
286
|
+
// directly — avoids needing to promote internal SDK helpers
|
|
287
|
+
// for cross-module access from the bridge.
|
|
288
|
+
val parsed = ShortKitBridge.parseFeedInputs(explicitFeedItemsJSON)
|
|
289
|
+
if (parsed != null && parsed.isNotEmpty()) {
|
|
290
|
+
val preload = sdk.preloadFeed(items = parsed)
|
|
291
|
+
feedConfig = feedConfig.copy(preload = preload)
|
|
292
|
+
} else {
|
|
293
|
+
Log.w(TAG, "feedItemsJSON parsed to null/empty — ignoring (config feedSource will drive the fetch path)")
|
|
294
|
+
}
|
|
196
295
|
}
|
|
197
296
|
|
|
198
297
|
val fragment = ShortKitFeedFragment.newInstance(sdk, feedConfig, startAtItemId)
|
|
199
298
|
|
|
200
299
|
val debugPanelEnabled = if (debugPanelPropSet) debugPanel else sdk.debugPanelEnabled
|
|
201
300
|
if (debugPanelEnabled) {
|
|
202
|
-
fragment.debugPanelFactory = {
|
|
301
|
+
fragment.debugPanelFactory = { activeMetrics, prev, next, lifecycleOwner ->
|
|
203
302
|
com.shortkit.sdk.debug.DebugPanelView(context).also { panel ->
|
|
204
|
-
panel.subscribe(
|
|
303
|
+
panel.subscribe(activeMetrics, prev, next, lifecycleOwner)
|
|
205
304
|
}
|
|
206
305
|
}
|
|
207
306
|
}
|
|
208
307
|
|
|
308
|
+
// SHO-11: seed a thumbnail from the host app's image cache so the first
|
|
309
|
+
// cell renders with a visible thumbnail from frame zero. Synchronous
|
|
310
|
+
// Glide lookup (memory-only via 10ms cap) — no network. If null (cache
|
|
311
|
+
// miss, timeout, or Glide not linked), the existing
|
|
312
|
+
// patchMissingThumbnailIfNeeded helper handles the fallback at feed-open
|
|
313
|
+
// time. Mirrors iOS ShortKitFeedView.swift:170-173.
|
|
314
|
+
seedThumbnailUrl?.let { url ->
|
|
315
|
+
com.shortkit.sdk.feed.SeedThumbnailResolver.resolveFromCache(context, url)?.let { bitmap ->
|
|
316
|
+
fragment.seedThumbnail = bitmap
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
209
320
|
try {
|
|
210
321
|
activity.supportFragmentManager
|
|
211
322
|
.beginTransaction()
|
|
@@ -213,6 +324,24 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
213
324
|
.commitNowAllowingStateLoss()
|
|
214
325
|
this.feedFragment = fragment
|
|
215
326
|
|
|
327
|
+
// Android-specific first-mount race: Fabric may have set active=false
|
|
328
|
+
// BEFORE we got to embed the fragment. The fragment self-claims the
|
|
329
|
+
// player pool in onResume (ShortKitFeedFragment.kt:2112), so by the
|
|
330
|
+
// time commitNowAllowingStateLoss returns the fragment is already
|
|
331
|
+
// active. Retroactively deactivate to honor the host's intent.
|
|
332
|
+
//
|
|
333
|
+
// Note: this produces a brief claim-then-yield window (microseconds).
|
|
334
|
+
// Acceptable for normal use; if a host needs strictly never-active
|
|
335
|
+
// mounts, the fragment's onResume self-claim would need a managed-mode
|
|
336
|
+
// flag to suppress (separate ticket).
|
|
337
|
+
//
|
|
338
|
+
// activePropSet guard: only fires when the host explicitly set the prop.
|
|
339
|
+
// Fabric maps absent booleans to Java false, so without this guard
|
|
340
|
+
// every fragment would deactivate at mount.
|
|
341
|
+
if (activePropSet && !active) {
|
|
342
|
+
fragment.deactivate()
|
|
343
|
+
}
|
|
344
|
+
|
|
216
345
|
// View.generateViewId() can return IDs that collide with Fabric-
|
|
217
346
|
// managed views (e.g. ReactSurfaceView also gets id=1). When that
|
|
218
347
|
// happens, FragmentManager.replace() places the fragment view in
|
|
@@ -35,12 +35,27 @@ class ShortKitFeedViewManager : SimpleViewManager<ShortKitFeedView>() {
|
|
|
35
35
|
view.preloadId = preloadId
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
@ReactProp(name = "feedItemsJSON")
|
|
39
|
+
fun setFeedItemsJSON(view: ShortKitFeedView, feedItemsJSON: String?) {
|
|
40
|
+
view.feedItemsJSON = feedItemsJSON
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
@ReactProp(name = "debugPanel")
|
|
39
44
|
fun setDebugPanel(view: ShortKitFeedView, debugPanel: Boolean) {
|
|
40
45
|
view.debugPanel = debugPanel
|
|
41
46
|
view.debugPanelPropSet = true
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
@ReactProp(name = "active")
|
|
50
|
+
fun setActive(view: ShortKitFeedView, active: Boolean) {
|
|
51
|
+
view.setActiveFromBridge(active)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ReactProp(name = "seedThumbnailUrl")
|
|
55
|
+
fun setSeedThumbnailUrl(view: ShortKitFeedView, url: String?) {
|
|
56
|
+
view.seedThumbnailUrl = url
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
override fun onDropViewInstance(view: ShortKitFeedView) {
|
|
45
60
|
view.destroy()
|
|
46
61
|
super.onDropViewInstance(view)
|
|
@@ -136,6 +136,11 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
136
136
|
@ReactMethod
|
|
137
137
|
override fun pause() { bridge?.pause() }
|
|
138
138
|
|
|
139
|
+
@ReactMethod
|
|
140
|
+
override fun setFeedScrollEnabled(enabled: Boolean) {
|
|
141
|
+
bridge?.setFeedScrollEnabled(enabled)
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
@ReactMethod
|
|
140
145
|
override fun seek(seconds: Double) { bridge?.seek(seconds) }
|
|
141
146
|
|
|
@@ -149,28 +154,30 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
149
154
|
override fun skipToPrevious() { bridge?.skipToPrevious() }
|
|
150
155
|
|
|
151
156
|
// -----------------------------------------------------------------
|
|
152
|
-
// Carousel commands —
|
|
153
|
-
//
|
|
157
|
+
// Carousel commands — bridge to ShortKit.activeInstance().carousel.
|
|
158
|
+
// Mirrors iOS RN bridge from PR #136. SHO-15.
|
|
159
|
+
// Boundary semantics: returns false / -1 / 0 when no ShortKit instance
|
|
160
|
+
// is active or no fragment is bound, matching iOS `?? false` / `?? nil`
|
|
161
|
+
// fallbacks via the namespace's onMain shim + null-tolerance.
|
|
154
162
|
// -----------------------------------------------------------------
|
|
155
163
|
|
|
156
164
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
157
|
-
override fun carouselNext(): Boolean = false
|
|
165
|
+
override fun carouselNext(): Boolean = bridge?.carouselNext() ?: false
|
|
158
166
|
|
|
159
167
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
160
|
-
override fun carouselPrevious(): Boolean = false
|
|
168
|
+
override fun carouselPrevious(): Boolean = bridge?.carouselPrevious() ?: false
|
|
161
169
|
|
|
162
170
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
163
|
-
override fun carouselSetActiveIndex(index: Double): Boolean =
|
|
164
|
-
|
|
165
|
-
// Carousel accessors — stubs for PR 1.
|
|
166
|
-
// onCarouselActiveVideoCompleted emitter intentionally unwired — never fires on Android in PR 1.
|
|
167
|
-
// PR 2 will subscribe to ShortKit.activeInstance.get().carousel.activeVideoCompleted.
|
|
171
|
+
override fun carouselSetActiveIndex(index: Double): Boolean =
|
|
172
|
+
bridge?.carouselSetActiveIndex(index.toInt()) ?: false
|
|
168
173
|
|
|
169
174
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
170
|
-
override fun getCarouselActiveIndex(): Double =
|
|
175
|
+
override fun getCarouselActiveIndex(): Double =
|
|
176
|
+
(bridge?.carouselActiveIndex() ?: -1).toDouble()
|
|
171
177
|
|
|
172
178
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
173
|
-
override fun getCarouselVideoCount(): Double =
|
|
179
|
+
override fun getCarouselVideoCount(): Double =
|
|
180
|
+
(bridge?.carouselVideoCount() ?: 0).toDouble()
|
|
174
181
|
|
|
175
182
|
@ReactMethod
|
|
176
183
|
override fun setMuted(muted: Boolean) { bridge?.setMuted(muted) }
|
|
@@ -326,6 +333,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
326
333
|
"onContentTapped" -> emitOnContentTapped(params)
|
|
327
334
|
"onDismiss" -> emitOnDismiss(params)
|
|
328
335
|
"onRefreshStateChanged" -> emitOnRefreshStateChanged(params)
|
|
336
|
+
"onRefreshStateChangedPerFeed" -> emitOnRefreshStateChangedPerFeed(params)
|
|
329
337
|
"onDidFetchContentItems" -> emitOnDidFetchContentItems(params)
|
|
330
338
|
"onFeedReady" -> emitOnFeedReady(params)
|
|
331
339
|
"onOverlayActiveChanged" -> emitOnOverlayActiveChanged(params)
|
|
@@ -341,8 +349,10 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
341
349
|
"onCarouselActiveImageChanged" -> emitOnCarouselActiveImageChanged(params)
|
|
342
350
|
"onCarouselItemChanged" -> emitOnCarouselItemChanged(params)
|
|
343
351
|
"onVideoCarouselActiveVideoChanged" -> emitOnVideoCarouselActiveVideoChanged(params)
|
|
352
|
+
"onVideoCarouselCellTap" -> emitOnVideoCarouselCellTap(params)
|
|
344
353
|
"onVideoCarouselItemChanged" -> emitOnVideoCarouselItemChanged(params)
|
|
345
354
|
"onCarouselActiveVideoCompleted" -> emitOnCarouselActiveVideoCompleted(params)
|
|
355
|
+
"onVideoCarouselCellTap" -> emitOnVideoCarouselCellTap(params)
|
|
346
356
|
else -> {
|
|
347
357
|
android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
|
|
348
358
|
reactApplicationContext
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import androidx.test.core.app.ApplicationProvider
|
|
7
|
+
import com.facebook.react.bridge.WritableMap
|
|
8
|
+
import com.facebook.react.interfaces.TaskInterface
|
|
9
|
+
import com.facebook.react.interfaces.fabric.ReactSurface
|
|
10
|
+
import com.shortkit.sdk.model.CarouselImage
|
|
11
|
+
import com.shortkit.sdk.model.ImageCarouselItem
|
|
12
|
+
import org.junit.Assert.assertEquals
|
|
13
|
+
import org.junit.Assert.assertFalse
|
|
14
|
+
import org.junit.Assert.assertTrue
|
|
15
|
+
import org.junit.Test
|
|
16
|
+
import org.junit.runner.RunWith
|
|
17
|
+
import org.robolectric.RobolectricTestRunner
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tests SHO-8 emit-on-change gating in ReactCarouselOverlayHost.
|
|
22
|
+
*
|
|
23
|
+
* Behavior under test (mirrors iOS ReactCarouselOverlayHost.swift:132-145):
|
|
24
|
+
* - First configure() with item A → applySurfaceProps path; no emit.
|
|
25
|
+
* - Second configure() with same item A → no emit, no setProperties.
|
|
26
|
+
* - Second configure() with different item B → emit onCarouselItemChanged
|
|
27
|
+
* with payload { surfaceId, item, isActive: false, activeImageIndex: 0 }.
|
|
28
|
+
*/
|
|
29
|
+
@RunWith(RobolectricTestRunner::class)
|
|
30
|
+
class ReactCarouselOverlayHostEmitTest {
|
|
31
|
+
|
|
32
|
+
/** No-op TaskInterface returned by stub surface lifecycle methods. */
|
|
33
|
+
private object NoOpTask : TaskInterface<Void> {
|
|
34
|
+
override fun waitForCompletion() = Unit
|
|
35
|
+
override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = true
|
|
36
|
+
override fun getResult(): Void? = null
|
|
37
|
+
override fun getError(): Exception? = null
|
|
38
|
+
override fun isCompleted(): Boolean = true
|
|
39
|
+
override fun isCancelled(): Boolean = false
|
|
40
|
+
override fun isFaulted(): Boolean = false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Minimal ReactSurface stub for Robolectric tests. ApplicationProvider does
|
|
45
|
+
* not supply a ReactApplication, so createSurfaceIfNeeded() would normally
|
|
46
|
+
* fail silently with reactHost == null. The createSurface seam lets tests
|
|
47
|
+
* inject this stub so surface != null after pushProps(), allowing
|
|
48
|
+
* hasPushedInitialProps to flip correctly (the bug-fix gate under test).
|
|
49
|
+
*
|
|
50
|
+
* isRunning is false so applySurfaceProps() calls start() (no-op here) and
|
|
51
|
+
* the updateInitProps cast to ReactSurfaceImpl is a safe no-op via `as?`.
|
|
52
|
+
*/
|
|
53
|
+
private class StubReactSurface(private val ctx: Context) : ReactSurface {
|
|
54
|
+
override val surfaceID: Int = 0
|
|
55
|
+
override val moduleName: String = "StubSurface"
|
|
56
|
+
override val isRunning: Boolean = false
|
|
57
|
+
override val view: ViewGroup? = null
|
|
58
|
+
override val context: Context get() = ctx
|
|
59
|
+
override fun prerender(): TaskInterface<Void> = NoOpTask
|
|
60
|
+
override fun start(): TaskInterface<Void> = NoOpTask
|
|
61
|
+
override fun stop(): TaskInterface<Void> = NoOpTask
|
|
62
|
+
override fun clear() = Unit
|
|
63
|
+
override fun detach() = Unit
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun makeHost(): ReactCarouselOverlayHost {
|
|
67
|
+
val ctx = ApplicationProvider.getApplicationContext<Context>()
|
|
68
|
+
return ReactCarouselOverlayHost(ctx).also {
|
|
69
|
+
// JavaOnlyMap is a pure-Java WritableMap that avoids the JNI init
|
|
70
|
+
// requirement of WritableNativeMap in Robolectric unit tests.
|
|
71
|
+
it.createMap = { com.facebook.react.bridge.JavaOnlyMap() }
|
|
72
|
+
// Inject a stub surface so createSurfaceIfNeeded() succeeds and
|
|
73
|
+
// hasPushedInitialProps flips after the first configure().
|
|
74
|
+
it.createSurface = { _, _ -> StubReactSurface(ctx) }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun item(id: String) = ImageCarouselItem(
|
|
79
|
+
id = id,
|
|
80
|
+
images = listOf(CarouselImage(url = "https://example.com/a.jpg", alt = null)),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
fun firstConfigureSetsHasPushedAndCurrentItemNoEmit() {
|
|
85
|
+
val host = makeHost()
|
|
86
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
87
|
+
host.emitEvent = { name, params -> emitted.add(name to params) }
|
|
88
|
+
|
|
89
|
+
host.configure(item("A"))
|
|
90
|
+
|
|
91
|
+
assertEquals("A", host.currentItemId)
|
|
92
|
+
assertTrue(host.hasPushedInitialProps)
|
|
93
|
+
assertTrue("first mount must not emit", emitted.isEmpty())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun sameItemConfigureIsNoOpAfterFirstMount() {
|
|
98
|
+
val host = makeHost()
|
|
99
|
+
host.configure(item("A"))
|
|
100
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
101
|
+
host.emitEvent = { name, params -> emitted.add(name to params) }
|
|
102
|
+
|
|
103
|
+
host.configure(item("A"))
|
|
104
|
+
|
|
105
|
+
assertEquals("A", host.currentItemId)
|
|
106
|
+
assertTrue("same item must not emit", emitted.isEmpty())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun differentItemEmitsCarouselItemChanged() {
|
|
111
|
+
val host = makeHost()
|
|
112
|
+
host.configure(item("A"))
|
|
113
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
114
|
+
host.emitEvent = { name, params -> emitted.add(name to params) }
|
|
115
|
+
|
|
116
|
+
host.configure(item("B"))
|
|
117
|
+
|
|
118
|
+
assertEquals("B", host.currentItemId)
|
|
119
|
+
assertEquals(1, emitted.size)
|
|
120
|
+
assertEquals("onCarouselItemChanged", emitted[0].first)
|
|
121
|
+
val params = emitted[0].second
|
|
122
|
+
assertEquals(host.surfaceId, params.getString("surfaceId"))
|
|
123
|
+
assertFalse(params.getBoolean("isActive"))
|
|
124
|
+
assertEquals(0, params.getInt("activeImageIndex"))
|
|
125
|
+
// `item` is a JSON string of the ImageCarouselItem; assert it contains
|
|
126
|
+
// the new id. Payload-shape correctness is covered by the cross-platform
|
|
127
|
+
// JS subscriber's destructuring, not asserted here.
|
|
128
|
+
val itemJson = params.getString("item")
|
|
129
|
+
assertTrue(
|
|
130
|
+
"item JSON should contain id=B but was: $itemJson",
|
|
131
|
+
itemJson?.contains("\"id\":\"B\"") == true,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import androidx.test.core.app.ApplicationProvider
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import org.junit.runner.RunWith
|
|
7
|
+
import org.robolectric.RobolectricTestRunner
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tests SHO-9 drag-phase field on ReactOverlayHost.
|
|
11
|
+
* The full state-machine wiring (flow → field, emissions gated, full
|
|
12
|
+
* state on settle) is verified via the manual / instrumented test in
|
|
13
|
+
* the spec test plan. This test verifies the field's existence and
|
|
14
|
+
* resettability via configure().
|
|
15
|
+
*/
|
|
16
|
+
@RunWith(RobolectricTestRunner::class)
|
|
17
|
+
class ReactOverlayHostDragTest {
|
|
18
|
+
|
|
19
|
+
private fun makeHost() = ReactOverlayHost(
|
|
20
|
+
ApplicationProvider.getApplicationContext()
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
fun isDraggingDefaultsFalse() {
|
|
25
|
+
val host = makeHost()
|
|
26
|
+
assertFalse(host.isDragging)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun configureResetsIsDragging() {
|
|
31
|
+
val host = makeHost()
|
|
32
|
+
host.isDraggingForTest = true
|
|
33
|
+
host.configure(makeContentItem("A"))
|
|
34
|
+
assertFalse(host.isDragging)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private fun makeContentItem(id: String) = com.shortkit.sdk.model.ContentItem(
|
|
38
|
+
id = id,
|
|
39
|
+
title = "t",
|
|
40
|
+
duration = 5.0,
|
|
41
|
+
streamingUrl = "https://example.com/$id.m3u8",
|
|
42
|
+
thumbnailUrl = "https://example.com/$id.jpg",
|
|
43
|
+
captionTracks = emptyList(),
|
|
44
|
+
)
|
|
45
|
+
}
|
package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostDragTest.kt
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import androidx.test.core.app.ApplicationProvider
|
|
4
|
+
import com.facebook.react.bridge.JavaOnlyMap
|
|
5
|
+
import com.shortkit.sdk.model.VideoCarouselItem
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.runner.RunWith
|
|
9
|
+
import org.robolectric.RobolectricTestRunner
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tests SHO-9 drag-phase field on ReactVideoCarouselOverlayHost.
|
|
13
|
+
* The full state-machine wiring requires a ShortKitPlayer instance with
|
|
14
|
+
* controllable flows; verified via manual / instrumented testing per the
|
|
15
|
+
* spec test plan. This test verifies the field's existence and reset
|
|
16
|
+
* behavior on configure().
|
|
17
|
+
*
|
|
18
|
+
* configure() always calls emitItemChanged(), so the createMap and
|
|
19
|
+
* createSurface seams must be injected here too (same as the emit test).
|
|
20
|
+
*/
|
|
21
|
+
@RunWith(RobolectricTestRunner::class)
|
|
22
|
+
class ReactVideoCarouselOverlayHostDragTest {
|
|
23
|
+
|
|
24
|
+
private fun makeHost(): ReactVideoCarouselOverlayHost {
|
|
25
|
+
val host = ReactVideoCarouselOverlayHost(ApplicationProvider.getApplicationContext())
|
|
26
|
+
host.createMap = { JavaOnlyMap() }
|
|
27
|
+
// Stub surface so hasPushedInitialProps can flip and configure()
|
|
28
|
+
// doesn't retry on every call due to null surface.
|
|
29
|
+
host.createSurface = { _, _ ->
|
|
30
|
+
// Re-use the same stub shape from the emit test — no-arg style.
|
|
31
|
+
object : com.facebook.react.interfaces.fabric.ReactSurface {
|
|
32
|
+
override val surfaceID: Int = 0
|
|
33
|
+
override val moduleName: String = "StubSurface"
|
|
34
|
+
override val isRunning: Boolean = false
|
|
35
|
+
override val view: android.view.ViewGroup? = null
|
|
36
|
+
override val context: android.content.Context
|
|
37
|
+
get() = ApplicationProvider.getApplicationContext()
|
|
38
|
+
override fun prerender() = object : com.facebook.react.interfaces.TaskInterface<Void> {
|
|
39
|
+
override fun waitForCompletion() = Unit
|
|
40
|
+
override fun waitForCompletion(d: Long, u: java.util.concurrent.TimeUnit) = true
|
|
41
|
+
override fun getResult(): Void? = null
|
|
42
|
+
override fun getError(): Exception? = null
|
|
43
|
+
override fun isCompleted() = true
|
|
44
|
+
override fun isCancelled() = false
|
|
45
|
+
override fun isFaulted() = false
|
|
46
|
+
}
|
|
47
|
+
override fun start() = prerender()
|
|
48
|
+
override fun stop() = prerender()
|
|
49
|
+
override fun clear() = Unit
|
|
50
|
+
override fun detach() = Unit
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return host
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test
|
|
57
|
+
fun isDraggingDefaultsFalse() {
|
|
58
|
+
val host = makeHost()
|
|
59
|
+
assertFalse(host.isDragging)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun configureResetsIsDragging() {
|
|
64
|
+
val host = makeHost()
|
|
65
|
+
host.isDraggingForTest = true
|
|
66
|
+
host.configure(VideoCarouselItem(id = "A", videos = emptyList()))
|
|
67
|
+
assertFalse(host.isDragging)
|
|
68
|
+
}
|
|
69
|
+
}
|