From 278c38f9ea66e33c5a5826aa1184f5f64301b819 Mon Sep 17 00:00:00 2001 From: sozinov Date: Fri, 27 Feb 2026 11:59:00 +0300 Subject: [PATCH 1/2] MOBILEWEBVIEW-60: add timeToDisplay for Inapp.Show action --- kmp-common-sdk | 2 +- .../di/modules/PresentationModule.kt | 3 +- .../deserializers/InAppTagsDeserializer.kt | 29 ++++ .../managers/InAppSerializationManagerImpl.kt | 25 ++- .../inapp/data/mapper/InAppMapper.kt | 6 +- .../data/repositories/InAppRepositoryImpl.kt | 8 +- .../inapp/domain/InAppInteractorImpl.kt | 18 ++- .../interfaces/interactors/InAppInteractor.kt | 11 +- .../managers/InAppSerializationManager.kt | 4 +- .../repositories/InAppRepository.kt | 2 +- .../inapp/domain/models/InAppConfig.kt | 1 + .../inapp/domain/models/InAppTypeWrapper.kt | 3 +- .../InAppMessageDelayedManager.kt | 12 +- .../presentation/InAppMessageManagerImpl.kt | 30 +++- .../presentation/InAppMessageViewDisplayer.kt | 3 +- .../InAppMessageViewDisplayerImpl.kt | 10 +- .../view/WebViewInappViewHolder.kt | 2 + .../operation/request/InAppHandleRequest.kt | 9 ++ .../operation/response/InAppConfigResponse.kt | 6 + .../InAppTagsDeserializerTest.kt | 102 ++++++++++++ .../managers/InAppSerializationManagerTest.kt | 47 ++++-- .../MobileConfigSerializationManagerTest.kt | 146 ++++++++++++++++++ .../inapp/data/mapper/InAppMapperTest.kt | 116 +++++++++++++- .../data/repositories/InAppRepositoryTest.kt | 24 +-- .../inapp/domain/InAppInteractorImplTest.kt | 18 +-- .../InAppMessageDelayedManagerTest.kt | 36 ++--- .../presentation/InAppMessageManagerTest.kt | 69 +++++---- .../mindbox/mobile_sdk/models/InAppStub.kt | 9 +- ...igWithSettingsABTestsMonitoringInapps.json | 4 + 29 files changed, 634 insertions(+), 121 deletions(-) create mode 100644 sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt create mode 100644 sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt diff --git a/kmp-common-sdk b/kmp-common-sdk index 7d0a46995..80e199ff1 160000 --- a/kmp-common-sdk +++ b/kmp-common-sdk @@ -1 +1 @@ -Subproject commit 7d0a469952c5291af01ded0cef9204aa1a5c466d +Subproject commit 80e199ff1d25f1bed6283287b4a89be6e3e65e10 diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt index ec8be65c0..de83c9b68 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/di/modules/PresentationModule.kt @@ -28,7 +28,8 @@ internal fun PresentationModule( monitoringInteractor = monitoringInteractor, sessionStorageManager = sessionStorageManager, userVisitManager = userVisitManager, - inAppMessageDelayedManager = inAppMessageDelayedManager + inAppMessageDelayedManager = inAppMessageDelayedManager, + timeProvider = timeProvider ) } override val clipboardManager: ClipboardManager by lazy { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt new file mode 100644 index 000000000..415e6e2f1 --- /dev/null +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializer.kt @@ -0,0 +1,29 @@ +package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import java.lang.reflect.Type + +internal class InAppTagsDeserializer : JsonDeserializer?> { + + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext, + ): Map? { + if (json.isJsonNull) return null + if (!json.isJsonObject) return null + return json.asJsonObject.entrySet().mapNotNull { (key, value) -> + if (value.isJsonPrimitive && value.asJsonPrimitive.isString) { + key to value.asString + } else { + null + } + }.toMap() + } + + companion object { + const val TAGS = "tags" + } +} diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt index 674168cd3..dc5400e2a 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt @@ -4,6 +4,7 @@ import cloud.mindbox.mobile_sdk.fromJsonTyped import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppSerializationManager import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppFailuresWrapper import cloud.mindbox.mobile_sdk.models.operation.request.InAppHandleRequest +import cloud.mindbox.mobile_sdk.models.operation.request.InAppShowRequest import cloud.mindbox.mobile_sdk.models.operation.request.InAppShowFailure import cloud.mindbox.mobile_sdk.toJsonTyped import cloud.mindbox.mobile_sdk.utils.LoggingExceptionHandler @@ -13,9 +14,29 @@ import com.google.gson.reflect.TypeToken internal class InAppSerializationManagerImpl(private val gson: Gson) : InAppSerializationManager { - override fun serializeToInAppHandledString(inAppId: String): String { + override fun serializeToInAppShownActionString( + inAppId: String, + timeToDisplay: String, + tags: Map?, + ): String { + return LoggingExceptionHandler.runCatching("") { + gson.toJson( + InAppShowRequest( + inAppId = inAppId, + timeToDisplay = timeToDisplay, + tags = tags, + ), + InAppShowRequest::class.java, + ) + } + } + + override fun serializeToInAppActionString(inAppId: String): String { return LoggingExceptionHandler.runCatching("") { - gson.toJson(InAppHandleRequest(inAppId), InAppHandleRequest::class.java) + gson.toJson( + InAppHandleRequest(inAppId = inAppId), + InAppHandleRequest::class.java, + ) } } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt index 60f617b59..35715f30c 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapper.kt @@ -62,7 +62,8 @@ internal class InAppMapper { sdkVersion = inApp.sdkVersion, targeting = targetingDto, frequency = frequencyDto, - form = formDto + form = formDto, + tags = inApp.tags, ) } } @@ -303,7 +304,8 @@ internal class InAppMapper { ), minVersion = inAppDto.sdkVersion?.minVersion, maxVersion = inAppDto.sdkVersion?.maxVersion, - frequency = Frequency(getDelay(inAppDto.frequency)) + frequency = Frequency(getDelay(inAppDto.frequency)), + tags = inAppDto.tags?.takeIf { it.isNotEmpty() } ) } ?: emptyList(), monitoring = inAppConfigResponse.monitoring?.map { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryImpl.kt index f6f8d044e..91cc66bf1 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryImpl.kt @@ -98,8 +98,8 @@ internal class InAppRepositoryImpl( mindboxLogI("Increase count of shown inapp per day") } - override fun sendInAppShown(inAppId: String) { - inAppSerializationManager.serializeToInAppHandledString(inAppId).apply { + override fun sendInAppShown(inAppId: String, timeToDisplay: String, tags: Map?) { + inAppSerializationManager.serializeToInAppShownActionString(inAppId, timeToDisplay, tags).apply { if (isNotBlank()) { MindboxEventManager.inAppShown( context, @@ -110,7 +110,7 @@ internal class InAppRepositoryImpl( } override fun sendInAppClicked(inAppId: String) { - inAppSerializationManager.serializeToInAppHandledString(inAppId).apply { + inAppSerializationManager.serializeToInAppActionString(inAppId).apply { if (isNotBlank()) { MindboxEventManager.inAppClicked( context, @@ -121,7 +121,7 @@ internal class InAppRepositoryImpl( } override fun sendUserTargeted(inAppId: String) { - inAppSerializationManager.serializeToInAppHandledString(inAppId).apply { + inAppSerializationManager.serializeToInAppActionString(inAppId).apply { if (isNotBlank()) { MindboxEventManager.sendUserTargeted( context, diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt index 26b020c35..fd20beb41 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt @@ -40,7 +40,7 @@ internal class InAppInteractorImpl( private val inAppTargetingChannel = Channel(Channel.UNLIMITED) - override suspend fun processEventAndConfig(): Flow { + override suspend fun processEventAndConfig(): Flow> { val inApps: List = mobileConfigRepository.getInAppsSection() .let { inApps -> inAppRepository.saveCurrentSessionInApps(inApps) @@ -66,6 +66,7 @@ internal class InAppInteractorImpl( .onEach { mindboxLogD("Event triggered: ${it.name}") }.map { event -> + val triggerTimeMillis = timeProvider.currentTimeMillis() val filteredInApps = inAppFilteringManager.filterUnShownInAppsByEvent(inApps, event).let { inAppFrequencyManager.filterInAppsFrequency(it) } @@ -83,10 +84,10 @@ internal class InAppInteractorImpl( inApp?.let { sessionStorageManager.inAppTriggerEvent = event } - inApp + inApp?.let { it to (timeProvider.currentTimeMillis() - triggerTimeMillis) } } - .onEach { inApp -> - inApp?.let { mindboxLogI("InApp ${inApp.id} isPriority=${inApp.isPriority}, delayTime=${inApp.delayTime}, skipLimitChecks=${inApp.isPriority}") } + .onEach { pair -> + pair?.let { (inApp, preparedTime) -> mindboxLogI("InApp ${inApp.id} isPriority=${inApp.isPriority}, delayTime=${inApp.delayTime}, skipLimitChecks=${inApp.isPriority}, preparedTime = $preparedTime ms") } ?: mindboxLogI("No inapps to show found") } .filterNotNull() @@ -104,9 +105,14 @@ internal class InAppInteractorImpl( ) } - override fun saveShownInApp(id: String, timeStamp: Long) { + override fun saveShownInApp( + id: String, + timeStamp: Long, + timeToDisplay: String, + tags: Map? + ) { inAppRepository.setInAppShown(id) - inAppRepository.sendInAppShown(id) + inAppRepository.sendInAppShown(id, timeToDisplay, tags) inAppRepository.saveShownInApp(id, timeStamp) inAppRepository.saveInAppStateChangeTime(timeStamp.toTimestamp()) } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt index cf558beb4..b3544e34f 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt @@ -9,9 +9,14 @@ internal interface InAppInteractor { fun setInAppShown(inAppId: String) - suspend fun processEventAndConfig(): Flow - - fun saveShownInApp(id: String, timeStamp: Long) + suspend fun processEventAndConfig(): Flow> + + fun saveShownInApp( + id: String, + timeStamp: Long, + timeToDisplay: String, + tags: Map? + ) fun sendInAppClicked(inAppId: String) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/InAppSerializationManager.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/InAppSerializationManager.kt index c4b92df67..e01acce06 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/InAppSerializationManager.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/managers/InAppSerializationManager.kt @@ -8,7 +8,9 @@ internal interface InAppSerializationManager { fun deserializeToShownInAppsMap(shownInApps: String): Map> - fun serializeToInAppHandledString(inAppId: String): String + fun serializeToInAppShownActionString(inAppId: String, timeToDisplay: String, tags: Map?): String + + fun serializeToInAppActionString(inAppId: String): String fun serializeToInAppShowFailuresString(inAppShowFailures: List): String diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/repositories/InAppRepository.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/repositories/InAppRepository.kt index f2167b44c..eba668ef2 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/repositories/InAppRepository.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/repositories/InAppRepository.kt @@ -29,7 +29,7 @@ internal interface InAppRepository { fun saveShownInApp(id: String, timeStamp: Long) - fun sendInAppShown(inAppId: String) + fun sendInAppShown(inAppId: String, timeToDisplay: String, tags: Map?) fun sendInAppClicked(inAppId: String) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt index 5283addda..e5a3f75b6 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppConfig.kt @@ -28,6 +28,7 @@ internal data class InApp( val frequency: Frequency, val targeting: TreeTargeting, val form: Form, + val tags: Map?, ) internal data class Frequency(val delay: Delay) { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppTypeWrapper.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppTypeWrapper.kt index 0fab35bf6..f520c7ab9 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppTypeWrapper.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/models/InAppTypeWrapper.kt @@ -4,7 +4,8 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppActionCallbacks internal data class InAppTypeWrapper( val inAppType: T, - val inAppActionCallbacks: InAppActionCallbacks + val inAppActionCallbacks: InAppActionCallbacks, + val onRenderStart: () -> Unit, ) internal fun interface OnInAppClick { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt index 266e242fd..b820d587b 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt @@ -33,16 +33,17 @@ internal class InAppMessageDelayedManager(private val timeProvider: TimeProvider pendingInAppComparator ) - private val _inAppToShowFlow = MutableSharedFlow() + private val _inAppToShowFlow = MutableSharedFlow>() val inAppToShowFlow = _inAppToShowFlow.asSharedFlow() private data class PendingInApp( val inApp: InApp, val showTimeMillis: Long, - val sequenceNumber: Long + val sequenceNumber: Long, + val preparedTimeMs: Long, ) - internal fun process(inApp: InApp) { + internal fun process(inApp: InApp, preparedTimeMs: Long) { coroutineScope.launchWithLock(processingMutex) { mindboxLogD("Processing In-App: ${inApp.id}, Priority: ${inApp.isPriority}, Delay: ${inApp.delayTime}") val delay = inApp.delayTime?.interval ?: 0L @@ -52,7 +53,8 @@ internal class InAppMessageDelayedManager(private val timeProvider: TimeProvider PendingInApp( inApp = inApp, showTimeMillis = showTime, - sequenceNumber = sequenceNumber.getAndIncrement() + sequenceNumber = sequenceNumber.getAndIncrement(), + preparedTimeMs = preparedTimeMs, ) ) processQueue() @@ -73,7 +75,7 @@ internal class InAppMessageDelayedManager(private val timeProvider: TimeProvider pendingInApps.pollIf { it.showTimeMillis <= now }?.let { showCandidate -> mindboxLogI("Winner found: ${showCandidate.inApp.id}. Emitting to show.") - _inAppToShowFlow.emit(showCandidate.inApp) + _inAppToShowFlow.emit(showCandidate.inApp to showCandidate.preparedTimeMs) do { val inApp = pendingInApps.pollIf { it.showTimeMillis <= now }.also { discarded -> diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt index 18c418e1e..a1031ccd4 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt @@ -6,16 +6,19 @@ import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.interactors.InAppInteractor import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.InAppActionCallbacks +import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType import cloud.mindbox.mobile_sdk.inapp.domain.models.OnInAppClick import cloud.mindbox.mobile_sdk.inapp.domain.models.OnInAppDismiss import cloud.mindbox.mobile_sdk.inapp.domain.models.OnInAppShown import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl import cloud.mindbox.mobile_sdk.logger.mindboxLogI +import cloud.mindbox.mobile_sdk.millisToTimeSpan import cloud.mindbox.mobile_sdk.managers.MindboxEventManager import cloud.mindbox.mobile_sdk.managers.UserVisitManager import cloud.mindbox.mobile_sdk.monitoring.domain.interfaces.MonitoringInteractor import cloud.mindbox.mobile_sdk.repository.MindboxPreferences import cloud.mindbox.mobile_sdk.utils.LoggingExceptionHandler +import cloud.mindbox.mobile_sdk.utils.TimeProvider import com.android.volley.VolleyError import kotlinx.coroutines.* import kotlinx.coroutines.flow.collect @@ -28,7 +31,8 @@ internal class InAppMessageManagerImpl( private val monitoringInteractor: MonitoringInteractor, private val sessionStorageManager: SessionStorageManager, private val userVisitManager: UserVisitManager, - private val inAppMessageDelayedManager: InAppMessageDelayedManager + private val inAppMessageDelayedManager: InAppMessageDelayedManager, + private val timeProvider: TimeProvider ) : InAppMessageManager { init { @@ -65,15 +69,15 @@ internal class InAppMessageManagerImpl( private suspend fun handleInAppFromInteractor() { inAppInteractor.processEventAndConfig() - .onEach { inApp -> + .onEach { (inApp, preparedTimeMs) -> mindboxLogI("Got in-app from interactor: ${inApp.id}. Processing with DelayedManager.") - inAppMessageDelayedManager.process(inApp) + inAppMessageDelayedManager.process(inApp, preparedTimeMs) } .collect() } private suspend fun handleInAppFromDelayedManager() { - inAppMessageDelayedManager.inAppToShowFlow.collect { inApp -> + inAppMessageDelayedManager.inAppToShowFlow.collect { (inApp, preparedTimeMs) -> mindboxLogI("Got in-app from DelayedManager: ${inApp.id}") withContext(Dispatchers.Main) { if (inAppMessageViewDisplayer.isInAppActive()) { @@ -92,14 +96,18 @@ internal class InAppMessageManagerImpl( return@withContext } + var renderStartTimeMs = 0L + val tags = inApp.tags?.takeIf { it.isNotEmpty() } + inAppMessageViewDisplayer.tryShowInAppMessage( inAppType = inAppMessage, + onRenderStart = { renderStartTimeMs = timeProvider.currentTimeMillis() }, inAppActionCallbacks = object : InAppActionCallbacks { override val onInAppClick = OnInAppClick { inAppInteractor.sendInAppClicked(inAppMessage.inAppId) } override val onInAppShown = OnInAppShown { - inAppInteractor.saveShownInApp(inAppMessage.inAppId, System.currentTimeMillis()) + handleInAppShown(renderStartTimeMs, preparedTimeMs, inAppMessage, tags) } override val onInAppDismiss = OnInAppDismiss { inAppInteractor.saveInAppDismissTime() @@ -194,6 +202,18 @@ internal class InAppMessageManagerImpl( } } + private fun handleInAppShown( + renderStartTimeMs: Long, + preparedTimeMs: Long, + inAppMessage: InAppType, + tags: Map? + ) { + val shownTime = timeProvider.currentTimeMillis() + mindboxLogI("Render time is ${shownTime - renderStartTimeMs}ms, prepared time is ${preparedTimeMs}ms") + val timeToDisplay = (preparedTimeMs + (shownTime - renderStartTimeMs)).millisToTimeSpan() + inAppInteractor.saveShownInApp(inAppMessage.inAppId, shownTime, timeToDisplay, tags) + } + companion object { const val CONFIG_NOT_FOUND = 404 } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayer.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayer.kt index 73f99d201..ab5637284 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayer.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayer.kt @@ -14,7 +14,8 @@ internal interface InAppMessageViewDisplayer { fun tryShowInAppMessage( inAppType: InAppType, - inAppActionCallbacks: InAppActionCallbacks + inAppActionCallbacks: InAppActionCallbacks, + onRenderStart: () -> Unit = {}, ) fun registerCurrentActivity(activity: Activity) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt index 1d6c50d45..97101257f 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageViewDisplayerImpl.kt @@ -127,9 +127,10 @@ internal class InAppMessageViewDisplayerImpl( override fun tryShowInAppMessage( inAppType: InAppType, - inAppActionCallbacks: InAppActionCallbacks + inAppActionCallbacks: InAppActionCallbacks, + onRenderStart: () -> Unit, ) { - val wrapper = InAppTypeWrapper(inAppType, inAppActionCallbacks) + val wrapper = InAppTypeWrapper(inAppType, inAppActionCallbacks, onRenderStart) if (isUiPresent() && currentHolder == null && pausedHolder == null) { val duration = Stopwatch.track(Stopwatch.INIT_SDK) @@ -156,7 +157,10 @@ internal class InAppMessageViewDisplayerImpl( wrapper: InAppTypeWrapper, isRestored: Boolean = false, ) { - if (!isRestored) isActionExecuted = false + if (!isRestored) { + wrapper.onRenderStart() + isActionExecuted = false + } if (isRestored && tryReattachRestoredInApp(wrapper.inAppType.inAppId)) return val callbackWrapper = InAppCallbackWrapper(inAppCallback) { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt index 7992f238c..0d9733fef 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/view/WebViewInappViewHolder.kt @@ -264,6 +264,7 @@ internal class WebViewInAppViewHolder( failureReason = FailureReason.WEBVIEW_PRESENTATION_FAILED, errorDescription = "WebView error: code=${error.code}, description=${error.description}, url=${error.url}" ) + release() } } }) @@ -423,6 +424,7 @@ internal class WebViewInAppViewHolder( failureReason = FailureReason.WEBVIEW_LOAD_FAILED, errorDescription = "WebView content URL is null" ) + hide() } } } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/request/InAppHandleRequest.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/request/InAppHandleRequest.kt index dfd6ddb0a..b8256c07a 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/request/InAppHandleRequest.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/request/InAppHandleRequest.kt @@ -6,3 +6,12 @@ internal data class InAppHandleRequest( @SerializedName("inappId") val inAppId: String ) + +internal data class InAppShowRequest( + @SerializedName("inappId") + val inAppId: String, + @SerializedName("timeToDisplay") + val timeToDisplay: String, + @SerializedName("tags") + val tags: Map? +) diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt index f9be86f54..f42155dfd 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/models/operation/response/InAppConfigResponse.kt @@ -12,6 +12,7 @@ import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.SlidingExpirationDt import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.InappSettingsDtoBlankDeserializer import com.google.gson.annotations.JsonAdapter import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.InAppDelayTimeDeserializer +import cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers.InAppTagsDeserializer internal data class InAppConfigResponse( @SerializedName("inapps") @@ -130,6 +131,8 @@ internal data class InAppDto( val targeting: TreeTargetingDto?, @SerializedName("form") val form: FormDto?, + @SerializedName(InAppTagsDeserializer.TAGS) + val tags: Map?, ) internal sealed class FrequencyDto { @@ -223,5 +226,8 @@ internal data class InAppConfigResponseBlank( // FormDto. Parsed after filtering inApp versions. @SerializedName("form") val form: JsonObject?, + @SerializedName("tags") + @JsonAdapter(InAppTagsDeserializer::class) + val tags: Map?, ) } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt new file mode 100644 index 000000000..dfdb715dd --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt @@ -0,0 +1,102 @@ +package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class InAppTagsDeserializerTest { + + private lateinit var gson: Gson + + @Before + fun setUp() { + val mapType = object : TypeToken?>() {}.type + gson = GsonBuilder() + .registerTypeAdapter(mapType, InAppTagsDeserializer()) + .create() + } + + private fun deserialize(json: String): Map? { + val mapType = object : TypeToken?>() {}.type + return gson.fromJson(json, mapType) + } + + @Test + fun `deserialize returns string values as is`() { + val inputJson = """{"layer": "webView", "type": "onboarding"}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView", "type" to "onboarding"), actualResult) + } + + @Test + fun `deserialize skips number values`() { + val inputJson = """{"layer": "webView", "count": 42}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView"), actualResult) + } + + @Test + fun `deserialize skips boolean values`() { + val inputJson = """{"layer": "webView", "isActive": true}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView"), actualResult) + } + + @Test + fun `deserialize skips object values`() { + val inputJson = """{"layer": "webView", "nested": {"key": "value"}}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView"), actualResult) + } + + @Test + fun `deserialize skips null values`() { + val inputJson = """{"layer": "webView", "nullKey": null}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView"), actualResult) + } + + @Test + fun `deserialize returns null when json is null`() { + val actualResult = deserialize("null") + assertNull(actualResult) + } + + @Test + fun `deserialize returns null when json is not an object`() { + val actualResult = deserialize("""["item1", "item2"]""") + assertNull(actualResult) + } + + @Test + fun `deserialize returns empty map when all values are non-string`() { + val inputJson = """{"count": 42, "flag": true, "nested": {}}""" + val actualResult = deserialize(inputJson) + assertTrue(actualResult?.isEmpty() == true) + } + + @Test + fun `deserialize returns empty map for empty object`() { + val actualResult = deserialize("{}") + assertTrue(actualResult?.isEmpty() == true) + } + + @Test + fun `deserialize preserves empty string values`() { + val inputJson = """{"layer": "", "type": "onboarding"}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "", "type" to "onboarding"), actualResult) + } + + @Test + fun `deserialize skips array values`() { + val inputJson = """{"layer": "webView", "items": [1, 2, 3]}""" + val actualResult = deserialize(inputJson) + assertEquals(mapOf("layer" to "webView"), actualResult) + } +} diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerTest.kt index 9de8b9487..71e300061 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerTest.kt @@ -3,12 +3,14 @@ package cloud.mindbox.mobile_sdk.inapp.data.managers import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.managers.InAppSerializationManager import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppFailuresWrapper import cloud.mindbox.mobile_sdk.models.operation.request.FailureReason +import cloud.mindbox.mobile_sdk.models.operation.request.InAppShowRequest import cloud.mindbox.mobile_sdk.models.operation.request.InAppShowFailure import com.google.gson.Gson import com.google.gson.reflect.TypeToken import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test @@ -69,22 +71,49 @@ internal class InAppSerializationManagerTest { } @Test - fun `serialize to inApp handled string success`() { + fun `serializeToInAppActionString returns JSON with inAppId only`() { val expectedResult = "{\"inappId\":\"${inAppId}\"}" - val actualResult = inAppSerializationManager.serializeToInAppHandledString(inAppId) + val actualResult = inAppSerializationManager.serializeToInAppActionString(inAppId) assertEquals(expectedResult, actualResult) } @Test - fun `serialize to inApp handled string error`() { + fun `serializeToInAppActionString returns empty string on error`() { val gson: Gson = mockk() - every { - gson.toJson(any()) - } throws Error("errorMessage") + every { gson.toJson(any(), any>()) } throws Error("errorMessage") inAppSerializationManager = InAppSerializationManagerImpl(gson) - val expectedResult = "" - val actualResult = inAppSerializationManager.serializeToInAppHandledString(inAppId) - assertEquals(expectedResult, actualResult) + val actualResult = inAppSerializationManager.serializeToInAppActionString(inAppId) + assertEquals("", actualResult) + } + + @Test + fun `serializeToInAppShownString returns JSON with inAppId timeToDisplay and tags`() { + val timeToDisplay = "0:00:00:00.2250000" + val tags = mapOf("layer" to "webView", "type" to "onboarding") + val actualResult = inAppSerializationManager.serializeToInAppShownActionString(inAppId, timeToDisplay, tags) + val parsed = gson.fromJson(actualResult, InAppShowRequest::class.java) + assertEquals(inAppId, parsed.inAppId) + assertEquals(timeToDisplay, parsed.timeToDisplay) + assertEquals(tags, parsed.tags) + } + + @Test + fun `serializeToInAppShownString omits tags when null`() { + val timeToDisplay = "0:00:00:00.2250000" + val actualResult = inAppSerializationManager.serializeToInAppShownActionString(inAppId, timeToDisplay, null) + val parsed = gson.fromJson(actualResult, InAppShowRequest::class.java) + assertEquals(inAppId, parsed.inAppId) + assertEquals(timeToDisplay, parsed.timeToDisplay) + assertNull(parsed.tags) + } + + @Test + fun `serializeToInAppShownString returns empty string on error`() { + val gson: Gson = mockk() + every { gson.toJson(any(), any>()) } throws Error("errorMessage") + inAppSerializationManager = InAppSerializationManagerImpl(gson) + val actualResult = inAppSerializationManager.serializeToInAppShownActionString(inAppId, "0:00:00:00.2250000", null) + assertEquals("", actualResult) } @Test diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/MobileConfigSerializationManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/MobileConfigSerializationManagerTest.kt index f0dc3fb56..72bf9fc91 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/MobileConfigSerializationManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/managers/serialization/MobileConfigSerializationManagerTest.kt @@ -662,4 +662,150 @@ internal class MobileConfigSerializationManagerTest { }) })) } + + @Test + fun `deserialize to config dto blank with tags success`() { + val successJson = gson.toJson(JsonObject().apply { + add("inapps", JsonArray().apply { + add(JsonObject().apply { + addProperty("id", "040810aa-d135-49f4-8916-7e68dcc61c71") + add("sdkVersion", JsonObject().apply { + addProperty("min", 1) + add("max", com.google.gson.JsonNull.INSTANCE) + }) + add("targeting", JsonObject().apply { + addProperty("${"$"}type", "true") + }) + add("tags", JsonObject().apply { + addProperty("layer", "webView") + addProperty("type", "onboarding") + }) + add("form", JsonObject().apply { + add("variants", JsonArray()) + }) + }) + }) + }) + val expectedResult = InAppConfigStub.getConfigResponseBlank().copy( + inApps = listOf( + InAppStub.getInAppDtoBlank().copy( + id = "040810aa-d135-49f4-8916-7e68dcc61c71", + sdkVersion = InAppStub.getSdkVersion().copy(minVersion = 1, maxVersion = null), + targeting = JsonObject().apply { addProperty("${'$'}type", "true") }, + form = JsonObject().apply { add("variants", JsonArray()) }, + tags = mapOf("layer" to "webView", "type" to "onboarding") + ) + ) + ) + val actualResult = mobileConfigSerializationManager.deserializeToConfigDtoBlank(successJson) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `deserialize to config dto blank without tags field success`() { + val successJson = gson.toJson(JsonObject().apply { + add("inapps", JsonArray().apply { + add(JsonObject().apply { + addProperty("id", "040810aa-d135-49f4-8916-7e68dcc61c71") + add("sdkVersion", JsonObject().apply { + addProperty("min", 1) + add("max", com.google.gson.JsonNull.INSTANCE) + }) + add("targeting", JsonObject().apply { + addProperty("${"$"}type", "true") + }) + add("form", JsonObject().apply { + add("variants", JsonArray()) + }) + }) + }) + }) + val expectedResult = InAppConfigStub.getConfigResponseBlank().copy( + inApps = listOf( + InAppStub.getInAppDtoBlank().copy( + id = "040810aa-d135-49f4-8916-7e68dcc61c71", + sdkVersion = InAppStub.getSdkVersion().copy(minVersion = 1, maxVersion = null), + targeting = JsonObject().apply { addProperty("${'$'}type", "true") }, + form = JsonObject().apply { add("variants", JsonArray()) }, + tags = null + ) + ) + ) + val actualResult = mobileConfigSerializationManager.deserializeToConfigDtoBlank(successJson) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `deserialize to config dto blank with empty tags success`() { + val successJson = gson.toJson(JsonObject().apply { + add("inapps", JsonArray().apply { + add(JsonObject().apply { + addProperty("id", "040810aa-d135-49f4-8916-7e68dcc61c71") + add("sdkVersion", JsonObject().apply { + addProperty("min", 1) + add("max", com.google.gson.JsonNull.INSTANCE) + }) + add("targeting", JsonObject().apply { + addProperty("${"$"}type", "true") + }) + add("tags", JsonObject()) + add("form", JsonObject().apply { + add("variants", JsonArray()) + }) + }) + }) + }) + val expectedResult = InAppConfigStub.getConfigResponseBlank().copy( + inApps = listOf( + InAppStub.getInAppDtoBlank().copy( + id = "040810aa-d135-49f4-8916-7e68dcc61c71", + sdkVersion = InAppStub.getSdkVersion().copy(minVersion = 1, maxVersion = null), + targeting = JsonObject().apply { addProperty("${'$'}type", "true") }, + form = JsonObject().apply { add("variants", JsonArray()) }, + tags = emptyMap() + ) + ) + ) + val actualResult = mobileConfigSerializationManager.deserializeToConfigDtoBlank(successJson) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `deserialize to config dto blank with non-string tag values skips them success`() { + val successJson = gson.toJson(JsonObject().apply { + add("inapps", JsonArray().apply { + add(JsonObject().apply { + addProperty("id", "040810aa-d135-49f4-8916-7e68dcc61c71") + add("sdkVersion", JsonObject().apply { + addProperty("min", 1) + add("max", com.google.gson.JsonNull.INSTANCE) + }) + add("targeting", JsonObject().apply { + addProperty("${"$"}type", "true") + }) + add("tags", JsonObject().apply { + addProperty("layer", "webView") + addProperty("count", 42) + addProperty("flag", true) + }) + add("form", JsonObject().apply { + add("variants", JsonArray()) + }) + }) + }) + }) + val expectedResult = InAppConfigStub.getConfigResponseBlank().copy( + inApps = listOf( + InAppStub.getInAppDtoBlank().copy( + id = "040810aa-d135-49f4-8916-7e68dcc61c71", + sdkVersion = InAppStub.getSdkVersion().copy(minVersion = 1, maxVersion = null), + targeting = JsonObject().apply { addProperty("${'$'}type", "true") }, + form = JsonObject().apply { add("variants", JsonArray()) }, + tags = mapOf("layer" to "webView") + ) + ) + ) + val actualResult = mobileConfigSerializationManager.deserializeToConfigDtoBlank(successJson) + assertEquals(expectedResult, actualResult) + } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapperTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapperTest.kt index b6a1c72ae..f482fdbfe 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapperTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/mapper/InAppMapperTest.kt @@ -54,6 +54,7 @@ class InAppMapperTest { ), ), form = null, + tags = null, ) ), monitoring = null, @@ -105,6 +106,7 @@ class InAppMapperTest { ), ), form = null, + tags = null, ) ), monitoring = null, @@ -140,6 +142,7 @@ class InAppMapperTest { ), ), form = null, + tags = null, ) ), monitoring = null, @@ -175,6 +178,7 @@ class InAppMapperTest { ), ), form = null, + tags = null, ) ), monitoring = null, @@ -210,6 +214,7 @@ class InAppMapperTest { ), ), form = null, + tags = null, ) ), monitoring = null, @@ -252,7 +257,8 @@ class InAppMapperTest { ), sdkVersion = null, targeting = TreeTargetingDto.TrueNodeDto(type = ""), - form = FormDto(variants = listOf(modalWindowDto)) + form = FormDto(variants = listOf(modalWindowDto)), + tags = null, ) ), monitoring = null, @@ -298,7 +304,8 @@ class InAppMapperTest { ), sdkVersion = null, targeting = TreeTargetingDto.TrueNodeDto(type = ""), - form = FormDto(variants = listOf(modalWindowDto)) + form = FormDto(variants = listOf(modalWindowDto)), + tags = null, ) ), monitoring = null, @@ -314,4 +321,109 @@ class InAppMapperTest { assertEquals(1, modalWindow.layers.size) assertTrue(modalWindow.layers.first() is Layer.ImageLayer) } + + @Test + fun `mapToInAppConfig maps tags from InAppDto to InApp`() { + val mapper = InAppMapper() + val inputTags = mapOf("layer" to "webView", "type" to "onboarding") + val result = mapper.mapToInAppConfig( + InAppConfigResponse( + inApps = listOf( + InAppDto( + id = "test-id", + isPriority = false, + delayTime = null, + frequency = FrequencyDto.FrequencyOnceDto(type = "once", kind = "lifetime"), + sdkVersion = null, + targeting = TreeTargetingDto.TrueNodeDto(type = ""), + form = null, + tags = inputTags, + ) + ), + monitoring = null, + abtests = null, + settings = null, + ) + ) + assertEquals(inputTags, result.inApps.first().tags) + } + + @Test + fun `mapToInAppConfig maps null tags to null in InApp`() { + val mapper = InAppMapper() + val result = mapper.mapToInAppConfig( + InAppConfigResponse( + inApps = listOf( + InAppDto( + id = "test-id", + isPriority = false, + delayTime = null, + frequency = FrequencyDto.FrequencyOnceDto(type = "once", kind = "lifetime"), + sdkVersion = null, + targeting = TreeTargetingDto.TrueNodeDto(type = ""), + form = null, + tags = null, + ) + ), + monitoring = null, + abtests = null, + settings = null, + ) + ) + assertNull(result.inApps.first().tags) + } + + @Test + fun `mapToInAppConfig maps empty tags map to null in InApp`() { + val mapper = InAppMapper() + val result = mapper.mapToInAppConfig( + InAppConfigResponse( + inApps = listOf( + InAppDto( + id = "test-id", + isPriority = false, + delayTime = null, + frequency = FrequencyDto.FrequencyOnceDto(type = "once", kind = "lifetime"), + sdkVersion = null, + targeting = TreeTargetingDto.TrueNodeDto(type = ""), + form = null, + tags = emptyMap(), + ) + ), + monitoring = null, + abtests = null, + settings = null, + ) + ) + assertNull(result.inApps.first().tags) + } + + @Test + fun `mapToInAppDto maps tags from InAppDtoBlank to InAppDto`() { + val mapper = InAppMapper() + val inputTags = mapOf("layer" to "webView", "type" to "onboarding") + val inputDtoBlank = InAppStub.getInAppDtoBlank().copy(tags = inputTags) + val result = mapper.mapToInAppDto( + inAppDtoBlank = inputDtoBlank, + delayTime = null, + formDto = null, + frequencyDto = FrequencyDto.FrequencyOnceDto(type = "once", kind = "lifetime"), + targetingDto = null, + ) + assertEquals(inputTags, result.tags) + } + + @Test + fun `mapToInAppDto maps null tags from InAppDtoBlank to InAppDto`() { + val mapper = InAppMapper() + val inputDtoBlank = InAppStub.getInAppDtoBlank().copy(tags = null) + val result = mapper.mapToInAppDto( + inAppDtoBlank = inputDtoBlank, + delayTime = null, + formDto = null, + frequencyDto = FrequencyDto.FrequencyOnceDto(type = "once", kind = "lifetime"), + targetingDto = null, + ) + assertNull(result.tags) + } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryTest.kt index b3758017a..df681930d 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/repositories/InAppRepositoryTest.kt @@ -126,8 +126,8 @@ class InAppRepositoryTest { fun `send in app shown success`() { val testInAppId = "testInAppId" val serializedString = "serializedString" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString - inAppRepository.sendInAppShown(testInAppId) + every { inAppSerializationManager.serializeToInAppShownActionString(any(), any(), any()) } returns serializedString + inAppRepository.sendInAppShown(testInAppId, "0:00:00:00.2250000", null) verify(exactly = 1) { MindboxEventManager.inAppShown(context, serializedString) } @@ -137,8 +137,8 @@ class InAppRepositoryTest { fun `send in app shown empty string`() { val testInAppId = "testInAppId" val serializedString = "" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString - inAppRepository.sendInAppShown(testInAppId) + every { inAppSerializationManager.serializeToInAppShownActionString(any(), any(), any()) } returns serializedString + inAppRepository.sendInAppShown(testInAppId, "0:00:00:00.2250000", null) verify(exactly = 0) { MindboxEventManager.inAppShown(context, serializedString) } @@ -148,7 +148,7 @@ class InAppRepositoryTest { fun `send in app clicked success`() { val testInAppId = "testInAppId" val serializedString = "serializedString" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString + every { inAppSerializationManager.serializeToInAppActionString(any()) } returns serializedString inAppRepository.sendInAppClicked(testInAppId) verify(exactly = 1) { MindboxEventManager.inAppClicked(context, serializedString) @@ -159,7 +159,7 @@ class InAppRepositoryTest { fun `send in app clicked empty string`() { val testInAppId = "testInAppId" val serializedString = "" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString + every { inAppSerializationManager.serializeToInAppActionString(any()) } returns serializedString inAppRepository.sendInAppClicked(testInAppId) verify(exactly = 0) { MindboxEventManager.inAppClicked(context, serializedString) @@ -170,10 +170,10 @@ class InAppRepositoryTest { fun `send user targeted success`() { val testInAppId = "testInAppId" val serializedString = "serializedString" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString - inAppRepository.sendInAppClicked(testInAppId) + every { inAppSerializationManager.serializeToInAppActionString(any()) } returns serializedString + inAppRepository.sendUserTargeted(testInAppId) verify(exactly = 1) { - MindboxEventManager.inAppClicked(context, serializedString) + MindboxEventManager.sendUserTargeted(context, serializedString) } } @@ -181,10 +181,10 @@ class InAppRepositoryTest { fun `send user targeted string`() { val testInAppId = "testInAppId" val serializedString = "" - every { inAppSerializationManager.serializeToInAppHandledString(any()) } returns serializedString - inAppRepository.sendInAppClicked(testInAppId) + every { inAppSerializationManager.serializeToInAppActionString(any()) } returns serializedString + inAppRepository.sendUserTargeted(testInAppId) verify(exactly = 0) { - MindboxEventManager.inAppClicked(context, serializedString) + MindboxEventManager.sendUserTargeted(context, serializedString) } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImplTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImplTest.kt index fc7adfba9..6f7dd52de 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImplTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImplTest.kt @@ -68,7 +68,7 @@ class InAppInteractorImplTest { @MockK private lateinit var minIntervalBetweenShowsLimitChecker: Checker - @MockK + @RelaxedMockK private lateinit var timeProvider: TimeProvider @RelaxedMockK @@ -122,7 +122,7 @@ class InAppInteractorImplTest { isPriority = false, targeting = InAppStub.getTargetingTrueNode().copy("true"), form = InAppStub.getInApp().form.copy( - listOf( + variants = listOf( InAppStub.getModalWindow().copy( inAppId = "nonPriorityInapp1" ) @@ -134,7 +134,7 @@ class InAppInteractorImplTest { isPriority = true, targeting = InAppStub.getTargetingTrueNode().copy("true"), form = InAppStub.getInApp().form.copy( - listOf( + variants = listOf( InAppStub.getModalWindow().copy( inAppId = "priorityInapp" ) @@ -147,7 +147,7 @@ class InAppInteractorImplTest { isPriority = true, targeting = InAppStub.getTargetingTrueNode().copy("true"), form = InAppStub.getInApp().form.copy( - listOf( + variants = listOf( InAppStub.getModalWindow().copy( inAppId = "priorityInapp2" ) @@ -159,7 +159,7 @@ class InAppInteractorImplTest { isPriority = false, targeting = InAppStub.getTargetingTrueNode().copy("true"), form = InAppStub.getInApp().form.copy( - listOf( + variants = listOf( InAppStub.getModalWindow().copy( inAppId = "nonPriorityInApp2" ) @@ -213,19 +213,19 @@ class InAppInteractorImplTest { interactor.processEventAndConfig().test { eventFlow.emit(InAppEventType.AppStartup) val firstItem = awaitItem() - assertEquals(priorityInApp, firstItem) + assertEquals(priorityInApp, firstItem.first) eventFlow.emit(InAppEventType.AppStartup) val secondItem = awaitItem() - assertEquals(priorityInAppTwo, secondItem) + assertEquals(priorityInAppTwo, secondItem.first) eventFlow.emit(InAppEventType.AppStartup) val thirdItem = awaitItem() - assertEquals(nonPriorityInApp, thirdItem) + assertEquals(nonPriorityInApp, thirdItem.first) eventFlow.emit(InAppEventType.AppStartup) val fourthItem = awaitItem() - assertEquals(nonPriorityInAppTwo, fourthItem) + assertEquals(nonPriorityInAppTwo, fourthItem.first) cancelAndIgnoreRemainingEvents() } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt index c4b35a9bf..7525ff28f 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt @@ -25,10 +25,10 @@ class InAppMessageDelayedManagerTest { val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } - inAppMessageDelayedManager.process(inApp) + inAppMessageDelayedManager.process(inApp, 0L) inAppMessageDelayedManager.inAppToShowFlow.test { advanceTimeBy(10_001) - assertEquals(inApp, awaitItem()) + assertEquals(inApp, awaitItem().first) cancelAndIgnoreRemainingEvents() } } @@ -38,13 +38,13 @@ class InAppMessageDelayedManagerTest { every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) - inAppMessageDelayedManager.process(inApp) + inAppMessageDelayedManager.process(inApp, 0L) inAppMessageDelayedManager.inAppToShowFlow.test { advanceTimeBy(9_999) expectNoEvents() advanceTimeBy(1) - assertEquals(inApp, awaitItem()) + assertEquals(inApp, awaitItem().first) cancelAndIgnoreRemainingEvents() } } @@ -53,7 +53,7 @@ class InAppMessageDelayedManagerTest { fun `clearSession should cancel pending jobs and clear queue`() = runTest(testDispatcher.scheduler) { every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) - inAppMessageDelayedManager.process(inApp) + inAppMessageDelayedManager.process(inApp, 0L) inAppMessageDelayedManager.clearSession() inAppMessageDelayedManager.inAppToShowFlow.test { testDispatcher.scheduler.advanceUntilIdle() @@ -67,11 +67,11 @@ class InAppMessageDelayedManagerTest { val inAppOne = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(10000)) val inAppTwo = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppOne) - inAppMessageDelayedManager.process(inAppTwo) + inAppMessageDelayedManager.process(inAppOne, 0L) + inAppMessageDelayedManager.process(inAppTwo, 0L) inAppMessageDelayedManager.inAppToShowFlow.test { - assertEquals(inAppTwo, awaitItem()) + assertEquals(inAppTwo, awaitItem().first) cancelAndIgnoreRemainingEvents() } } @@ -82,11 +82,11 @@ class InAppMessageDelayedManagerTest { val inAppNonPriority = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(5000)) val inAppPriority = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppNonPriority) - inAppMessageDelayedManager.process(inAppPriority) + inAppMessageDelayedManager.process(inAppNonPriority, 0L) + inAppMessageDelayedManager.process(inAppPriority, 0L) inAppMessageDelayedManager.inAppToShowFlow.test { - assertEquals(inAppPriority, awaitItem()) + assertEquals(inAppPriority, awaitItem().first) cancelAndIgnoreRemainingEvents() } } @@ -97,11 +97,11 @@ class InAppMessageDelayedManagerTest { val inAppFirst = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(5000), isPriority = true) val inAppSecond = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppFirst) - inAppMessageDelayedManager.process(inAppSecond) + inAppMessageDelayedManager.process(inAppFirst, 0L) + inAppMessageDelayedManager.process(inAppSecond, 0L) inAppMessageDelayedManager.inAppToShowFlow.test { - assertEquals(inAppFirst, awaitItem()) + assertEquals(inAppFirst, awaitItem().first) cancelAndIgnoreRemainingEvents() } } @@ -113,12 +113,12 @@ class InAppMessageDelayedManagerTest { val inAppLoser1 = InAppStub.getInApp().copy(id = "loser1", delayTime = Milliseconds(5000), isPriority = false) val inAppLoser2 = InAppStub.getInApp().copy(id = "loser2", delayTime = Milliseconds(3000), isPriority = false) - inAppMessageDelayedManager.process(inAppWinner) - inAppMessageDelayedManager.process(inAppLoser1) - inAppMessageDelayedManager.process(inAppLoser2) + inAppMessageDelayedManager.process(inAppWinner, 0L) + inAppMessageDelayedManager.process(inAppLoser1, 0L) + inAppMessageDelayedManager.process(inAppLoser2, 0L) advanceTimeBy(5000) inAppMessageDelayedManager.inAppToShowFlow.test { - assertEquals(inAppWinner, awaitItem()) + assertEquals(inAppWinner, awaitItem().first) expectNoEvents() } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt index 5d784fbeb..335b26276 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt @@ -11,6 +11,7 @@ import cloud.mindbox.mobile_sdk.monitoring.domain.interfaces.MonitoringInteracto import cloud.mindbox.mobile_sdk.repository.MindboxPreferences import cloud.mindbox.mobile_sdk.sortByPriority import cloud.mindbox.mobile_sdk.utils.LoggingExceptionHandler +import cloud.mindbox.mobile_sdk.utils.SystemTimeProvider import cloud.mindbox.mobile_sdk.utils.mockLogger import cloud.mindbox.mobile_sdk.utils.mockPreferencesConfigSetter import com.android.volley.NetworkResponse @@ -55,6 +56,8 @@ internal class InAppMessageManagerTest { private val testDispatcher = StandardTestDispatcher() + private val timeProvider = mockk() + /** * sets a thread to be used as main dispatcher for running on JVM * **/ @@ -90,7 +93,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) coEvery { inAppMessageInteractor.fetchMobileConfig() @@ -112,7 +116,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) mockkObject(LoggingExceptionHandler) every { MindboxPreferences.inAppConfig } returns "test" @@ -132,14 +137,14 @@ internal class InAppMessageManagerTest { @Test fun `in app messages success message shown`() = runTest { - val inAppToShowFlow = MutableSharedFlow() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageViewDisplayer.isInAppActive() } returns false every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns true every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow - every { inAppMessageDelayedManager.process(inApp) } coAnswers { + every { inAppMessageDelayedManager.process(inApp, any()) } coAnswers { this@runTest.launch { - inAppToShowFlow.emit(inApp) + inAppToShowFlow.emit(inApp to 0L) } } @@ -150,28 +155,27 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) coEvery { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit( - inApp - ) + emit(inApp to 0L) } } inAppMessageManager.listenEventAndInApp() advanceUntilIdle() - verify(exactly = 1) { inAppMessageDelayedManager.process(inApp) } - verify(exactly = 1) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any()) } + verify(exactly = 1) { inAppMessageDelayedManager.process(inApp, any()) } + verify(exactly = 1) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any(), any()) } } @Test fun `in app messages success message not shown when inApp already active`() = runTest { - val inAppToShowFlow = MutableSharedFlow() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns true every { inAppMessageViewDisplayer.isInAppActive() } returns true @@ -182,7 +186,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) coEvery { inAppMessageInteractor.listenToTargetingEvents() @@ -191,28 +196,26 @@ internal class InAppMessageManagerTest { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit( - inApp - ) + emit(inApp to 0L) } } every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow - every { inAppMessageDelayedManager.process(inApp) } answers { + every { inAppMessageDelayedManager.process(inApp, any()) } answers { this@runTest.launch { - inAppToShowFlow.emit(inApp) + inAppToShowFlow.emit(inApp to 0L) } } inAppMessageManager.listenEventAndInApp() advanceUntilIdle() - verify(exactly = 1) { inAppMessageDelayedManager.process(inApp) } + verify(exactly = 1) { inAppMessageDelayedManager.process(inApp, any()) } coVerify(exactly = 1) { inAppMessageInteractor.listenToTargetingEvents() } - verify(exactly = 0) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any()) } + verify(exactly = 0) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any(), any()) } } @Test fun `in app messages success message not shown when inApp frequency or limits not allowed`() = runTest { - val inAppToShowFlow = MutableSharedFlow() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns false every { inAppMessageViewDisplayer.isInAppActive() } returns false @@ -223,7 +226,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) coEvery { inAppMessageInteractor.listenToTargetingEvents() @@ -232,23 +236,21 @@ internal class InAppMessageManagerTest { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit( - inApp - ) + emit(inApp to 0L) } } every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow - every { inAppMessageDelayedManager.process(inApp) } answers { + every { inAppMessageDelayedManager.process(inApp, any()) } answers { this@runTest.launch { - inAppToShowFlow.emit(inApp) + inAppToShowFlow.emit(inApp to 0L) } } inAppMessageManager.listenEventAndInApp() advanceUntilIdle() - verify(exactly = 1) { inAppMessageDelayedManager.process(inApp) } + verify(exactly = 1) { inAppMessageDelayedManager.process(inApp, any()) } coVerify(exactly = 1) { inAppMessageInteractor.listenToTargetingEvents() } - verify(exactly = 0) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any()) } + verify(exactly = 0) { inAppMessageViewDisplayer.tryShowInAppMessage(inApp.form.variants.first(), any(), any()) } } @Test @@ -260,7 +262,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) coEvery { inAppMessageInteractor.processEventAndConfig() @@ -294,7 +297,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) mockkConstructor(NetworkResponse::class) val networkResponse = mockk() @@ -328,7 +332,8 @@ internal class InAppMessageManagerTest { monitoringRepository, sessionStorageManager, userVisitManager, - inAppMessageDelayedManager + inAppMessageDelayedManager, + timeProvider ) mockkConstructor(NetworkResponse::class) val networkResponse = mockk() diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/InAppStub.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/InAppStub.kt index 066ac53b6..7d374bb6c 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/InAppStub.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/models/InAppStub.kt @@ -22,7 +22,8 @@ internal class InAppStub { ), form = Form(variants = listOf(getModalWindow())), isPriority = false, - delayTime = null + delayTime = null, + tags = null, ) fun getInAppDto(): InAppDto = InAppDto( @@ -32,7 +33,8 @@ internal class InAppStub { targeting = (TreeTargetingDto.TrueNodeDto("")), form = FormDto(variants = listOf(getModalWindowDto())), isPriority = false, - delayTime = null + delayTime = null, + tags = null, ) fun getFrequencyOnceDto(): FrequencyDto.FrequencyOnceDto = FrequencyDto.FrequencyOnceDto( @@ -157,7 +159,8 @@ internal class InAppStub { sdkVersion = null, targeting = null, frequency = null, - form = null + form = null, + tags = null, ) } diff --git a/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json b/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json index 4234fccae..045b13cac 100644 --- a/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json +++ b/sdk/src/test/resources/ConfigParsing/ConfigWithSettingsABTestsMonitoringInapps.json @@ -20,6 +20,10 @@ ], "$type": "and" }, + "tags": { + "layer": "webView", + "type": "modal" + }, "form": { "variants": [ { From 3f78c15de874d39878b965b3427c52e9559cb7cc Mon Sep 17 00:00:00 2001 From: sozinov Date: Mon, 2 Mar 2026 15:05:42 +0300 Subject: [PATCH 2/2] MOBILEWEBVIEW-60: follow codereview --- .../managers/InAppSerializationManagerImpl.kt | 14 ++-- .../inapp/domain/InAppInteractorImpl.kt | 13 ++-- .../interfaces/interactors/InAppInteractor.kt | 3 +- .../InAppMessageDelayedManager.kt | 7 +- .../presentation/InAppMessageManagerImpl.kt | 21 +++--- .../mindbox/mobile_sdk/utils/TimeProvider.kt | 5 ++ .../InAppTagsDeserializerTest.kt | 7 +- .../InAppMessageDelayedManagerTest.kt | 24 +++--- .../presentation/InAppMessageManagerTest.kt | 19 ++--- .../MobileConfigSettingsManagerTest.kt | 17 ++--- .../mobile_sdk/utils/TimeProviderTest.kt | 73 +++++++++++++++++++ 11 files changed, 139 insertions(+), 64 deletions(-) create mode 100644 sdk/src/test/java/cloud/mindbox/mobile_sdk/utils/TimeProviderTest.kt diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt index dc5400e2a..d80cff2c1 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/data/managers/InAppSerializationManagerImpl.kt @@ -19,24 +19,20 @@ internal class InAppSerializationManagerImpl(private val gson: Gson) : InAppSeri timeToDisplay: String, tags: Map?, ): String { - return LoggingExceptionHandler.runCatching("") { - gson.toJson( + return loggingRunCatching("") { + gson.toJsonTyped( InAppShowRequest( inAppId = inAppId, timeToDisplay = timeToDisplay, tags = tags, - ), - InAppShowRequest::class.java, + ) ) } } override fun serializeToInAppActionString(inAppId: String): String { - return LoggingExceptionHandler.runCatching("") { - gson.toJson( - InAppHandleRequest(inAppId = inAppId), - InAppHandleRequest::class.java, - ) + return loggingRunCatching("") { + gson.toJsonTyped(InAppHandleRequest(inAppId = inAppId)) } } diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt index fd20beb41..758e8613b 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/InAppInteractorImpl.kt @@ -13,6 +13,7 @@ import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.InAppReposi import cloud.mindbox.mobile_sdk.inapp.domain.interfaces.repositories.MobileConfigRepository import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp import cloud.mindbox.mobile_sdk.logger.MindboxLog +import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.logger.mindboxLogD import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.models.InAppEventType @@ -40,7 +41,7 @@ internal class InAppInteractorImpl( private val inAppTargetingChannel = Channel(Channel.UNLIMITED) - override suspend fun processEventAndConfig(): Flow> { + override suspend fun processEventAndConfig(): Flow> { val inApps: List = mobileConfigRepository.getInAppsSection() .let { inApps -> inAppRepository.saveCurrentSessionInApps(inApps) @@ -63,10 +64,10 @@ internal class InAppInteractorImpl( } return inAppRepository.listenInAppEvents() .filter { event -> inAppEventManager.isValidInAppEvent(event) } - .onEach { - mindboxLogD("Event triggered: ${it.name}") + .onEach { event -> + mindboxLogD("Event triggered: ${event.name}") }.map { event -> - val triggerTimeMillis = timeProvider.currentTimeMillis() + val triggerTimeMillis = timeProvider.currentTimestamp() val filteredInApps = inAppFilteringManager.filterUnShownInAppsByEvent(inApps, event).let { inAppFrequencyManager.filterInAppsFrequency(it) } @@ -84,10 +85,10 @@ internal class InAppInteractorImpl( inApp?.let { sessionStorageManager.inAppTriggerEvent = event } - inApp?.let { it to (timeProvider.currentTimeMillis() - triggerTimeMillis) } + inApp?.let { inapp -> inapp to timeProvider.elapsedSince(triggerTimeMillis) } } .onEach { pair -> - pair?.let { (inApp, preparedTime) -> mindboxLogI("InApp ${inApp.id} isPriority=${inApp.isPriority}, delayTime=${inApp.delayTime}, skipLimitChecks=${inApp.isPriority}, preparedTime = $preparedTime ms") } + pair?.let { (inApp, preparedTime) -> mindboxLogI("InApp ${inApp.id} isPriority=${inApp.isPriority}, delayTime=${inApp.delayTime}, skipLimitChecks=${inApp.isPriority}, preparedTime = ${preparedTime.interval} ms") } ?: mindboxLogI("No inapps to show found") } .filterNotNull() diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt index b3544e34f..44d940eb6 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/domain/interfaces/interactors/InAppInteractor.kt @@ -1,6 +1,7 @@ package cloud.mindbox.mobile_sdk.inapp.domain.interfaces.interactors import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp +import cloud.mindbox.mobile_sdk.models.Milliseconds import kotlinx.coroutines.flow.Flow internal interface InAppInteractor { @@ -9,7 +10,7 @@ internal interface InAppInteractor { fun setInAppShown(inAppId: String) - suspend fun processEventAndConfig(): Flow> + suspend fun processEventAndConfig(): Flow> fun saveShownInApp( id: String, diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt index b820d587b..36f375e5c 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManager.kt @@ -3,6 +3,7 @@ package cloud.mindbox.mobile_sdk.inapp.presentation import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp import cloud.mindbox.mobile_sdk.logger.mindboxLogD +import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.pollIf import cloud.mindbox.mobile_sdk.utils.TimeProvider @@ -33,17 +34,17 @@ internal class InAppMessageDelayedManager(private val timeProvider: TimeProvider pendingInAppComparator ) - private val _inAppToShowFlow = MutableSharedFlow>() + private val _inAppToShowFlow = MutableSharedFlow>() val inAppToShowFlow = _inAppToShowFlow.asSharedFlow() private data class PendingInApp( val inApp: InApp, val showTimeMillis: Long, val sequenceNumber: Long, - val preparedTimeMs: Long, + val preparedTimeMs: Milliseconds, ) - internal fun process(inApp: InApp, preparedTimeMs: Long) { + internal fun process(inApp: InApp, preparedTimeMs: Milliseconds) { coroutineScope.launchWithLock(processingMutex) { mindboxLogD("Processing In-App: ${inApp.id}, Priority: ${inApp.isPriority}, Delay: ${inApp.delayTime}") val delay = inApp.delayTime?.interval ?: 0L diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt index a1031ccd4..e7916b306 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerImpl.kt @@ -13,6 +13,8 @@ import cloud.mindbox.mobile_sdk.inapp.domain.models.OnInAppShown import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl import cloud.mindbox.mobile_sdk.logger.mindboxLogI import cloud.mindbox.mobile_sdk.millisToTimeSpan +import cloud.mindbox.mobile_sdk.models.Milliseconds +import cloud.mindbox.mobile_sdk.models.Timestamp import cloud.mindbox.mobile_sdk.managers.MindboxEventManager import cloud.mindbox.mobile_sdk.managers.UserVisitManager import cloud.mindbox.mobile_sdk.monitoring.domain.interfaces.MonitoringInteractor @@ -96,18 +98,18 @@ internal class InAppMessageManagerImpl( return@withContext } - var renderStartTimeMs = 0L + var renderStartTime = Timestamp(0L) val tags = inApp.tags?.takeIf { it.isNotEmpty() } inAppMessageViewDisplayer.tryShowInAppMessage( inAppType = inAppMessage, - onRenderStart = { renderStartTimeMs = timeProvider.currentTimeMillis() }, + onRenderStart = { renderStartTime = timeProvider.currentTimestamp() }, inAppActionCallbacks = object : InAppActionCallbacks { override val onInAppClick = OnInAppClick { inAppInteractor.sendInAppClicked(inAppMessage.inAppId) } override val onInAppShown = OnInAppShown { - handleInAppShown(renderStartTimeMs, preparedTimeMs, inAppMessage, tags) + handleInAppShown(renderStartTime, preparedTimeMs, inAppMessage, tags) } override val onInAppDismiss = OnInAppDismiss { inAppInteractor.saveInAppDismissTime() @@ -203,15 +205,16 @@ internal class InAppMessageManagerImpl( } private fun handleInAppShown( - renderStartTimeMs: Long, - preparedTimeMs: Long, + renderStartTime: Timestamp, + preparedTimeMs: Milliseconds, inAppMessage: InAppType, tags: Map? ) { - val shownTime = timeProvider.currentTimeMillis() - mindboxLogI("Render time is ${shownTime - renderStartTimeMs}ms, prepared time is ${preparedTimeMs}ms") - val timeToDisplay = (preparedTimeMs + (shownTime - renderStartTimeMs)).millisToTimeSpan() - inAppInteractor.saveShownInApp(inAppMessage.inAppId, shownTime, timeToDisplay, tags) + val shownTime = timeProvider.currentTimestamp() + val renderTime = shownTime - renderStartTime + mindboxLogI("Render time is ${renderTime.ms}ms, prepared time is ${preparedTimeMs.interval}ms") + val timeToDisplay = (preparedTimeMs.interval + renderTime.ms).millisToTimeSpan() + inAppInteractor.saveShownInApp(inAppMessage.inAppId, shownTime.ms, timeToDisplay, tags) } companion object { diff --git a/sdk/src/main/java/cloud/mindbox/mobile_sdk/utils/TimeProvider.kt b/sdk/src/main/java/cloud/mindbox/mobile_sdk/utils/TimeProvider.kt index 30d1107d6..2313be075 100644 --- a/sdk/src/main/java/cloud/mindbox/mobile_sdk/utils/TimeProvider.kt +++ b/sdk/src/main/java/cloud/mindbox/mobile_sdk/utils/TimeProvider.kt @@ -1,5 +1,6 @@ package cloud.mindbox.mobile_sdk.utils +import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.models.Timestamp import cloud.mindbox.mobile_sdk.models.toTimestamp @@ -7,10 +8,14 @@ internal interface TimeProvider { fun currentTimeMillis(): Long fun currentTimestamp(): Timestamp + + fun elapsedSince(startTimeMillis: Timestamp): Milliseconds } internal class SystemTimeProvider : TimeProvider { override fun currentTimeMillis() = System.currentTimeMillis() override fun currentTimestamp() = System.currentTimeMillis().toTimestamp() + + override fun elapsedSince(startTimeMillis: Timestamp): Milliseconds = Milliseconds(currentTimeMillis() - startTimeMillis.ms) } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt index dfdb715dd..5d03521fe 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/data/dto/deserializers/InAppTagsDeserializerTest.kt @@ -1,5 +1,6 @@ package cloud.mindbox.mobile_sdk.inapp.data.dto.deserializers +import cloud.mindbox.mobile_sdk.fromJsonTyped import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken @@ -21,10 +22,8 @@ internal class InAppTagsDeserializerTest { .create() } - private fun deserialize(json: String): Map? { - val mapType = object : TypeToken?>() {}.type - return gson.fromJson(json, mapType) - } + private fun deserialize(json: String): Map? = + gson.fromJsonTyped?>(json) @Test fun `deserialize returns string values as is`() { diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt index 7525ff28f..7946d9a24 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageDelayedManagerTest.kt @@ -25,7 +25,7 @@ class InAppMessageDelayedManagerTest { val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } - inAppMessageDelayedManager.process(inApp, 0L) + inAppMessageDelayedManager.process(inApp, Milliseconds(0L)) inAppMessageDelayedManager.inAppToShowFlow.test { advanceTimeBy(10_001) assertEquals(inApp, awaitItem().first) @@ -38,7 +38,7 @@ class InAppMessageDelayedManagerTest { every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) - inAppMessageDelayedManager.process(inApp, 0L) + inAppMessageDelayedManager.process(inApp, Milliseconds(0L)) inAppMessageDelayedManager.inAppToShowFlow.test { advanceTimeBy(9_999) @@ -53,7 +53,7 @@ class InAppMessageDelayedManagerTest { fun `clearSession should cancel pending jobs and clear queue`() = runTest(testDispatcher.scheduler) { every { timeProvider.currentTimeMillis() } answers { testDispatcher.scheduler.currentTime } val inApp = InAppStub.getInApp().copy(delayTime = Milliseconds(10000)) - inAppMessageDelayedManager.process(inApp, 0L) + inAppMessageDelayedManager.process(inApp, Milliseconds(0L)) inAppMessageDelayedManager.clearSession() inAppMessageDelayedManager.inAppToShowFlow.test { testDispatcher.scheduler.advanceUntilIdle() @@ -67,8 +67,8 @@ class InAppMessageDelayedManagerTest { val inAppOne = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(10000)) val inAppTwo = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppOne, 0L) - inAppMessageDelayedManager.process(inAppTwo, 0L) + inAppMessageDelayedManager.process(inAppOne, Milliseconds(0L)) + inAppMessageDelayedManager.process(inAppTwo, Milliseconds(0L)) inAppMessageDelayedManager.inAppToShowFlow.test { assertEquals(inAppTwo, awaitItem().first) @@ -82,8 +82,8 @@ class InAppMessageDelayedManagerTest { val inAppNonPriority = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(5000)) val inAppPriority = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppNonPriority, 0L) - inAppMessageDelayedManager.process(inAppPriority, 0L) + inAppMessageDelayedManager.process(inAppNonPriority, Milliseconds(0L)) + inAppMessageDelayedManager.process(inAppPriority, Milliseconds(0L)) inAppMessageDelayedManager.inAppToShowFlow.test { assertEquals(inAppPriority, awaitItem().first) @@ -97,8 +97,8 @@ class InAppMessageDelayedManagerTest { val inAppFirst = InAppStub.getInApp().copy(id = "inApp1", delayTime = Milliseconds(5000), isPriority = true) val inAppSecond = InAppStub.getInApp().copy(id = "inApp2", delayTime = Milliseconds(5000), isPriority = true) - inAppMessageDelayedManager.process(inAppFirst, 0L) - inAppMessageDelayedManager.process(inAppSecond, 0L) + inAppMessageDelayedManager.process(inAppFirst, Milliseconds(0L)) + inAppMessageDelayedManager.process(inAppSecond, Milliseconds(0L)) inAppMessageDelayedManager.inAppToShowFlow.test { assertEquals(inAppFirst, awaitItem().first) @@ -113,9 +113,9 @@ class InAppMessageDelayedManagerTest { val inAppLoser1 = InAppStub.getInApp().copy(id = "loser1", delayTime = Milliseconds(5000), isPriority = false) val inAppLoser2 = InAppStub.getInApp().copy(id = "loser2", delayTime = Milliseconds(3000), isPriority = false) - inAppMessageDelayedManager.process(inAppWinner, 0L) - inAppMessageDelayedManager.process(inAppLoser1, 0L) - inAppMessageDelayedManager.process(inAppLoser2, 0L) + inAppMessageDelayedManager.process(inAppWinner, Milliseconds(0L)) + inAppMessageDelayedManager.process(inAppLoser1, Milliseconds(0L)) + inAppMessageDelayedManager.process(inAppLoser2, Milliseconds(0L)) advanceTimeBy(5000) inAppMessageDelayedManager.inAppToShowFlow.test { assertEquals(inAppWinner, awaitItem().first) diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt index 335b26276..c0e33f37f 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/inapp/presentation/InAppMessageManagerTest.kt @@ -7,6 +7,7 @@ import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl import cloud.mindbox.mobile_sdk.managers.UserVisitManager import cloud.mindbox.mobile_sdk.models.InAppStub +import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.monitoring.domain.interfaces.MonitoringInteractor import cloud.mindbox.mobile_sdk.repository.MindboxPreferences import cloud.mindbox.mobile_sdk.sortByPriority @@ -137,14 +138,14 @@ internal class InAppMessageManagerTest { @Test fun `in app messages success message shown`() = runTest { - val inAppToShowFlow = MutableSharedFlow>() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageViewDisplayer.isInAppActive() } returns false every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns true every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow every { inAppMessageDelayedManager.process(inApp, any()) } coAnswers { this@runTest.launch { - inAppToShowFlow.emit(inApp to 0L) + inAppToShowFlow.emit(inApp to Milliseconds(0L)) } } @@ -162,7 +163,7 @@ internal class InAppMessageManagerTest { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit(inApp to 0L) + emit(inApp to Milliseconds(0L)) } } @@ -175,7 +176,7 @@ internal class InAppMessageManagerTest { @Test fun `in app messages success message not shown when inApp already active`() = runTest { - val inAppToShowFlow = MutableSharedFlow>() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns true every { inAppMessageViewDisplayer.isInAppActive() } returns true @@ -196,13 +197,13 @@ internal class InAppMessageManagerTest { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit(inApp to 0L) + emit(inApp to Milliseconds(0L)) } } every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow every { inAppMessageDelayedManager.process(inApp, any()) } answers { this@runTest.launch { - inAppToShowFlow.emit(inApp to 0L) + inAppToShowFlow.emit(inApp to Milliseconds(0L)) } } @@ -215,7 +216,7 @@ internal class InAppMessageManagerTest { @Test fun `in app messages success message not shown when inApp frequency or limits not allowed`() = runTest { - val inAppToShowFlow = MutableSharedFlow>() + val inAppToShowFlow = MutableSharedFlow>() val inApp = InAppStub.getInApp() every { inAppMessageInteractor.areShowAndFrequencyLimitsAllowed(any()) } returns false every { inAppMessageViewDisplayer.isInAppActive() } returns false @@ -236,13 +237,13 @@ internal class InAppMessageManagerTest { inAppMessageInteractor.processEventAndConfig() }.answers { flow { - emit(inApp to 0L) + emit(inApp to Milliseconds(0L)) } } every { inAppMessageDelayedManager.inAppToShowFlow } returns inAppToShowFlow every { inAppMessageDelayedManager.process(inApp, any()) } answers { this@runTest.launch { - inAppToShowFlow.emit(inApp to 0L) + inAppToShowFlow.emit(inApp to Milliseconds(0L)) } } diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt index 43d055ec1..5a22a40b7 100644 --- a/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/managers/MobileConfigSettingsManagerTest.kt @@ -4,13 +4,11 @@ import cloud.mindbox.mobile_sdk.Mindbox import cloud.mindbox.mobile_sdk.inapp.data.managers.SessionStorageManager import cloud.mindbox.mobile_sdk.models.Milliseconds import cloud.mindbox.mobile_sdk.models.SettingsStub.Companion.getSlidingExpiration -import cloud.mindbox.mobile_sdk.models.Timestamp import cloud.mindbox.mobile_sdk.models.operation.response.InAppConfigResponse import cloud.mindbox.mobile_sdk.models.operation.response.SettingsDto import cloud.mindbox.mobile_sdk.models.toTimestamp import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager import cloud.mindbox.mobile_sdk.repository.MindboxPreferences -import cloud.mindbox.mobile_sdk.utils.SystemTimeProvider import cloud.mindbox.mobile_sdk.utils.TimeProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -21,21 +19,18 @@ import org.junit.Test class MobileConfigSettingsManagerImplTest { + private val mockTimeProvider = mockk() private lateinit var sessionStorageManager: SessionStorageManager private lateinit var mobileConfigSettingsManager: MobileConfigSettingsManagerImpl private val now = 100_000L @Before fun onTestStart() { - val realSessionStorageManager = SessionStorageManager(SystemTimeProvider()) - sessionStorageManager = spyk(realSessionStorageManager) - mobileConfigSettingsManager = MobileConfigSettingsManagerImpl(mockk(), sessionStorageManager, object : TimeProvider { - override fun currentTimeMillis(): Long = now - - override fun currentTimestamp(): Timestamp { - return now.toTimestamp() - } - }) + every { mockTimeProvider.currentTimeMillis() } returns now + every { mockTimeProvider.currentTimestamp() } returns now.toTimestamp() + + sessionStorageManager = spyk(SessionStorageManager(mockTimeProvider)) + mobileConfigSettingsManager = MobileConfigSettingsManagerImpl(mockk(), sessionStorageManager, mockTimeProvider) mockkObject(Mindbox) mockkObject(MindboxPreferences) mockkObject(MindboxEventManager) diff --git a/sdk/src/test/java/cloud/mindbox/mobile_sdk/utils/TimeProviderTest.kt b/sdk/src/test/java/cloud/mindbox/mobile_sdk/utils/TimeProviderTest.kt new file mode 100644 index 000000000..0f16a400d --- /dev/null +++ b/sdk/src/test/java/cloud/mindbox/mobile_sdk/utils/TimeProviderTest.kt @@ -0,0 +1,73 @@ +package cloud.mindbox.mobile_sdk.utils + +import cloud.mindbox.mobile_sdk.models.Timestamp +import io.mockk.every +import io.mockk.spyk +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class TimeProviderTest { + + private lateinit var timeProvider: SystemTimeProvider + + @Before + fun setup() { + timeProvider = spyk(SystemTimeProvider()) + } + + @Test + fun `elapsedSince returns positive difference when current time is greater`() { + val inputStartTimeMillis = Timestamp(1000L) + val expectedElapsed = 500L + every { timeProvider.currentTimeMillis() } returns 1500L + + val actualElapsed = timeProvider.elapsedSince(inputStartTimeMillis) + + assertEquals(expectedElapsed, actualElapsed.interval) + } + + @Test + fun `elapsedSince returns zero when current time equals start time`() { + val inputStartTimeMillis = Timestamp(1000L) + val expectedElapsed = 0L + every { timeProvider.currentTimeMillis() } returns 1000L + + val actualElapsed = timeProvider.elapsedSince(inputStartTimeMillis) + + assertEquals(expectedElapsed, actualElapsed.interval) + } + + @Test + fun `elapsedSince returns negative value when current time is less than start time`() { + val inputStartTimeMillis = Timestamp(2000L) + val expectedElapsed = -1000L + every { timeProvider.currentTimeMillis() } returns 1000L + + val actualElapsed = timeProvider.elapsedSince(inputStartTimeMillis) + + assertEquals(expectedElapsed, actualElapsed.interval) + } + + @Test + fun `elapsedSince returns correct value when start time is zero`() { + val inputStartTimeMillis = Timestamp(0L) + val expectedElapsed = 5000L + every { timeProvider.currentTimeMillis() } returns 5000L + + val actualElapsed = timeProvider.elapsedSince(inputStartTimeMillis) + + assertEquals(expectedElapsed, actualElapsed.interval) + } + + @Test + fun `elapsedSince returns correct value for large timestamps`() { + val inputStartTimeMillis = Timestamp(1_700_000_000_000L) + val expectedElapsed = 3500L + every { timeProvider.currentTimeMillis() } returns 1_700_000_003_500L + + val actualElapsed = timeProvider.elapsedSince(inputStartTimeMillis) + + assertEquals(expectedElapsed, actualElapsed.interval) + } +}