-
Notifications
You must be signed in to change notification settings - Fork 33
[SDK - 7] - BCIT Push #963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,11 @@ abstract class BaseIntegrationTest { | |
| // URL handler tracking for tests | ||
| private val urlHandlerCalled = AtomicBoolean(false) | ||
| private val lastHandledUrl = AtomicReference<String?>(null) | ||
|
|
||
| // Custom action handler tracking for tests | ||
| private val customActionHandlerCalled = AtomicBoolean(false) | ||
| private val lastHandledAction = AtomicReference<com.iterable.iterableapi.IterableAction?>(null) | ||
| private val lastHandledActionType = AtomicReference<String?>(null) | ||
|
Comment on lines
+39
to
+40
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is like mutual lock which is assigned a value when handlers are called. Used by test to affirm handlers getting called. |
||
|
|
||
| @Before | ||
| open fun setUp() { | ||
|
|
@@ -41,6 +46,7 @@ abstract class BaseIntegrationTest { | |
|
|
||
| // Reset tracking flags | ||
| resetUrlHandlerTracking() | ||
| resetCustomActionHandlerTracking() | ||
|
|
||
| // Set test mode flag to prevent MainActivity from initializing SDK | ||
| // This ensures our test config (with test handlers) is the one used | ||
|
|
@@ -71,17 +77,26 @@ abstract class BaseIntegrationTest { | |
| testUtils.setInAppMessageDisplayed(true) | ||
| com.iterable.iterableapi.IterableInAppHandler.InAppResponse.SHOW | ||
| } | ||
| .setCustomActionHandler { action, context -> | ||
| // Handle custom actions during tests | ||
| Log.d("BaseIntegrationTest", "Custom action triggered: $action") | ||
| true | ||
| } | ||
| .setCustomActionHandler(object : com.iterable.iterableapi.IterableCustomActionHandler { | ||
| override fun handleIterableCustomAction( | ||
| action: com.iterable.iterableapi.IterableAction, | ||
| actionContext: com.iterable.iterableapi.IterableActionContext | ||
| ): Boolean { | ||
| // Handle custom actions during tests | ||
| val actionType = action.getType() | ||
| Log.d("BaseIntegrationTest", "Custom action triggered: type=$actionType, action=$action, source=${actionContext.source}") | ||
| customActionHandlerCalled.set(true) | ||
| lastHandledAction.set(action) | ||
| lastHandledActionType.set(actionType) | ||
| return false | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. returning false here despite handling the deeplink so taht we can verify if SDK is going to take any action aruond it or not. |
||
| } | ||
|
Comment on lines
+80
to
+92
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking |
||
| }) | ||
| .setUrlHandler { url, context -> | ||
| // Handle URLs during tests | ||
| Log.d("BaseIntegrationTest", "URL handler triggered: $url") | ||
| urlHandlerCalled.set(true) | ||
| lastHandledUrl.set(url.toString()) | ||
| true | ||
| false | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. returning false here as I want SDK to take action when google.com is clicked. Returning true indicates that app has handled the link and prevent SDK to take further action. Without this change, it will not open google.com on chrome. It gives opportunity for developer to handle it internally first. |
||
| } | ||
| .build() | ||
|
|
||
|
|
@@ -110,6 +125,21 @@ abstract class BaseIntegrationTest { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if notification permission is granted | ||
| */ | ||
| protected fun hasNotificationPermission(): Boolean { | ||
| return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { | ||
| androidx.core.content.ContextCompat.checkSelfPermission( | ||
| context, | ||
| android.Manifest.permission.POST_NOTIFICATIONS | ||
| ) == android.content.pm.PackageManager.PERMISSION_GRANTED | ||
| } else { | ||
| // For Android 12 and below, notifications are enabled by default | ||
| androidx.core.app.NotificationManagerCompat.from(context).areNotificationsEnabled() | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
+131
to
+142
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notification permission logic |
||
| /** | ||
| * Wait for a condition to be true with timeout | ||
| */ | ||
|
|
@@ -141,6 +171,18 @@ abstract class BaseIntegrationTest { | |
| testUtils.triggerCampaignViaAPI(campaignId, recipientEmail, dataFields, callback) | ||
| } | ||
|
|
||
| /** | ||
| * Trigger a push campaign via Iterable API | ||
| */ | ||
| protected fun triggerPushCampaignViaAPI( | ||
| campaignId: Int, | ||
| recipientEmail: String = TestConstants.TEST_USER_EMAIL, | ||
| dataFields: Map<String, Any>? = null, | ||
| callback: ((Boolean) -> Unit)? = null | ||
| ) { | ||
| testUtils.triggerPushCampaignViaAPI(campaignId, recipientEmail, dataFields, callback) | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Reset URL handler tracking | ||
|
|
@@ -165,4 +207,36 @@ abstract class BaseIntegrationTest { | |
| urlHandlerCalled.get() | ||
| }, timeoutSeconds) | ||
| } | ||
|
|
||
| /** | ||
| * Reset custom action handler tracking | ||
| */ | ||
| protected fun resetCustomActionHandlerTracking() { | ||
| customActionHandlerCalled.set(false) | ||
| lastHandledAction.set(null) | ||
| lastHandledActionType.set(null) | ||
| } | ||
|
|
||
| /** | ||
| * Get the last action handled by the custom action handler | ||
| */ | ||
| protected fun getLastHandledAction(): com.iterable.iterableapi.IterableAction? { | ||
| return lastHandledAction.get() | ||
| } | ||
|
|
||
| /** | ||
| * Get the last action type handled by the custom action handler | ||
| */ | ||
| protected fun getLastHandledActionType(): String? { | ||
| return lastHandledActionType.get() | ||
| } | ||
|
|
||
| /** | ||
| * Wait for custom action handler to be called | ||
| */ | ||
| protected fun waitForCustomActionHandler(timeoutSeconds: Long = TIMEOUT_SECONDS): Boolean { | ||
| return waitForCondition({ | ||
| customActionHandlerCalled.get() | ||
| }, timeoutSeconds) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| package com.iterable.integration.tests | ||
|
|
||
| import android.content.Intent | ||
| import android.util.Log | ||
| import androidx.lifecycle.Lifecycle | ||
| import androidx.test.core.app.ActivityScenario | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import androidx.test.uiautomator.UiDevice | ||
| import androidx.test.uiautomator.UiObject2 | ||
| import androidx.test.uiautomator.UiSelector | ||
| import androidx.test.uiautomator.By | ||
| import com.iterable.iterableapi.IterableApi | ||
| import com.iterable.integration.tests.activities.PushNotificationTestActivity | ||
| import org.awaitility.Awaitility | ||
| import org.junit.After | ||
| import org.junit.Assert | ||
| import org.junit.Before | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class PushNotificationIntegrationTest : BaseIntegrationTest() { | ||
|
|
||
| companion object { | ||
| private const val TAG = "PushNotificationIntegrationTest" | ||
| private const val TEST_PUSH_CAMPAIGN_ID = TestConstants.TEST_PUSH_CAMPAIGN_ID | ||
| } | ||
|
|
||
| private lateinit var uiDevice: UiDevice | ||
| private lateinit var mainActivityScenario: ActivityScenario<MainActivity> | ||
|
|
||
| @Before | ||
| override fun setUp() { | ||
| uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) | ||
| super.setUp() | ||
|
|
||
| IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) | ||
| IterableApi.getInstance().inAppManager.messages.forEach { | ||
| IterableApi.getInstance().inAppManager.removeMessage(it) | ||
| } | ||
|
Comment on lines
+39
to
+42
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping this part similar so that inapps do not interrupt the UI |
||
|
|
||
| launchAppAndNavigateToPushNotificationTesting() | ||
| } | ||
|
|
||
| @After | ||
| override fun tearDown() { | ||
| super.tearDown() | ||
| } | ||
|
|
||
| private fun launchAppAndNavigateToPushNotificationTesting() { | ||
| Log.d(TAG, "Step 1: Launching MainActivity and navigating to PushNotificationTestActivity") | ||
| val mainIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, MainActivity::class.java) | ||
| mainActivityScenario = ActivityScenario.launch(mainIntent) | ||
|
|
||
| Awaitility.await() | ||
| .atMost(5, TimeUnit.SECONDS) | ||
| .pollInterval(500, TimeUnit.MILLISECONDS) | ||
| .until { | ||
| mainActivityScenario.state == Lifecycle.State.RESUMED | ||
| } | ||
|
|
||
| val pushButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnPushNotifications")) | ||
| if (!pushButton.exists()) { | ||
| Assert.fail("Push Notifications button not found in MainActivity") | ||
| } | ||
| pushButton.click() | ||
| Thread.sleep(2000) | ||
| } | ||
|
|
||
| @Test | ||
| fun testPushNotificationMVP() { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the main test function |
||
| Assert.assertTrue("User should be signed in", testUtils.ensureUserSignedIn(TestConstants.TEST_USER_EMAIL)) | ||
| Assert.assertTrue("Notification permission should be granted", hasNotificationPermission()) | ||
|
|
||
| // Test 1: Trigger campaign, minimize app, open notification, verify app opens | ||
| Log.d(TAG, "Test 1: Push notification open action") | ||
| triggerCampaignAndWait() | ||
| uiDevice.pressHome() | ||
| Thread.sleep(1000) | ||
|
|
||
| uiDevice.openNotification() | ||
| Thread.sleep(1000) | ||
| val notification1 = findNotification() | ||
| Assert.assertNotNull("Notification should be found", notification1) | ||
|
|
||
| notification1?.click() | ||
| Thread.sleep(2000) // Wait for app to open | ||
|
|
||
| // Verify app is in foreground by checking current package name | ||
| val isAppInForeground = waitForCondition({ | ||
| val currentPackage = uiDevice.currentPackageName | ||
| currentPackage == "com.iterable.integration.tests" | ||
| }, timeoutSeconds = 5) | ||
| Assert.assertTrue("App should be in foreground after opening notification", isAppInForeground) | ||
| navigateToPushNotificationTestActivity() | ||
|
|
||
| // Test 2: Trigger campaign again, tap first action button (Google), verify URL handler | ||
| Log.d(TAG, "Test 2: Action button with URL handler") | ||
| triggerCampaignAndWait() | ||
| uiDevice.pressHome() | ||
| Thread.sleep(1000) | ||
|
|
||
| uiDevice.openNotification() | ||
| Thread.sleep(2000) | ||
| val notification2 = findNotification() | ||
| Assert.assertNotNull("Notification should be found", notification2) | ||
|
|
||
| resetUrlHandlerTracking() | ||
| val googleButton = uiDevice.findObject(By.text("Google")) | ||
| Assert.assertNotNull("Google button should be found", googleButton) | ||
| googleButton?.click() | ||
| Thread.sleep(2000) | ||
|
|
||
| Assert.assertTrue("URL handler should be called", waitForUrlHandler(timeoutSeconds = 5)) | ||
| Assert.assertNotNull("Handled URL should not be null", getLastHandledUrl()) | ||
|
|
||
| // Navigate back to PushNotificationTestActivity for next test (in case action button opened app) | ||
| Thread.sleep(1000) | ||
| navigateToPushNotificationTestActivity() | ||
|
|
||
| // Test 3: Trigger campaign again, tap second action button (Deeplink), verify custom action handler | ||
| Log.d(TAG, "Test 3: Action button with custom action handler") | ||
| triggerCampaignAndWait() | ||
| uiDevice.pressHome() | ||
| Thread.sleep(1000) | ||
|
|
||
| uiDevice.openNotification() | ||
| Thread.sleep(2000) | ||
| val notification3 = findNotification() | ||
| Assert.assertNotNull("Notification should be found", notification3) | ||
|
|
||
| resetCustomActionHandlerTracking() | ||
| val deeplinkButton = uiDevice.findObject(By.text("Deeplink")) | ||
| Assert.assertNotNull("Deeplink button should be found", deeplinkButton) | ||
| deeplinkButton?.click() | ||
| Thread.sleep(2000) | ||
|
|
||
| Assert.assertTrue("Custom action handler should be called", waitForCustomActionHandler(timeoutSeconds = 5)) | ||
| Assert.assertNotNull("Action type should not be null", getLastHandledActionType()) | ||
|
|
||
| // Navigate back to PushNotificationTestActivity (in case action button opened app) | ||
| Thread.sleep(1000) | ||
| navigateToPushNotificationTestActivity() | ||
|
|
||
| // Note: trackPushOpen() is called internally by the SDK when notifications are opened | ||
| // It's automatically invoked by IterablePushNotificationUtil.executeAction() which is called | ||
| // by the trampoline activity when handling push notification clicks | ||
| Log.d(TAG, "Test completed successfully") | ||
| } | ||
|
|
||
| private fun triggerCampaignAndWait() { | ||
| var campaignTriggered = false | ||
| val latch = java.util.concurrent.CountDownLatch(1) | ||
| triggerPushCampaignViaAPI(TEST_PUSH_CAMPAIGN_ID, TestConstants.TEST_USER_EMAIL, null) { success -> | ||
| campaignTriggered = success | ||
| latch.countDown() | ||
| } | ||
| Assert.assertTrue("Campaign trigger should complete", latch.await(10, java.util.concurrent.TimeUnit.SECONDS)) | ||
| Assert.assertTrue("Campaign should be triggered successfully", campaignTriggered) | ||
| Thread.sleep(5000) // Wait for FCM delivery | ||
| } | ||
|
|
||
| private fun findNotification(): UiObject2? { | ||
| val searchTexts = listOf("BCIT", "iterable", "Test", TestConstants.TEST_USER_EMAIL) | ||
| for (searchText in searchTexts) { | ||
| val notification = uiDevice.findObject(By.textContains(searchText)) | ||
| if (notification != null) return notification | ||
| } | ||
|
|
||
| val allNotifications = uiDevice.findObjects(By.res("com.android.systemui:id/notification_text")) | ||
| for (notif in allNotifications) { | ||
| val text = notif.text ?: "" | ||
| if (text.contains("Iterable", ignoreCase = true) || text.contains("iterable", ignoreCase = true)) { | ||
| return notif.parent | ||
| } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| private fun navigateToPushNotificationTestActivity() { | ||
| // Wait a bit for the app to fully open | ||
| Thread.sleep(1000) | ||
|
|
||
| // Try to find and click the Push Notifications button in MainActivity | ||
| val pushButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnPushNotifications")) | ||
| if (pushButton.exists()) { | ||
| pushButton.click() | ||
| Thread.sleep(2000) // Wait for navigation | ||
| } else { | ||
| // If button not found, try launching the activity directly | ||
| val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, PushNotificationTestActivity::class.java) | ||
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||
| InstrumentationRegistry.getInstrumentation().targetContext.startActivity(intent) | ||
| Thread.sleep(2000) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,7 +57,21 @@ class MainActivity : AppCompatActivity() { | |
| val intent = Intent(this@MainActivity, DeepLinkTestActivity::class.java) | ||
| intent.putExtra(EXTRA_DEEP_LINK_URL, url.toString()) | ||
| startActivity(intent) | ||
| return true | ||
| return false | ||
| } | ||
| }) | ||
| .setCustomActionHandler(object : com.iterable.iterableapi.IterableCustomActionHandler { | ||
| override fun handleIterableCustomAction( | ||
| action: com.iterable.iterableapi.IterableAction, | ||
| actionContext: com.iterable.iterableapi.IterableActionContext | ||
| ): Boolean { | ||
| val actionType = action.getType() | ||
| // Log action.data() | ||
| val actionData = action.getData() | ||
| Log.d(TAG, "Custom action received: type=$actionType, data=$actionData") | ||
| // You can add custom logic here to handle different action types | ||
| // For now, just log and return true to indicate the action was handled | ||
| return false | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to BaseIntegration test, it may seem duplicate, but for manual testing, this is where the Iterable initializes vs for Test, BaseIntegrationTest file is where the SDK initializes with all the config |
||
| } | ||
| }) | ||
| .build() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on following Firebase docs. pretty standard