diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesTests.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesTests.kt new file mode 100644 index 0000000000..dc875dd84a --- /dev/null +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagesTests.kt @@ -0,0 +1,307 @@ +package com.onesignal.inAppMessages.internal + +import br.com.colman.kotest.android.extensions.robolectric.RobolectricTest +import com.onesignal.OneSignal +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.inAppMessages.internal.InAppMessagingHelpers.Companion.buildTestMessageWithRedisplay +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import java.util.UUID + +@RobolectricTest +class InAppMessagesTests : FunSpec({ + val iamClickId = "button_id_123" + val limit = 5 + val delay: Long = 60 + + fun setLocalTriggerValue( + key: String, + localValue: String, + ) { + if (localValue != null) { + OneSignal.InAppMessages.addTrigger( + key, + localValue, + ) + } else { + OneSignal.InAppMessages.removeTrigger(key) + } + } + + fun comparativeOperatorTest( + operator: Trigger.OSTriggerOperator, + triggerValue: String, + localValue: String, + ): Boolean { +// TODO +// setLocalTriggerValue("test_property", localValue) +// val testMessage: InAppMessagingHelpers.OSTestInAppMessageInternal = +// InAppMessagingHelpers.buildTestMessageWithSingleTrigger( +// Trigger.OSTriggerKind.CUSTOM, +// "test_property", +// operator.toString(), +// triggerValue +// ) +// +// return InAppMessagingHelpers.evaluateMessage(testMessage) + return true + } + +// Define message at class level with lazy initialization + val message: InAppMessagingHelpers.OSTestInAppMessageInternal by lazy { + InAppMessagingHelpers.buildTestMessageWithSingleTrigger( + Trigger.OSTriggerKind.SESSION_TIME, + null, + Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO.toString(), + 3, + ) + } + + beforeAny { + Logging.logLevel = LogLevel.VERBOSE + // TODO: add more from Player Model @BeforeClass in InAppMessagingUnitTests.java + } + + beforeTest { + // TODO: add more from Player Model @BeforeTest in InAppMessagingUnitTests.java + var iamLifecycleCounter = 0 + } + + afterTest { + // TODO: reset back to default: clear timers and clean up helpers + } + + test("testBuiltMessage") { + // Given + val messageId = message.messageId + val variants = message.variants + + // Then + UUID.fromString(messageId) // Throws if invalid + variants shouldNotBe null + } + + test("testBuiltMessageVariants") { + message.variants["android"]?.get("es") shouldBe InAppMessagingHelpers.TEST_SPANISH_ANDROID_VARIANT_ID + message.variants["android"]?.get("en") shouldBe InAppMessagingHelpers.TEST_ENGLISH_ANDROID_VARIANT_ID + } + + test("testBuiltMessageReDisplay") { + // Given + val message = buildTestMessageWithRedisplay(limit, delay) + + // Then + message.redisplayStats.isRedisplayEnabled shouldBe true + message.redisplayStats.displayLimit shouldBe limit + message.redisplayStats.displayDelay shouldBe delay + message.redisplayStats.lastDisplayTime shouldBe -1 + message.redisplayStats.displayQuantity shouldBe 0 + + // When + val messageWithoutDisplay: InAppMessagingHelpers.OSTestInAppMessageInternal = + InAppMessagingHelpers.buildTestMessageWithSingleTrigger( + Trigger.OSTriggerKind.SESSION_TIME, + null, + Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO.toString(), + 3, + ) + + // Then + messageWithoutDisplay.redisplayStats.isRedisplayEnabled shouldBe false + messageWithoutDisplay.redisplayStats.displayLimit shouldBe 1 + messageWithoutDisplay.redisplayStats.displayDelay shouldBe 0 + messageWithoutDisplay.redisplayStats.lastDisplayTime shouldBe -1 + messageWithoutDisplay.redisplayStats.displayQuantity shouldBe 0 + } + + test("testBuiltMessageRedisplayLimit") { + val message: InAppMessagingHelpers.OSTestInAppMessageInternal = + buildTestMessageWithRedisplay( + limit, + delay, + ) + for (i in 0 until limit) { + message.redisplayStats.shouldDisplayAgain() shouldBe true + message.redisplayStats.incrementDisplayQuantity() + } + message.redisplayStats.incrementDisplayQuantity() + message.redisplayStats.shouldDisplayAgain() shouldBe false + } + + test("testBuiltMessageRedisplayDelay") { + // TODO + } + + test("testBuiltMessageRedisplayCLickId") { + val message: InAppMessagingHelpers.OSTestInAppMessageInternal = + buildTestMessageWithRedisplay( + limit, + delay, + ) + + message.clickedClickIds.isEmpty() shouldBe true + message.isClickAvailable(iamClickId) + + message.addClickId(iamClickId) + message.clearClickIds() + + message.clickedClickIds.isEmpty() shouldBe true + + message.addClickId(iamClickId) + message.addClickId(iamClickId) + message.clickedClickIds.size shouldBe 1 + + message.isClickAvailable(iamClickId) shouldBe false + + val messageWithoutDisplay2: InAppMessagingHelpers.OSTestInAppMessageInternal = + InAppMessagingHelpers.buildTestMessageWithSingleTrigger( + Trigger.OSTriggerKind.SESSION_TIME, + null, + Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO.toString(), + 3, + ) + + messageWithoutDisplay2.addClickId(iamClickId) + messageWithoutDisplay2.isClickAvailable(iamClickId) shouldBe false + } + + test("testBuiltMessageTrigger") { + val trigger = message.triggers[0][0] + + trigger.kind shouldBe Trigger.OSTriggerKind.SESSION_TIME + trigger.operatorType shouldBe Trigger.OSTriggerOperator.GREATER_THAN_OR_EQUAL_TO + trigger.property shouldBe null + trigger.value shouldBe 3 + } + + test("testParsesMessageActions") { + // TODO + } + + test("testSaveMultipleTriggerValuesGetTrigger") { + // TODO + // Since trigger getter method no longer exists, will need to refactor + } + + test("testSaveMultipleTriggerValues") { + // TODO + } + + // removed tests checking for non-string trigger values + + // create new test for ensuring only string trigger value + test("testTriggerValuesAreStrings") { + // TODO + } + + test("testDeleteSavedTriggerValueGetTriggers") { + // TODO + } + + test("testDeleteSavedTriggerValue") { + // TODO + } + + test("testDeleteMultipleTriggers") { + // TODO + } + + test("testDeleteAllTriggers") { + // TODO + } + + test("testGreaterThanOperator") { + // TODO + } + + test("testGreaterThanOperatorWithString") { + // TODO + } + + // add more operator tests + + test("testMessageSchedulesSessionDurationTimer") { + // TODO + } + + // more trigger tests + + test("testOnMessageActionOccurredOnMessage") { + // TODO: + + // add clickListener + + // val clickListener = object : IInAppMessageClickListener { + // override fun onClick(event: IInAppMessageClickEvent) { + // print(event.result.actionId) + // } + // } + // OneSignal.InAppMessages.addClickListener(clickListener) + + // assertMainThread() + // threadAndTaskWait() + + // call onMessageActionOccurredOnMessage + + // Ensure we make REST call to OneSignal to report click. + + // Ensure we fire public callback that In-App was clicked. + } + + test("testOnMessageWasShown") { + // TODO: +// threadAndTaskWait() +// InAppMessagingHelpers.onMessageWasDisplayed(message) +// +// Compare Shadow Rest Client request + } + + test("testOnPageChanged") { + // TODO + } + + // Tests for IAM Lifecycle +// var iamLifecycleCounter = 0 + + test("testIAMLifecycleEventsFlow") { + + // TODO + // add listener and incremenet counter +// val lifecycleListener = object : IInAppMessageLifecycleListener { +// override fun onWillDisplay(event: IInAppMessageWillDisplayEvent) { +// iamLifecycleCounter++ +// } +// +// override fun onDidDisplay(event: IInAppMessageDidDisplayEvent) { +// iamLifecycleCounter++ +// } +// +// override fun onWillDismiss(event: IInAppMessageWillDismissEvent) { +// iamLifecycleCounter++ +// } +// +// override fun onDidDismiss(event: IInAppMessageDidDismissEvent) { +// iamLifecycleCounter++ +// } +// } +// OneSignal.InAppMessages.addLifecycleListener(lifecycleListener) + +// threadAndTaskWait() +// iamLifecycleCounter shouldBe 0 +// // maybe need threadAndTaskWait +// +// InAppMessagingHelpers.onMessageWillDisplay(message) +// iamLifecycleCounter shouldBe 1 +// +// InAppMessagingHelpers.onMessageWasDisplayed(message) +// iamLifecycleCounter shouldBe 2 +// +// InAppMessagingHelpers.onMessageWillDismiss(message) +// iamLifecycleCounter shouldBe 3 +// +// InAppMessagingHelpers.onMessageDidDismiss(message) +// iamLifecycleCounter shouldBe 4 + } +}) diff --git a/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagingHelpers.kt b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagingHelpers.kt new file mode 100644 index 0000000000..9474362400 --- /dev/null +++ b/OneSignalSDK/onesignal/in-app-messages/src/test/java/com/onesignal/inAppMessages/internal/InAppMessagingHelpers.kt @@ -0,0 +1,269 @@ +package com.onesignal.inAppMessages.internal + +import com.onesignal.core.internal.time.ITime +import io.mockk.mockk +import org.json.JSONArray +import org.json.JSONObject +import java.util.UUID + +class InAppMessagingHelpers { + companion object { + const val TEST_SPANISH_ANDROID_VARIANT_ID = "d8cc-11e4-bed1-df8f05be55ba-a4b3gj7f" + const val TEST_ENGLISH_ANDROID_VARIANT_ID = "11e4-bed1-df8f05be55ba-a4b3gj7f-d8cc" + const val IAM_CLICK_ID = "12345678-1234-1234-1234-123456789012" + const val IAM_PAGE_ID = "12345678-1234-ABCD-1234-123456789012" + const val IAM_HAS_LIQUID = "has_liquid" + + internal fun evaluateMessage(message: InAppMessage) { + // TODO + } + + internal fun onMessageWasDisplayed(message: InAppMessage) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageWasDisplayed(message) + } + + internal fun onMessageActionOccurredOnMessage( + message: InAppMessage, + clickResult: InAppMessageClickResult, + ) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageActionOccurredOnMessage(message, clickResult) + } + + fun buildTestMessageWithLiquid(triggerJson: JSONArray?): OSTestInAppMessageInternal { + val json = basicIAMJSONObject(triggerJson) + json.put(IAM_HAS_LIQUID, true) + return OSTestInAppMessageInternal(json) + } + + internal fun buildTestMessageWithSingleTriggerAndLiquid( + kind: Trigger.OSTriggerKind, + key: String?, + operator: String?, + value: Any?, + ): OSTestInAppMessageInternal { + val triggersJson = + basicTrigger( + kind, + key, + operator!!, + value!!, + ) + return buildTestMessageWithLiquid(triggersJson) + } + + // Most tests build a test message using only one trigger. + // This convenience method makes it easy to build such a message + internal fun buildTestMessageWithSingleTrigger( + kind: Trigger.OSTriggerKind, + key: String?, + operator: String, + value: Any, + ): OSTestInAppMessageInternal { + val triggersJson = basicTrigger(kind, key, operator, value) + return buildTestMessage(triggersJson) + } + + private fun buildTestMessage(triggerJson: JSONArray?): OSTestInAppMessageInternal { + return OSTestInAppMessageInternal(basicIAMJSONObject(triggerJson)) + } + + private fun basicTrigger( + kind: Trigger.OSTriggerKind, + key: String?, + operator: String, + value: Any, + ): JSONArray { + val triggerJson: JSONObject = + object : JSONObject() { + init { + put("id", UUID.randomUUID().toString()) + put("kind", kind.toString()) + put("property", key) + put("operator", operator) + put("value", value) + } + } + + return wrap(wrap(triggerJson)) + } + + private fun wrap(`object`: Any?): JSONArray { + return object : JSONArray() { + init { + put(`object`) + } + } + } + + fun buildTestMessageWithRedisplay( + limit: Int, + delay: Long, + ): OSTestInAppMessageInternal { + return buildTestMessageWithMultipleDisplays(null, limit, delay) + } + + private fun buildTestMessageWithMultipleDisplays( + triggerJson: JSONArray?, + limit: Int, + delay: Long, + ): OSTestInAppMessageInternal { + val json = basicIAMJSONObject(triggerJson) + json.put( + "redisplay", + object : JSONObject() { + init { + put("limit", limit) + put("delay", delay) // in seconds + } + }, + ) + + return OSTestInAppMessageInternal(json) + } + + private fun basicIAMJSONObject(triggerJson: JSONArray?): JSONObject { + val jsonObject = JSONObject() + jsonObject.put("id", UUID.randomUUID().toString()) + jsonObject.put("clickIds", JSONArray(listOf("clickId1", "clickId2", "clickId3"))) + // shouldn't hard-code? + jsonObject.put("displayedInSession", true) + jsonObject.put( + "variants", + JSONObject().apply { + put( + "android", + JSONObject().apply { + put("es", TEST_SPANISH_ANDROID_VARIANT_ID) + put("en", TEST_ENGLISH_ANDROID_VARIANT_ID) + }, + ) + }, + ) + jsonObject.put("max_display_time", 30) + if (triggerJson != null) { + jsonObject.put("triggers", triggerJson) + } else { + jsonObject.put("triggers", JSONArray()) + } + jsonObject.put( + "actions", + JSONArray().apply { + put(buildTestActionJson()) + }, + ) + + return jsonObject + } + + fun buildTestActionJson(): JSONObject { + return object : JSONObject() { + init { + put("click_type", "button") + put("id", IAM_CLICK_ID) + put("name", "click_name") + put("url", "https://www.onesignal.com") + put("url_target", "webview") + put("close", true) + put("pageId", IAM_PAGE_ID) + put( + "data", + object : JSONObject() { + init { + put("test", "value") + } + }, + ) + } + } + } + } + + // WIP + + /** IAM Lifecycle */ + internal fun onMessageWillDisplay(message: InAppMessage) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageWillDisplay(message) + } + + internal fun onMessageDidDisplay(message: InAppMessage) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageWasDisplayed(message) + } + + internal fun onMessageWillDismiss(message: InAppMessage) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageWillDismiss(message) + } + + internal fun onMessageDidDismiss(message: InAppMessage) { + val mockInAppMessageManager = mockk() + mockInAppMessageManager.onMessageWasDismissed(message) + } + + // End IAM Lifecycle + + class OSTestInAppMessageInternal( + private val jsonObject: JSONObject, + ) { + private val inAppMessage: InAppMessage by lazy { + initializeInAppMessage() + } + + private fun initializeInAppMessage(): InAppMessage { + val time = + object : ITime { + override val currentTimeMillis: Long + get() = System.currentTimeMillis() + } + + return InAppMessage(jsonObject, time) + } + + val messageId: String + get() = inAppMessage.messageId + + val variants: Map> + get() = inAppMessage.variants + + internal val triggers: List> + get() = inAppMessage.triggers + + val clickedClickIds: MutableSet + get() = inAppMessage.clickedClickIds + + var isDisplayedInSession: Boolean + get() = inAppMessage.isDisplayedInSession + set(value) { + inAppMessage.isDisplayedInSession = value + } + + internal val redisplayStats: InAppMessageRedisplayStats + get() = inAppMessage.redisplayStats + + // Extract limit and delay from the JSON object + private val redisplayLimit: Int + get() = jsonObject.optJSONObject("redisplay")?.optInt("limit", -1) ?: -1 + + private val redisplayDelay: Long + get() = jsonObject.optJSONObject("redisplay")?.optLong("delay", -1L) ?: -1L + + fun isClickAvailable(clickId: String?): Boolean { + return !clickedClickIds.contains(clickId) + } + + fun addClickId(clickId: String) { + clickedClickIds.add(clickId) + } + + fun clearClickIds() { + clickedClickIds.clear() + } + + override fun toString(): String { + return "OSTestInAppMessageInternal(jsonObject=$jsonObject, redisplayLimit=$redisplayLimit, redisplayDelay=$redisplayDelay)" + } + } +}