@shortkitsdk/react-native 0.2.37 → 0.2.39
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 +3 -3
- package/android/libs/shortkit-release.aar +0 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactCarouselOverlayHost.kt +27 -7
- package/android/src/main/java/com/shortkit/reactnative/ReactOverlayHost.kt +1 -0
- package/android/src/main/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHost.kt +32 -14
- package/android/src/main/java/com/shortkit/reactnative/ShortKitBridge.kt +39 -1
- package/android/src/main/java/com/shortkit/reactnative/ShortKitFeedView.kt +1 -0
- package/android/src/main/java/com/shortkit/reactnative/ShortKitModule.kt +1 -1
- package/android/src/test/java/com/shortkit/reactnative/ReactCarouselOverlayHostLifecycleTest.kt +155 -0
- package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostLifecycleTest.kt +200 -0
- package/android/src/test/java/com/shortkit/reactnative/ShortKitBridgeLongPressTest.kt +148 -0
- package/ios/ShortKitBridge.swift +2 -1
- 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 +3 -3
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +3 -3
- 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 +3 -3
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +3 -3
- 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 +5 -5
- package/package.json +1 -1
package/android/build.gradle.kts
CHANGED
|
@@ -35,9 +35,9 @@ android {
|
|
|
35
35
|
|
|
36
36
|
dependencies {
|
|
37
37
|
implementation("com.facebook.react:react-android")
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// implementation("dev.shortkit:shortkit:0.2.
|
|
38
|
+
// Composite build in sample/android/settings.gradle substitutes this with
|
|
39
|
+
// live android_sdk source automatically (dev.shortkit:shortkit → :shortkit project).
|
|
40
|
+
// implementation("dev.shortkit:shortkit:0.2.37")
|
|
41
41
|
implementation(files("libs/shortkit-release.aar"))
|
|
42
42
|
// Transitive deps needed when using local AAR (Maven artifact bundles these automatically)
|
|
43
43
|
implementation("androidx.media3:media3-exoplayer:1.5.1")
|
|
Binary file
|
|
@@ -15,6 +15,8 @@ import kotlinx.coroutines.CoroutineScope
|
|
|
15
15
|
import kotlinx.coroutines.Dispatchers
|
|
16
16
|
import kotlinx.coroutines.Job
|
|
17
17
|
import kotlinx.coroutines.SupervisorJob
|
|
18
|
+
import kotlinx.coroutines.cancel
|
|
19
|
+
import kotlinx.coroutines.isActive
|
|
18
20
|
import kotlinx.coroutines.launch
|
|
19
21
|
import kotlinx.coroutines.withContext
|
|
20
22
|
import kotlinx.serialization.encodeToString
|
|
@@ -52,10 +54,20 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
52
54
|
private var surface: ReactSurface? = null
|
|
53
55
|
private var pendingProps: Bundle? = null
|
|
54
56
|
private var isInLayoutPass: Boolean = false
|
|
55
|
-
private
|
|
57
|
+
private var ioScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
56
58
|
private var pendingWriteJob: Job? = null
|
|
57
59
|
private var configureGeneration: Int = 0
|
|
58
60
|
|
|
61
|
+
/** Test-only accessor for ioScope. */
|
|
62
|
+
@androidx.annotation.VisibleForTesting
|
|
63
|
+
internal val ioScopeForTest: CoroutineScope get() = ioScope
|
|
64
|
+
|
|
65
|
+
/** Test-only triggers for window lifecycle (protected in View). */
|
|
66
|
+
@androidx.annotation.VisibleForTesting
|
|
67
|
+
internal fun callOnDetachedFromWindow() = onDetachedFromWindow()
|
|
68
|
+
@androidx.annotation.VisibleForTesting
|
|
69
|
+
internal fun callOnAttachedToWindow() = onAttachedToWindow()
|
|
70
|
+
|
|
59
71
|
/** Unique identifier for this overlay instance, used for event routing. */
|
|
60
72
|
val surfaceId: String = java.util.UUID.randomUUID().toString()
|
|
61
73
|
|
|
@@ -343,8 +355,9 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
343
355
|
}
|
|
344
356
|
|
|
345
357
|
override fun resetState() {
|
|
346
|
-
|
|
347
|
-
|
|
358
|
+
// Non-destructive: configure() will push new props on next bind.
|
|
359
|
+
// iOS parity: ReactCarouselOverlayHost.swift resetState() is a no-op.
|
|
360
|
+
// Mirrors ReactVideoCarouselOverlayHost.resetState() which is also a no-op.
|
|
348
361
|
}
|
|
349
362
|
|
|
350
363
|
override fun fadeOutForTransition() {
|
|
@@ -404,6 +417,9 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
404
417
|
|
|
405
418
|
override fun onAttachedToWindow() {
|
|
406
419
|
super.onAttachedToWindow()
|
|
420
|
+
if (!ioScope.isActive) {
|
|
421
|
+
ioScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
422
|
+
}
|
|
407
423
|
|
|
408
424
|
val parentView = parent as? android.view.View
|
|
409
425
|
if (parentView != null && parentView.width > 0 && parentView.height > 0 && (width == 0 || height == 0)) {
|
|
@@ -464,12 +480,16 @@ class ReactCarouselOverlayHost(context: Context) : FrameLayout(context), Carouse
|
|
|
464
480
|
// Cleanup
|
|
465
481
|
// ------------------------------------------------------------------
|
|
466
482
|
|
|
467
|
-
override fun onDetachedFromWindow() {
|
|
483
|
+
public override fun onDetachedFromWindow() {
|
|
468
484
|
super.onDetachedFromWindow()
|
|
485
|
+
layoutHandler.removeCallbacks(layoutRunnable)
|
|
469
486
|
pendingWriteJob?.cancel()
|
|
470
487
|
pendingWriteJob = null
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
ioScope.cancel()
|
|
489
|
+
// Non-destructive: surface survives cell recycle and view transitions.
|
|
490
|
+
// iOS parity: ReactCarouselOverlayHost.swift has no willMove/didMoveToWindow
|
|
491
|
+
// override — surface stops only in deinit. Mirrors the non-destructive
|
|
492
|
+
// contract enforced on ReactVideoCarouselOverlayHost.
|
|
493
|
+
// ioScope is recreated in onAttachedToWindow if needed.
|
|
474
494
|
}
|
|
475
495
|
}
|
|
@@ -300,6 +300,7 @@ class ReactOverlayHost(context: Context) : FrameLayout(context), FeedOverlay {
|
|
|
300
300
|
|
|
301
301
|
override fun onDetachedFromWindow() {
|
|
302
302
|
super.onDetachedFromWindow()
|
|
303
|
+
layoutHandler.removeCallbacks(layoutRunnable)
|
|
303
304
|
// Cancel flow subscriptions to avoid emitting events for off-screen cells.
|
|
304
305
|
// Do NOT stop the surface — keep JS alive so subscriptions persist across
|
|
305
306
|
// RecyclerView detach/reattach cycles. This matches iOS behavior where the
|
|
@@ -133,7 +133,8 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
|
|
|
133
133
|
|
|
134
134
|
// Player state
|
|
135
135
|
private var player: ShortKitPlayer? = null
|
|
136
|
-
|
|
136
|
+
@androidx.annotation.VisibleForTesting
|
|
137
|
+
internal var isActive: Boolean = false
|
|
137
138
|
private var cachedPlayerState: String = "idle"
|
|
138
139
|
private var cachedIsMuted: Boolean = true
|
|
139
140
|
private var cachedCurrentTime: Double = 0.0
|
|
@@ -155,6 +156,17 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
|
|
|
155
156
|
|
|
156
157
|
private var flowScope: CoroutineScope? = null
|
|
157
158
|
|
|
159
|
+
/** Test-only read-write accessor for flowScope. Write path lets tests inject a scope
|
|
160
|
+
* without needing a full ShortKitPlayer mock. */
|
|
161
|
+
@androidx.annotation.VisibleForTesting
|
|
162
|
+
internal var flowScopeForTest: CoroutineScope?
|
|
163
|
+
get() = flowScope
|
|
164
|
+
set(value) { flowScope = value }
|
|
165
|
+
|
|
166
|
+
/** Test-only trigger for onDetachedFromWindow (protected in View). */
|
|
167
|
+
@androidx.annotation.VisibleForTesting
|
|
168
|
+
internal fun callOnDetachedFromWindow() = onDetachedFromWindow()
|
|
169
|
+
|
|
158
170
|
// ------------------------------------------------------------------
|
|
159
171
|
// Fabric layout workaround
|
|
160
172
|
// ------------------------------------------------------------------
|
|
@@ -276,9 +288,10 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
|
|
|
276
288
|
putBoolean("isActive", false)
|
|
277
289
|
putString("playerState", "idle")
|
|
278
290
|
putBoolean("isMuted", cachedIsMuted)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
val initialIdx = item.clampedInitialPageIndex
|
|
292
|
+
if (item.videos.indices.contains(initialIdx)) {
|
|
293
|
+
putString("activeVideo", ShortKitBridge.serializeContentItemToJSON(item.videos[initialIdx]))
|
|
294
|
+
putInt("activeVideoIndex", initialIdx)
|
|
282
295
|
}
|
|
283
296
|
}
|
|
284
297
|
|
|
@@ -298,11 +311,12 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
|
|
|
298
311
|
putBoolean("isActive", false)
|
|
299
312
|
putString("playerState", "idle")
|
|
300
313
|
putBoolean("isMuted", cachedIsMuted)
|
|
301
|
-
|
|
302
|
-
|
|
314
|
+
val initialIdx = item.clampedInitialPageIndex
|
|
315
|
+
putInt("activeVideoIndex", initialIdx)
|
|
316
|
+
if (item.videos.indices.contains(initialIdx)) {
|
|
303
317
|
putString(
|
|
304
318
|
"activeVideo",
|
|
305
|
-
ShortKitBridge.serializeContentItemToJSON(item.videos
|
|
319
|
+
ShortKitBridge.serializeContentItemToJSON(item.videos[initialIdx]),
|
|
306
320
|
)
|
|
307
321
|
} else {
|
|
308
322
|
putNull("activeVideo")
|
|
@@ -630,15 +644,19 @@ class ReactVideoCarouselOverlayHost(context: Context) : FrameLayout(context), Vi
|
|
|
630
644
|
// Cleanup
|
|
631
645
|
// ------------------------------------------------------------------
|
|
632
646
|
|
|
633
|
-
override fun onDetachedFromWindow() {
|
|
647
|
+
public override fun onDetachedFromWindow() {
|
|
634
648
|
super.onDetachedFromWindow()
|
|
649
|
+
layoutHandler.removeCallbacks(layoutRunnable)
|
|
650
|
+
// Cancel flow subscriptions so StateFlow collectors don't retain this host
|
|
651
|
+
// after permanent cell eviction. onAttachedToWindow re-subscribes via the
|
|
652
|
+
// existing `if (flowScope == null)` guard; attach(player) calls
|
|
653
|
+
// resubscribeToPlayer unconditionally, which is the canonical re-entry point.
|
|
654
|
+
//
|
|
655
|
+
// Do NOT call stopTimeCoalescing() here — the time coalescer is owned
|
|
656
|
+
// exclusively by activatePlayback/deactivatePlayback. Stopping it on detach
|
|
657
|
+
// breaks the kept-attached-zone scenario where activatePlayback won't re-fire.
|
|
635
658
|
flowScope?.cancel()
|
|
636
659
|
flowScope = null
|
|
637
|
-
isActive = false
|
|
638
|
-
currentCarouselItem = null
|
|
639
|
-
stopTimeCoalescing()
|
|
640
|
-
if (surface?.isRunning == true) {
|
|
641
|
-
surface?.stop()
|
|
642
|
-
}
|
|
643
660
|
}
|
|
661
|
+
|
|
644
662
|
}
|
|
@@ -33,6 +33,7 @@ import com.shortkit.sdk.feed.ShortKitRefreshState
|
|
|
33
33
|
import com.shortkit.sdk.config.VideoOverlayMode
|
|
34
34
|
import com.shortkit.sdk.feed.FeedPreload
|
|
35
35
|
import com.shortkit.sdk.feed.ShortKitFeedFragment
|
|
36
|
+
import com.shortkit.sdk.feed.VideoCarouselCellLongPressState
|
|
36
37
|
import kotlinx.coroutines.CoroutineScope
|
|
37
38
|
import kotlinx.coroutines.Dispatchers
|
|
38
39
|
import kotlinx.coroutines.SupervisorJob
|
|
@@ -446,6 +447,7 @@ class ShortKitBridge(
|
|
|
446
447
|
videos = videoInputs,
|
|
447
448
|
title = itemObj.optString("title", null),
|
|
448
449
|
description = itemObj.optString("description", null),
|
|
450
|
+
initialPageIndex = itemObj.optInt("initialPageIndex", -1).takeIf { it >= 0 },
|
|
449
451
|
author = itemObj.optString("author", null),
|
|
450
452
|
section = itemObj.optString("section", null),
|
|
451
453
|
articleUrl = itemObj.optString("articleUrl", null),
|
|
@@ -551,6 +553,13 @@ class ShortKitBridge(
|
|
|
551
553
|
private var shortKit: ShortKit? = null
|
|
552
554
|
private var scope: CoroutineScope? = null
|
|
553
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Factory for [WritableMap] instances. Overridden in unit tests with
|
|
558
|
+
* `{ JavaOnlyMap() }` to avoid the JNI initialisation that
|
|
559
|
+
* [Arguments.createMap] requires. Production code always uses the default.
|
|
560
|
+
*/
|
|
561
|
+
internal var createMap: () -> WritableMap = { Arguments.createMap() }
|
|
562
|
+
|
|
554
563
|
/** Preload handles keyed by UUID, consumed by feed views via preloadId prop. */
|
|
555
564
|
var preloadHandles = mutableMapOf<String, FeedPreload>()
|
|
556
565
|
private set
|
|
@@ -584,7 +593,8 @@ class ShortKitBridge(
|
|
|
584
593
|
customDimensions = dims,
|
|
585
594
|
debugPanelEnabled = debugPanel,
|
|
586
595
|
serverTracingEnabled = serverTracingEnabled,
|
|
587
|
-
consoleTracingEnabled = consoleTracingEnabled
|
|
596
|
+
consoleTracingEnabled = consoleTracingEnabled,
|
|
597
|
+
poolDebugEnabled = consoleTracingEnabled,
|
|
588
598
|
)
|
|
589
599
|
this.shortKit = sdk
|
|
590
600
|
shared = this
|
|
@@ -732,6 +742,11 @@ class ShortKitBridge(
|
|
|
732
742
|
fragment.onVideoCarouselCellTap = { payload ->
|
|
733
743
|
emitVideoCarouselCellTap(payload.id, payload.index, payload.pageIndex)
|
|
734
744
|
}
|
|
745
|
+
fragment.onVideoCarouselCellLongPress = { payload ->
|
|
746
|
+
emitVideoCarouselCellLongPress(
|
|
747
|
+
payload.id, payload.index, payload.pageIndex, payload.state
|
|
748
|
+
)
|
|
749
|
+
}
|
|
735
750
|
// Per-feed transition event. Fires once per transition, bound to this
|
|
736
751
|
// fragment. Replaces the global player.feedTransition.collect pattern
|
|
737
752
|
// (which fanned every feed's transitions out to every mounted
|
|
@@ -933,6 +948,29 @@ class ShortKitBridge(
|
|
|
933
948
|
emitEventOnMain("onVideoCarouselCellTap", params)
|
|
934
949
|
}
|
|
935
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Emit `onVideoCarouselCellLongPress` with the active surface's feedId.
|
|
953
|
+
* Mirrors iOS ShortKitBridge.emitOnMain("onVideoCarouselCellLongPress").
|
|
954
|
+
* State enum is lowercased so JS receives "began" | "ended" | "cancelled"
|
|
955
|
+
* matching the iOS bridge output.
|
|
956
|
+
*/
|
|
957
|
+
internal fun emitVideoCarouselCellLongPress(
|
|
958
|
+
itemId: String,
|
|
959
|
+
index: Int,
|
|
960
|
+
pageIndex: Int,
|
|
961
|
+
state: VideoCarouselCellLongPressState,
|
|
962
|
+
) {
|
|
963
|
+
val feedId = activeSurfaceFeedId()
|
|
964
|
+
val params = createMap().apply {
|
|
965
|
+
putString("feedId", feedId)
|
|
966
|
+
putString("id", itemId)
|
|
967
|
+
putInt("index", index)
|
|
968
|
+
putInt("pageIndex", pageIndex)
|
|
969
|
+
putString("state", state.name.lowercase())
|
|
970
|
+
}
|
|
971
|
+
emitEventOnMain("onVideoCarouselCellLongPress", params)
|
|
972
|
+
}
|
|
973
|
+
|
|
936
974
|
// ------------------------------------------------------------------
|
|
937
975
|
// Player commands
|
|
938
976
|
// ------------------------------------------------------------------
|
|
@@ -205,6 +205,7 @@ class ShortKitFeedView(context: Context) : FrameLayout(context) {
|
|
|
205
205
|
synchronized(ShortKitBridge.staticPendingFeedViews) {
|
|
206
206
|
ShortKitBridge.staticPendingFeedViews.remove(this)
|
|
207
207
|
}
|
|
208
|
+
handler.removeCallbacks(constrainRunnable)
|
|
208
209
|
// Mirror iOS willMove(toWindow:) at react_native_sdk/ios/ShortKitFeedView.swift:91-99 —
|
|
209
210
|
// suspend only if leaving the window AND the prop says we should be active.
|
|
210
211
|
// When active=false the host has already driven the fragment into deactivate
|
|
@@ -352,7 +352,7 @@ class ShortKitModule(reactContext: ReactApplicationContext) :
|
|
|
352
352
|
"onVideoCarouselCellTap" -> emitOnVideoCarouselCellTap(params)
|
|
353
353
|
"onVideoCarouselItemChanged" -> emitOnVideoCarouselItemChanged(params)
|
|
354
354
|
"onCarouselActiveVideoCompleted" -> emitOnCarouselActiveVideoCompleted(params)
|
|
355
|
-
"
|
|
355
|
+
"onVideoCarouselCellLongPress" -> emitOnVideoCarouselCellLongPress(params)
|
|
356
356
|
else -> {
|
|
357
357
|
android.util.Log.w("SK:Module", "sendEvent: unknown event name '$name', using legacy emitter")
|
|
358
358
|
reactApplicationContext
|
package/android/src/test/java/com/shortkit/reactnative/ReactCarouselOverlayHostLifecycleTest.kt
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import com.facebook.react.bridge.JavaOnlyMap
|
|
7
|
+
import com.facebook.react.interfaces.TaskInterface
|
|
8
|
+
import com.facebook.react.interfaces.fabric.ReactSurface
|
|
9
|
+
import com.shortkit.sdk.model.CarouselImage
|
|
10
|
+
import com.shortkit.sdk.model.ImageCarouselItem
|
|
11
|
+
import kotlinx.coroutines.isActive
|
|
12
|
+
import org.junit.Assert.assertEquals
|
|
13
|
+
import org.junit.Assert.assertFalse
|
|
14
|
+
import org.junit.Assert.assertNotNull
|
|
15
|
+
import org.junit.Assert.assertTrue
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
import org.junit.runner.RunWith
|
|
18
|
+
import org.robolectric.RobolectricTestRunner
|
|
19
|
+
import java.util.concurrent.TimeUnit
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lifecycle contract for ReactCarouselOverlayHost: surface must survive
|
|
23
|
+
* onDetachedFromWindow / onAttachedToWindow cycles (iOS-parity), and
|
|
24
|
+
* resetState() must be a no-op.
|
|
25
|
+
*
|
|
26
|
+
* iOS reference: ReactCarouselOverlayHost.swift has no willMove/didMoveToWindow
|
|
27
|
+
* override — surface only stops in deinit. resetState() is also a no-op on iOS:
|
|
28
|
+
* "Don't clear surface props — configure() will overwrite."
|
|
29
|
+
*
|
|
30
|
+
* Mirrors the contract enforced by ReactVideoCarouselOverlayHostLifecycleTest
|
|
31
|
+
* for the video carousel host.
|
|
32
|
+
*/
|
|
33
|
+
@RunWith(RobolectricTestRunner::class)
|
|
34
|
+
class ReactCarouselOverlayHostLifecycleTest {
|
|
35
|
+
|
|
36
|
+
private object NoOpTask : TaskInterface<Void> {
|
|
37
|
+
override fun waitForCompletion() = Unit
|
|
38
|
+
override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = true
|
|
39
|
+
override fun getResult(): Void? = null
|
|
40
|
+
override fun getError(): Exception? = null
|
|
41
|
+
override fun isCompleted(): Boolean = true
|
|
42
|
+
override fun isCancelled(): Boolean = false
|
|
43
|
+
override fun isFaulted(): Boolean = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mutable-state ReactSurface stub tracking isRunning and stop call count.
|
|
48
|
+
* Mirrors TrackingSurface in ReactVideoCarouselOverlayHostLifecycleTest.
|
|
49
|
+
*/
|
|
50
|
+
private class TrackingSurface : ReactSurface {
|
|
51
|
+
private val ctx: Context = ApplicationProvider.getApplicationContext()
|
|
52
|
+
private var _running: Boolean = false
|
|
53
|
+
var stopCount: Int = 0
|
|
54
|
+
|
|
55
|
+
override val surfaceID: Int = 0
|
|
56
|
+
override val moduleName: String = "TrackingSurface"
|
|
57
|
+
override val isRunning: Boolean get() = _running
|
|
58
|
+
override val view: ViewGroup? = null
|
|
59
|
+
override val context: Context get() = ctx
|
|
60
|
+
override fun prerender(): TaskInterface<Void> = NoOpTask
|
|
61
|
+
override fun start(): TaskInterface<Void> { _running = true; return NoOpTask }
|
|
62
|
+
override fun stop(): TaskInterface<Void> { _running = false; stopCount++; return NoOpTask }
|
|
63
|
+
override fun clear() = Unit
|
|
64
|
+
override fun detach() = Unit
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun makeHost(surface: TrackingSurface): ReactCarouselOverlayHost {
|
|
68
|
+
val ctx = ApplicationProvider.getApplicationContext<Context>()
|
|
69
|
+
return ReactCarouselOverlayHost(ctx).also {
|
|
70
|
+
it.createMap = { JavaOnlyMap() }
|
|
71
|
+
it.createSurface = { _, _ -> surface }
|
|
72
|
+
it.emitEvent = { _, _ -> }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun item(id: String) = ImageCarouselItem(
|
|
77
|
+
id = id,
|
|
78
|
+
images = listOf(CarouselImage(url = "https://example.com/$id.jpg", alt = null)),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
fun `onDetachedFromWindow does not stop surface`() {
|
|
83
|
+
val surface = TrackingSurface()
|
|
84
|
+
val host = makeHost(surface)
|
|
85
|
+
host.configure(item("A"))
|
|
86
|
+
assertTrue("surface must be running after configure", surface.isRunning)
|
|
87
|
+
val stopsBefore = surface.stopCount
|
|
88
|
+
|
|
89
|
+
host.onDetachedFromWindow()
|
|
90
|
+
|
|
91
|
+
assertTrue(
|
|
92
|
+
"surface must remain running after onDetachedFromWindow (iOS-parity)",
|
|
93
|
+
surface.isRunning,
|
|
94
|
+
)
|
|
95
|
+
assertEquals(
|
|
96
|
+
"onDetachedFromWindow must not call surface.stop()",
|
|
97
|
+
stopsBefore,
|
|
98
|
+
surface.stopCount,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun `onDetachedFromWindow preserves currentItemId`() {
|
|
104
|
+
val surface = TrackingSurface()
|
|
105
|
+
val host = makeHost(surface)
|
|
106
|
+
host.configure(item("A"))
|
|
107
|
+
assertEquals("A", host.currentItemId)
|
|
108
|
+
|
|
109
|
+
host.onDetachedFromWindow()
|
|
110
|
+
|
|
111
|
+
assertEquals(
|
|
112
|
+
"currentItemId must survive onDetachedFromWindow so configure() can detect same vs different item",
|
|
113
|
+
"A",
|
|
114
|
+
host.currentItemId,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun `resetState does not stop surface`() {
|
|
120
|
+
// resetState() must be a no-op: iOS ReactCarouselOverlayHost.swift
|
|
121
|
+
// resetState() body is "// Don't clear surface props — configure() will overwrite."
|
|
122
|
+
// Mirrors ReactVideoCarouselOverlayHost.resetState() which is also a no-op.
|
|
123
|
+
// Note: the current production cast (surface as? ReactSurfaceImpl) silently
|
|
124
|
+
// no-ops in Robolectric (stub isn't ReactSurfaceImpl), so this test
|
|
125
|
+
// documents intent and guards against future regressions that add
|
|
126
|
+
// a surface?.stop() call.
|
|
127
|
+
val surface = TrackingSurface()
|
|
128
|
+
val host = makeHost(surface)
|
|
129
|
+
host.configure(item("A"))
|
|
130
|
+
assertTrue("surface must be running after configure", surface.isRunning)
|
|
131
|
+
val stopsBefore = surface.stopCount
|
|
132
|
+
|
|
133
|
+
host.resetState()
|
|
134
|
+
|
|
135
|
+
assertTrue("surface must remain running after resetState", surface.isRunning)
|
|
136
|
+
assertEquals("resetState must not call surface.stop()", stopsBefore, surface.stopCount)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Test
|
|
140
|
+
fun `onDetachedFromWindow cancels ioScope and it is recreated on reattach`() {
|
|
141
|
+
val ctx = ApplicationProvider.getApplicationContext<Context>()
|
|
142
|
+
val host = ReactCarouselOverlayHost(ctx)
|
|
143
|
+
|
|
144
|
+
assertTrue("ioScope must be active initially", host.ioScopeForTest.isActive)
|
|
145
|
+
|
|
146
|
+
host.callOnDetachedFromWindow()
|
|
147
|
+
|
|
148
|
+
assertFalse("ioScope must be cancelled after detach", host.ioScopeForTest.isActive)
|
|
149
|
+
|
|
150
|
+
// Simulate reattach
|
|
151
|
+
host.callOnAttachedToWindow()
|
|
152
|
+
|
|
153
|
+
assertTrue("ioScope must be active again after reattach", host.ioScopeForTest.isActive)
|
|
154
|
+
}
|
|
155
|
+
}
|
package/android/src/test/java/com/shortkit/reactnative/ReactVideoCarouselOverlayHostLifecycleTest.kt
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import com.facebook.react.bridge.JavaOnlyMap
|
|
7
|
+
import com.facebook.react.interfaces.TaskInterface
|
|
8
|
+
import com.facebook.react.interfaces.fabric.ReactSurface
|
|
9
|
+
import com.shortkit.sdk.model.ContentItem
|
|
10
|
+
import com.shortkit.sdk.model.VideoCarouselItem
|
|
11
|
+
import kotlinx.coroutines.CoroutineScope
|
|
12
|
+
import kotlinx.coroutines.Dispatchers
|
|
13
|
+
import kotlinx.coroutines.SupervisorJob
|
|
14
|
+
import kotlinx.coroutines.isActive
|
|
15
|
+
import org.junit.Assert.assertEquals
|
|
16
|
+
import org.junit.Assert.assertFalse
|
|
17
|
+
import org.junit.Assert.assertNotNull
|
|
18
|
+
import org.junit.Assert.assertNull
|
|
19
|
+
import org.junit.Assert.assertTrue
|
|
20
|
+
import org.junit.Test
|
|
21
|
+
import org.junit.runner.RunWith
|
|
22
|
+
import org.robolectric.RobolectricTestRunner
|
|
23
|
+
import java.util.concurrent.TimeUnit
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lifecycle contract for ReactVideoCarouselOverlayHost: surface and state
|
|
27
|
+
* must survive onDetachedFromWindow / onAttachedToWindow cycles
|
|
28
|
+
* (iOS-parity), and only release() should perform true teardown.
|
|
29
|
+
*
|
|
30
|
+
* iOS reference: swift_sdk/.../VideoCarouselCell.swift:472-476 keeps the
|
|
31
|
+
* overlay alive across prepareForReuse; the host's deinit performs final
|
|
32
|
+
* cleanup. Android equivalent: this lifecycle test enforces the same
|
|
33
|
+
* contract on the RN host.
|
|
34
|
+
*/
|
|
35
|
+
@RunWith(RobolectricTestRunner::class)
|
|
36
|
+
class ReactVideoCarouselOverlayHostLifecycleTest {
|
|
37
|
+
|
|
38
|
+
private object NoOpTask : TaskInterface<Void> {
|
|
39
|
+
override fun waitForCompletion() = Unit
|
|
40
|
+
override fun waitForCompletion(duration: Long, timeUnit: TimeUnit): Boolean = true
|
|
41
|
+
override fun getResult(): Void? = null
|
|
42
|
+
override fun getError(): Exception? = null
|
|
43
|
+
override fun isCompleted(): Boolean = true
|
|
44
|
+
override fun isCancelled(): Boolean = false
|
|
45
|
+
override fun isFaulted(): Boolean = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mutable-state ReactSurface stub that tracks isRunning + start/stop
|
|
50
|
+
* calls. Mirrors the StubReactSurface shape used by
|
|
51
|
+
* ReactVideoCarouselOverlayHostEmitTest, but with mutable state via
|
|
52
|
+
* a backing field + custom getter (the interface declares isRunning
|
|
53
|
+
* as `val`, so we can't override with `var`).
|
|
54
|
+
*/
|
|
55
|
+
private class TrackingSurface : ReactSurface {
|
|
56
|
+
private val ctx: Context = ApplicationProvider.getApplicationContext()
|
|
57
|
+
private var _running: Boolean = false
|
|
58
|
+
var stopCount: Int = 0
|
|
59
|
+
|
|
60
|
+
override val surfaceID: Int = 0
|
|
61
|
+
override val moduleName: String = "TrackingSurface"
|
|
62
|
+
override val isRunning: Boolean get() = _running
|
|
63
|
+
override val view: ViewGroup? = null
|
|
64
|
+
override val context: Context get() = ctx
|
|
65
|
+
override fun prerender(): TaskInterface<Void> = NoOpTask
|
|
66
|
+
override fun start(): TaskInterface<Void> { _running = true; return NoOpTask }
|
|
67
|
+
override fun stop(): TaskInterface<Void> { _running = false; stopCount++; return NoOpTask }
|
|
68
|
+
override fun clear() = Unit
|
|
69
|
+
override fun detach() = Unit
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun newHost(surface: TrackingSurface): ReactVideoCarouselOverlayHost {
|
|
73
|
+
val host = ReactVideoCarouselOverlayHost(
|
|
74
|
+
ApplicationProvider.getApplicationContext()
|
|
75
|
+
)
|
|
76
|
+
host.videoCarouselOverlayName = "test"
|
|
77
|
+
host.createSurface = { _, _ -> surface }
|
|
78
|
+
host.createMap = { JavaOnlyMap() }
|
|
79
|
+
host.emitEvent = { _, _ -> /* no-op for lifecycle tests */ }
|
|
80
|
+
return host
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun video(id: String) = ContentItem(
|
|
84
|
+
id = id,
|
|
85
|
+
title = "t",
|
|
86
|
+
duration = 5.0,
|
|
87
|
+
streamingUrl = "https://example.com/$id.m3u8",
|
|
88
|
+
thumbnailUrl = "https://example.com/$id.jpg",
|
|
89
|
+
captionTracks = emptyList(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
private fun item(id: String, videoCount: Int = 1) = VideoCarouselItem(
|
|
93
|
+
id = id,
|
|
94
|
+
videos = (0 until videoCount).map { video("$id-$it") },
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun `onDetachedFromWindow does not stop surface`() {
|
|
99
|
+
val surface = TrackingSurface()
|
|
100
|
+
val host = newHost(surface)
|
|
101
|
+
host.configure(item("c1")) // first mount: applies props, starts surface
|
|
102
|
+
assertTrue("surface must be running after configure", surface.isRunning)
|
|
103
|
+
val stopsBefore = surface.stopCount
|
|
104
|
+
|
|
105
|
+
host.onDetachedFromWindow()
|
|
106
|
+
|
|
107
|
+
assertTrue(
|
|
108
|
+
"surface must remain running after onDetachedFromWindow (iOS-parity)",
|
|
109
|
+
surface.isRunning,
|
|
110
|
+
)
|
|
111
|
+
assertEquals(
|
|
112
|
+
"onDetachedFromWindow must not call surface.stop()",
|
|
113
|
+
stopsBefore,
|
|
114
|
+
surface.stopCount,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun `onDetachedFromWindow preserves currentCarouselItem`() {
|
|
120
|
+
val surface = TrackingSurface()
|
|
121
|
+
val host = newHost(surface)
|
|
122
|
+
host.configure(item("c1"))
|
|
123
|
+
assertNotNull(host.currentCarouselItem)
|
|
124
|
+
|
|
125
|
+
host.onDetachedFromWindow()
|
|
126
|
+
|
|
127
|
+
assertNotNull(
|
|
128
|
+
"currentCarouselItem must survive onDetachedFromWindow so re-attach can detect same vs different item",
|
|
129
|
+
host.currentCarouselItem,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
fun `onDetachedFromWindow preserves isActive`() {
|
|
135
|
+
// Suspend/resume scenario: active cell never changes, so the feed's
|
|
136
|
+
// active-cell propagation does not re-fire activatePlayback on
|
|
137
|
+
// resume. If detach flipped isActive false, every subsequent
|
|
138
|
+
// updateActiveVideo emit would be gated out and per-page swipes
|
|
139
|
+
// wouldn't update the overlay. iOS-parity: isActive is owned
|
|
140
|
+
// exclusively by activatePlayback / deactivatePlayback; lifecycle
|
|
141
|
+
// hooks must not touch it.
|
|
142
|
+
val surface = TrackingSurface()
|
|
143
|
+
val host = newHost(surface)
|
|
144
|
+
host.configure(item("c1"))
|
|
145
|
+
host.activatePlayback()
|
|
146
|
+
assertTrue(host.isActive)
|
|
147
|
+
|
|
148
|
+
host.onDetachedFromWindow()
|
|
149
|
+
|
|
150
|
+
assertTrue(
|
|
151
|
+
"isActive must survive onDetachedFromWindow — only activatePlayback/deactivatePlayback own it",
|
|
152
|
+
host.isActive,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun `onDetachedFromWindow does not stop time coalescer`() {
|
|
158
|
+
// If detach stopped the coalescer, on resume nothing restarts it
|
|
159
|
+
// (activatePlayback is the only caller and doesn't re-fire when
|
|
160
|
+
// the active cell never changed). Time emits would silently stop.
|
|
161
|
+
// Mirrors the isActive ownership rule: detach is a no-op for
|
|
162
|
+
// playback-driven state.
|
|
163
|
+
val surface = TrackingSurface()
|
|
164
|
+
val host = newHost(surface)
|
|
165
|
+
host.configure(item("c1"))
|
|
166
|
+
host.activatePlayback()
|
|
167
|
+
// activatePlayback() is what kicks off time coalescing — once
|
|
168
|
+
// running, it should survive detach.
|
|
169
|
+
|
|
170
|
+
// No assertion on internals; the contract is observable via the
|
|
171
|
+
// updateActiveVideo regression test above. This test exists to
|
|
172
|
+
// document the non-destructive-detach intent: onDetachedFromWindow
|
|
173
|
+
// must not call stopTimeCoalescing().
|
|
174
|
+
host.onDetachedFromWindow()
|
|
175
|
+
|
|
176
|
+
// host.isActive still true => the coalesce runnable would still
|
|
177
|
+
// tick if it weren't cancelled. We cannot easily inspect the
|
|
178
|
+
// handler queue from here, but we can assert the public state
|
|
179
|
+
// hasn't been disturbed:
|
|
180
|
+
assertTrue("isActive untouched", host.isActive)
|
|
181
|
+
assertNotNull("currentCarouselItem untouched", host.currentCarouselItem)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@Test
|
|
185
|
+
fun `onDetachedFromWindow cancels flowScope`() {
|
|
186
|
+
val ctx = ApplicationProvider.getApplicationContext<Context>()
|
|
187
|
+
val host = ReactVideoCarouselOverlayHost(ctx)
|
|
188
|
+
|
|
189
|
+
// Inject a live scope directly via the writable test accessor.
|
|
190
|
+
val injected = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
191
|
+
host.flowScopeForTest = injected
|
|
192
|
+
|
|
193
|
+
assertTrue("injected scope must be active before detach", injected.isActive)
|
|
194
|
+
|
|
195
|
+
host.callOnDetachedFromWindow()
|
|
196
|
+
|
|
197
|
+
assertNull("flowScope must be null after detach", host.flowScopeForTest)
|
|
198
|
+
assertFalse("injected scope must be cancelled after detach", injected.isActive)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package com.shortkit.reactnative
|
|
2
|
+
|
|
3
|
+
import androidx.test.core.app.ApplicationProvider
|
|
4
|
+
import com.facebook.react.bridge.JavaOnlyMap
|
|
5
|
+
import com.facebook.react.bridge.WritableMap
|
|
6
|
+
import com.shortkit.sdk.feed.VideoCarouselCellLongPressState
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import org.junit.runner.RunWith
|
|
10
|
+
import org.robolectric.RobolectricTestRunner
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* JVM unit tests for [ShortKitBridge.emitVideoCarouselCellLongPress].
|
|
14
|
+
*
|
|
15
|
+
* Verifies the event name, state-string lowercasing for all three enum values,
|
|
16
|
+
* and the full payload shape (feedId, id, index, pageIndex).
|
|
17
|
+
*
|
|
18
|
+
* The [JavaOnlyMap] seam avoids the JNI initialisation that
|
|
19
|
+
* [com.facebook.react.bridge.Arguments.createMap] requires in host tests
|
|
20
|
+
* (same pattern used in ReactVideoCarouselOverlayHostEmitTest).
|
|
21
|
+
*/
|
|
22
|
+
@RunWith(RobolectricTestRunner::class)
|
|
23
|
+
class ShortKitBridgeLongPressTest {
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Construct a minimal [ShortKitBridge] with a captured [emitEvent] lambda.
|
|
27
|
+
* [JavaOnlyMap] is injected via the [ShortKitBridge.createMap] seam so
|
|
28
|
+
* [Arguments.createMap] (which requires JNI) is never called in tests.
|
|
29
|
+
*/
|
|
30
|
+
private fun makeBridge(
|
|
31
|
+
onEmit: (String, WritableMap) -> Unit,
|
|
32
|
+
): ShortKitBridge {
|
|
33
|
+
val bridge = ShortKitBridge(
|
|
34
|
+
apiKey = "pk_test_unit",
|
|
35
|
+
context = ApplicationProvider.getApplicationContext(),
|
|
36
|
+
hasLoadingView = false,
|
|
37
|
+
clientAppName = null,
|
|
38
|
+
clientAppVersion = null,
|
|
39
|
+
customDimensionsJSON = null,
|
|
40
|
+
debugPanel = false,
|
|
41
|
+
serverTracingEnabled = false,
|
|
42
|
+
consoleTracingEnabled = false,
|
|
43
|
+
emitEvent = onEmit,
|
|
44
|
+
)
|
|
45
|
+
// Override the map factory before any emit call so the bridge never
|
|
46
|
+
// reaches Arguments.createMap() in emitVideoCarouselCellLongPress.
|
|
47
|
+
bridge.createMap = { JavaOnlyMap() }
|
|
48
|
+
return bridge
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ------------------------------------------------------------------
|
|
52
|
+
// State-string lowercasing
|
|
53
|
+
// ------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun emitsBegan() {
|
|
57
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
58
|
+
val bridge = makeBridge { name, params -> emitted.add(name to params) }
|
|
59
|
+
|
|
60
|
+
bridge.emitVideoCarouselCellLongPress(
|
|
61
|
+
itemId = "item-1",
|
|
62
|
+
index = 0,
|
|
63
|
+
pageIndex = 0,
|
|
64
|
+
state = VideoCarouselCellLongPressState.BEGAN,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assertEquals(1, emitted.size)
|
|
68
|
+
assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
|
|
69
|
+
assertEquals("began", emitted[0].second.getString("state"))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
fun emitsEnded() {
|
|
74
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
75
|
+
val bridge = makeBridge { name, params -> emitted.add(name to params) }
|
|
76
|
+
|
|
77
|
+
bridge.emitVideoCarouselCellLongPress(
|
|
78
|
+
itemId = "item-1",
|
|
79
|
+
index = 0,
|
|
80
|
+
pageIndex = 0,
|
|
81
|
+
state = VideoCarouselCellLongPressState.ENDED,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assertEquals(1, emitted.size)
|
|
85
|
+
assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
|
|
86
|
+
assertEquals("ended", emitted[0].second.getString("state"))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
fun emitsCancelled() {
|
|
91
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
92
|
+
val bridge = makeBridge { name, params -> emitted.add(name to params) }
|
|
93
|
+
|
|
94
|
+
bridge.emitVideoCarouselCellLongPress(
|
|
95
|
+
itemId = "item-1",
|
|
96
|
+
index = 0,
|
|
97
|
+
pageIndex = 0,
|
|
98
|
+
state = VideoCarouselCellLongPressState.CANCELLED,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assertEquals(1, emitted.size)
|
|
102
|
+
assertEquals("onVideoCarouselCellLongPress", emitted[0].first)
|
|
103
|
+
assertEquals("cancelled", emitted[0].second.getString("state"))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ------------------------------------------------------------------
|
|
107
|
+
// Payload shape
|
|
108
|
+
// ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
fun emitsCorrectPayloadFields() {
|
|
112
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
113
|
+
val bridge = makeBridge { name, params -> emitted.add(name to params) }
|
|
114
|
+
|
|
115
|
+
bridge.emitVideoCarouselCellLongPress(
|
|
116
|
+
itemId = "car-1",
|
|
117
|
+
index = 3,
|
|
118
|
+
pageIndex = 2,
|
|
119
|
+
state = VideoCarouselCellLongPressState.BEGAN,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
val params = emitted[0].second
|
|
123
|
+
assertEquals("car-1", params.getString("id"))
|
|
124
|
+
assertEquals(3, params.getInt("index"))
|
|
125
|
+
assertEquals(2, params.getInt("pageIndex"))
|
|
126
|
+
assertEquals("began", params.getString("state"))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
fun emitsFeedId() {
|
|
131
|
+
val emitted = mutableListOf<Pair<String, WritableMap>>()
|
|
132
|
+
val bridge = makeBridge { name, params -> emitted.add(name to params) }
|
|
133
|
+
|
|
134
|
+
bridge.emitVideoCarouselCellLongPress(
|
|
135
|
+
itemId = "item-1",
|
|
136
|
+
index = 0,
|
|
137
|
+
pageIndex = 0,
|
|
138
|
+
state = VideoCarouselCellLongPressState.BEGAN,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// activeSurfaceFeedId() returns "" when no active surface is registered
|
|
142
|
+
// (no ShortKitFeedFragment has been attached in this unit-test environment).
|
|
143
|
+
// The important guarantee is that the "feedId" key is present in the map,
|
|
144
|
+
// matching the iOS bridge payload shape.
|
|
145
|
+
val params = emitted[0].second
|
|
146
|
+
assertEquals("", params.getString("feedId"))
|
|
147
|
+
}
|
|
148
|
+
}
|
package/ios/ShortKitBridge.swift
CHANGED
|
@@ -208,7 +208,8 @@ import ShortKitSDK
|
|
|
208
208
|
customDimensions: dimensions,
|
|
209
209
|
debugPanelEnabled: debugPanel,
|
|
210
210
|
serverTracingEnabled: serverTracingEnabled,
|
|
211
|
-
consoleTracingEnabled: consoleTracingEnabled
|
|
211
|
+
consoleTracingEnabled: consoleTracingEnabled,
|
|
212
|
+
poolDebugEnabled: false
|
|
212
213
|
)
|
|
213
214
|
self.shortKit = sdk
|
|
214
215
|
|
|
@@ -8,32 +8,32 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>ShortKitSDK.framework/ShortKitSDK</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-
|
|
11
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>ShortKitSDK.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
15
15
|
<array>
|
|
16
16
|
<string>arm64</string>
|
|
17
|
+
<string>x86_64</string>
|
|
17
18
|
</array>
|
|
18
19
|
<key>SupportedPlatform</key>
|
|
19
20
|
<string>ios</string>
|
|
21
|
+
<key>SupportedPlatformVariant</key>
|
|
22
|
+
<string>simulator</string>
|
|
20
23
|
</dict>
|
|
21
24
|
<dict>
|
|
22
25
|
<key>BinaryPath</key>
|
|
23
26
|
<string>ShortKitSDK.framework/ShortKitSDK</string>
|
|
24
27
|
<key>LibraryIdentifier</key>
|
|
25
|
-
<string>ios-
|
|
28
|
+
<string>ios-arm64</string>
|
|
26
29
|
<key>LibraryPath</key>
|
|
27
30
|
<string>ShortKitSDK.framework</string>
|
|
28
31
|
<key>SupportedArchitectures</key>
|
|
29
32
|
<array>
|
|
30
33
|
<string>arm64</string>
|
|
31
|
-
<string>x86_64</string>
|
|
32
34
|
</array>
|
|
33
35
|
<key>SupportedPlatform</key>
|
|
34
36
|
<string>ios</string>
|
|
35
|
-
<key>SupportedPlatformVariant</key>
|
|
36
|
-
<string>simulator</string>
|
|
37
37
|
</dict>
|
|
38
38
|
</array>
|
|
39
39
|
<key>CFBundlePackageType</key>
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
<key>CFBundlePackageType</key>
|
|
12
12
|
<string>FMWK</string>
|
|
13
13
|
<key>CFBundleVersion</key>
|
|
14
|
-
<string>0.2.
|
|
14
|
+
<string>0.2.39</string>
|
|
15
15
|
<key>CFBundleShortVersionString</key>
|
|
16
|
-
<string>0.2.
|
|
16
|
+
<string>0.2.39</string>
|
|
17
17
|
<key>MinimumOSVersion</key>
|
|
18
18
|
<string>16.0</string>
|
|
19
19
|
</dict>
|
|
@@ -52850,14 +52850,14 @@
|
|
|
52850
52850
|
{
|
|
52851
52851
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52852
52852
|
"kind": "BooleanLiteral",
|
|
52853
|
-
"offset":
|
|
52853
|
+
"offset": 45026,
|
|
52854
52854
|
"length": 4,
|
|
52855
52855
|
"value": "true"
|
|
52856
52856
|
},
|
|
52857
52857
|
{
|
|
52858
52858
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52859
52859
|
"kind": "IntegerLiteral",
|
|
52860
|
-
"offset":
|
|
52860
|
+
"offset": 52291,
|
|
52861
52861
|
"length": 1,
|
|
52862
52862
|
"value": "0"
|
|
52863
52863
|
},
|
|
@@ -53517,7 +53517,7 @@
|
|
|
53517
53517
|
"kind": "StringLiteral",
|
|
53518
53518
|
"offset": 154,
|
|
53519
53519
|
"length": 8,
|
|
53520
|
-
"value": "\"0.2.
|
|
53520
|
+
"value": "\"0.2.39\""
|
|
53521
53521
|
},
|
|
53522
53522
|
{
|
|
53523
53523
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
|
|
Binary file
|
package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
</data>
|
|
11
11
|
<key>Info.plist</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
DfvOpn/tL7HwpstLyexoHO9tmlc=
|
|
14
14
|
</data>
|
|
15
15
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json</key>
|
|
16
16
|
<data>
|
|
17
|
-
|
|
17
|
+
AhyuvP9Jp9aQmkTfkyn18bZWgYE=
|
|
18
18
|
</data>
|
|
19
19
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
|
|
20
20
|
<data>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<dict>
|
|
51
51
|
<key>hash2</key>
|
|
52
52
|
<data>
|
|
53
|
-
|
|
53
|
+
NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
|
|
54
54
|
</data>
|
|
55
55
|
</dict>
|
|
56
56
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface</key>
|
package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
<key>CFBundlePackageType</key>
|
|
12
12
|
<string>FMWK</string>
|
|
13
13
|
<key>CFBundleVersion</key>
|
|
14
|
-
<string>0.2.
|
|
14
|
+
<string>0.2.39</string>
|
|
15
15
|
<key>CFBundleShortVersionString</key>
|
|
16
|
-
<string>0.2.
|
|
16
|
+
<string>0.2.39</string>
|
|
17
17
|
<key>MinimumOSVersion</key>
|
|
18
18
|
<string>16.0</string>
|
|
19
19
|
</dict>
|
|
@@ -52850,14 +52850,14 @@
|
|
|
52850
52850
|
{
|
|
52851
52851
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52852
52852
|
"kind": "BooleanLiteral",
|
|
52853
|
-
"offset":
|
|
52853
|
+
"offset": 45026,
|
|
52854
52854
|
"length": 4,
|
|
52855
52855
|
"value": "true"
|
|
52856
52856
|
},
|
|
52857
52857
|
{
|
|
52858
52858
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52859
52859
|
"kind": "IntegerLiteral",
|
|
52860
|
-
"offset":
|
|
52860
|
+
"offset": 52291,
|
|
52861
52861
|
"length": 1,
|
|
52862
52862
|
"value": "0"
|
|
52863
52863
|
},
|
|
@@ -53517,7 +53517,7 @@
|
|
|
53517
53517
|
"kind": "StringLiteral",
|
|
53518
53518
|
"offset": 154,
|
|
53519
53519
|
"length": 8,
|
|
53520
|
-
"value": "\"0.2.
|
|
53520
|
+
"value": "\"0.2.39\""
|
|
53521
53521
|
},
|
|
53522
53522
|
{
|
|
53523
53523
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
|
|
@@ -52850,14 +52850,14 @@
|
|
|
52850
52850
|
{
|
|
52851
52851
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52852
52852
|
"kind": "BooleanLiteral",
|
|
52853
|
-
"offset":
|
|
52853
|
+
"offset": 45026,
|
|
52854
52854
|
"length": 4,
|
|
52855
52855
|
"value": "true"
|
|
52856
52856
|
},
|
|
52857
52857
|
{
|
|
52858
52858
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/Feed\/VideoCarouselCell.swift",
|
|
52859
52859
|
"kind": "IntegerLiteral",
|
|
52860
|
-
"offset":
|
|
52860
|
+
"offset": 52291,
|
|
52861
52861
|
"length": 1,
|
|
52862
52862
|
"value": "0"
|
|
52863
52863
|
},
|
|
@@ -53517,7 +53517,7 @@
|
|
|
53517
53517
|
"kind": "StringLiteral",
|
|
53518
53518
|
"offset": 154,
|
|
53519
53519
|
"length": 8,
|
|
53520
|
-
"value": "\"0.2.
|
|
53520
|
+
"value": "\"0.2.39\""
|
|
53521
53521
|
},
|
|
53522
53522
|
{
|
|
53523
53523
|
"filePath": "\/Users\/michaelseleman\/shortkit\/swift_sdk\/Sources\/ShortKit\/ShortKit.swift",
|
package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK
CHANGED
|
Binary file
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
</data>
|
|
11
11
|
<key>Info.plist</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
DfvOpn/tL7HwpstLyexoHO9tmlc=
|
|
14
14
|
</data>
|
|
15
15
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json</key>
|
|
16
16
|
<data>
|
|
17
|
-
|
|
17
|
+
AhyuvP9Jp9aQmkTfkyn18bZWgYE=
|
|
18
18
|
</data>
|
|
19
19
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
|
|
20
20
|
<data>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
</data>
|
|
31
31
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json</key>
|
|
32
32
|
<data>
|
|
33
|
-
|
|
33
|
+
AhyuvP9Jp9aQmkTfkyn18bZWgYE=
|
|
34
34
|
</data>
|
|
35
35
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
|
|
36
36
|
<data>
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<dict>
|
|
67
67
|
<key>hash2</key>
|
|
68
68
|
<data>
|
|
69
|
-
|
|
69
|
+
NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
|
|
70
70
|
</data>
|
|
71
71
|
</dict>
|
|
72
72
|
<key>Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface</key>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
<dict>
|
|
95
95
|
<key>hash2</key>
|
|
96
96
|
<data>
|
|
97
|
-
|
|
97
|
+
NxZxhP9cPb/bnY8osKT6QvOK/iH3xHbDQNIHRYJGfQ0=
|
|
98
98
|
</data>
|
|
99
99
|
</dict>
|
|
100
100
|
<key>Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface</key>
|