From 8f43866a435f25c6da3a7d55652202143118deda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <β€œayyanchira.akshay@gmail.com”> Date: Thu, 13 Nov 2025 07:25:25 -0800 Subject: [PATCH 1/3] [SDK-7]-BCIT Push Base Activity created; utility function added in BaseIntegrationTest; Dedicated PushNotification Test file with one mvp function; Constants for new campaign id from Mobile SDK do not Delete project; An actvity for push modified --- .../integration/tests/BaseIntegrationTest.kt | 12 + .../tests/PushNotificationIntegrationTest.kt | 213 ++++++++++++++++++ .../integration/tests/TestConstants.kt | 2 +- .../PushNotificationTestActivity.kt | 14 +- .../activity_push_notification_test.xml | 47 ++-- 5 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt index d7c8d5e3f..649765852 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt @@ -141,6 +141,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? = null, + callback: ((Boolean) -> Unit)? = null + ) { + testUtils.triggerPushCampaignViaAPI(campaignId, recipientEmail, dataFields, callback) + } + /** * Reset URL handler tracking diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt new file mode 100644 index 000000000..097944ac5 --- /dev/null +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt @@ -0,0 +1,213 @@ +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.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 + + @Before + override fun setUp() { + Log.d(TAG, "πŸ”§ Test setup starting...") + + uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + // Call super.setUp() to initialize SDK with BaseIntegrationTest's config + // This sets test mode flag and initializes SDK with test handlers (including urlHandler) + super.setUp() + + Log.d(TAG, "πŸ”§ Base setup complete, SDK initialized with test handlers") + + // Disable in-app auto display and clear existing messages BEFORE launching app + // This prevents in-app messages from obscuring the push notification test screen + Log.d(TAG, "πŸ”§ Disabling in-app auto display and clearing existing messages...") + IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) + Log.d(TAG, "βœ… In-app auto display paused") + + // Clear all existing in-app messages + IterableApi.getInstance().inAppManager.messages.forEach { + Log.d(TAG, "Clearing existing in-app message: ${it.messageId}") + IterableApi.getInstance().inAppManager.removeMessage(it) + } + Log.d(TAG, "βœ… All in-app messages cleared") + + Log.d(TAG, "πŸ”§ MainActivity will skip initialization due to test mode flag") + + // Now launch the app flow with custom handlers already configured + launchAppAndNavigateToPushNotificationTesting() + } + + @After + override fun tearDown() { + super.tearDown() + } + + private fun launchAppAndNavigateToPushNotificationTesting() { + // Step 1: Launch MainActivity (the home page) + Log.d(TAG, "πŸ”§ Step 1: Launching MainActivity...") + val mainIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, MainActivity::class.java) + mainActivityScenario = ActivityScenario.launch(mainIntent) + + // Wait for MainActivity to be ready + Awaitility.await() + .atMost(5, TimeUnit.SECONDS) + .pollInterval(500, TimeUnit.MILLISECONDS) + .until { + val state = mainActivityScenario.state + Log.d(TAG, "πŸ”§ MainActivity state: $state") + state == Lifecycle.State.RESUMED + } + + Log.d(TAG, "πŸ”§ MainActivity is ready!") + + // Step 2: Click the "Push Notifications" button to navigate to PushNotificationTestActivity + Log.d(TAG, "πŸ”§ Step 2: Clicking 'Push Notifications' button...") + val pushButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnPushNotifications")) + if (pushButton.exists()) { + pushButton.click() + Log.d(TAG, "πŸ”§ Clicked Push Notifications button successfully") + } else { + Log.e(TAG, "❌ Push Notifications button not found!") + Assert.fail("Push Notifications button not found in MainActivity") + } + + // Step 3: Wait for PushNotificationTestActivity to load + Log.d(TAG, "πŸ”§ Step 3: Waiting for PushNotificationTestActivity to load...") + Thread.sleep(2000) // Give time for navigation + + Log.d(TAG, "πŸ”§ App navigation complete: Now on PushNotificationTestActivity!") + } + + @Test + fun testPushNotificationMVP() { + // Step 1: Ensure user is signed in + Log.d(TAG, "πŸ“§ Step 1: Ensuring user is signed in...") + val userSignedIn = testUtils.ensureUserSignedIn(TestConstants.TEST_USER_EMAIL) + Assert.assertTrue("User should be signed in", userSignedIn) + Log.d(TAG, "βœ… User signed in successfully: ${TestConstants.TEST_USER_EMAIL}") + + // Step 2: Trigger push notification campaign via API + Log.d(TAG, "🎯 Step 2: Triggering push notification campaign via API...") + Log.d(TAG, "Campaign ID: $TEST_PUSH_CAMPAIGN_ID") + Log.d(TAG, "User Email: ${TestConstants.TEST_USER_EMAIL}") + + var campaignTriggered = false + val latch = java.util.concurrent.CountDownLatch(1) + + triggerPushCampaignViaAPI(TEST_PUSH_CAMPAIGN_ID, TestConstants.TEST_USER_EMAIL, null) { success -> + campaignTriggered = success + Log.d(TAG, "🎯 Push campaign trigger result: $success") + if (!success) { + val errorMessage = testUtils.getLastErrorMessage() + Log.w(TAG, "⚠️ Push campaign trigger failed: $errorMessage") + } + latch.countDown() + } + + // Wait for API call to complete (up to 10 seconds for CI) + val apiCallCompleted = latch.await(10, java.util.concurrent.TimeUnit.SECONDS) + Log.d(TAG, "🎯 API call completed: $apiCallCompleted, success: $campaignTriggered") + + if (!apiCallCompleted) { + Log.e(TAG, "❌ API call did not complete in time") + Assert.fail("Push campaign trigger API call did not complete in time") + return + } + + if (!campaignTriggered) { + val errorMessage = testUtils.getLastErrorMessage() + Log.e(TAG, "❌ Push campaign trigger FAILED: $errorMessage") + Log.e(TAG, "❌ Cannot proceed with test - no push notification will be available") + Assert.fail("Push campaign trigger failed: $errorMessage. Check API key and campaign configuration.") + return + } + + Log.d(TAG, "βœ… Push campaign triggered successfully, waiting for notification...") + + // Step 3: Wait for push notification to arrive (give time for FCM delivery) + Log.d(TAG, "⏳ Step 3: Waiting for push notification to arrive...") + Thread.sleep(5000) // Give time for FCM to deliver the notification + + // Step 4: Open notification drawer and verify notification is present + Log.d(TAG, "πŸ“± Step 4: Opening notification drawer...") + uiDevice.openNotification() + Thread.sleep(2000) // Wait for notification drawer to open + + // Step 5: Find and interact with the notification + Log.d(TAG, "πŸ” Step 5: Looking for push notification in notification drawer...") + + // Try to find notification by text (common notification text patterns) + var notificationFound = false + var notification = uiDevice.findObject(By.textContains("Iterable")) + ?: uiDevice.findObject(By.textContains("iterable")) + ?: uiDevice.findObject(By.textContains("Test")) + + if (notification == null) { + // Try to find any notification that might be from our app + val notifications = uiDevice.findObjects(By.res("com.android.systemui:id/notification_stack_scroller")) + if (notifications.isNotEmpty()) { + notification = notifications.first() + notificationFound = true + } + } else { + notificationFound = true + } + + if (!notificationFound || notification == null) { + Log.e(TAG, "❌ Push notification not found in notification drawer") + uiDevice.pressBack() // Close notification drawer + Assert.fail("Push notification not found in notification drawer. Check FCM configuration and campaign setup.") + return + } + + Log.d(TAG, "βœ… Push notification found in notification drawer") + + // Step 6: Click on the notification to open it + Log.d(TAG, "🎯 Step 6: Clicking on push notification...") + notification.click() + Thread.sleep(2000) // Wait for app to open + + // Step 7: Verify URL handler was called (if notification has action) + Log.d(TAG, "🎯 Step 7: Verifying URL handler was called after notification click...") + + val urlHandlerCalled = waitForUrlHandler(timeoutSeconds = 5) + if (urlHandlerCalled) { + // Step 8: Verify the correct URL was handled + val handledUrl = getLastHandledUrl() + Log.d(TAG, "🎯 URL handler received: $handledUrl") + + Assert.assertNotNull("Handled URL should not be null", handledUrl) + Log.d(TAG, "βœ… URL handler was called with URL: $handledUrl") + } else { + Log.d(TAG, "ℹ️ URL handler was not called - notification may not have an action URL") + // This is acceptable if the notification doesn't have a deep link action + } + + Log.d(TAG, "βœ…βœ…βœ… Test completed successfully! All steps passed.") + } +} + diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/TestConstants.kt b/integration-tests/src/main/java/com/iterable/integration/tests/TestConstants.kt index 80f2464e0..9e84d3760 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/TestConstants.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/TestConstants.kt @@ -12,7 +12,7 @@ object TestConstants { // Test campaign IDs - these should be configured in your Iterable project const val TEST_INAPP_CAMPAIGN_ID = 14332357 - const val TEST_PUSH_CAMPAIGN_ID = 14332358 + const val TEST_PUSH_CAMPAIGN_ID = 15671239 const val TEST_EMBEDDED_CAMPAIGN_ID = 14332359 // Test placement IDs diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt index 1eeab9cf3..43e3d273e 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt @@ -2,6 +2,7 @@ package com.iterable.integration.tests.activities import android.os.Bundle import android.util.Log +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.iterable.integration.tests.R @@ -11,12 +12,23 @@ class PushNotificationTestActivity : AppCompatActivity() { private const val TAG = "PushNotificationTest" } + private lateinit var statusTextView: TextView + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_push_notification_test) Log.d(TAG, "Push Notification Test Activity started") - // TODO: Implement push notification test UI and logic + initializeViews() + updateStatus("Ready to test push notifications") + } + + private fun initializeViews() { + statusTextView = findViewById(R.id.status_text) + } + + private fun updateStatus(status: String) { + statusTextView.text = "Status: $status" } } \ No newline at end of file diff --git a/integration-tests/src/main/res/layout/activity_push_notification_test.xml b/integration-tests/src/main/res/layout/activity_push_notification_test.xml index 8034d9639..3d11f23cc 100644 --- a/integration-tests/src/main/res/layout/activity_push_notification_test.xml +++ b/integration-tests/src/main/res/layout/activity_push_notification_test.xml @@ -1,25 +1,34 @@ - + android:layout_height="match_parent"> - + android:orientation="vertical" + android:padding="16dp"> - + + + - \ No newline at end of file + + \ No newline at end of file From 5a8ccd8aeace464cbdfa2e70436708c0d828bc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <β€œayyanchira.akshay@gmail.com”> Date: Thu, 13 Nov 2025 20:16:14 -0800 Subject: [PATCH 2/3] google service json now has actual firebase project. - Json file is gitignored for now - Not sure how github action will perform. - Device is receiving push notification - However, it was noticed taht test were passing even before that indicating tests written are not accurate --- integration-tests/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 03416e6a6..4ef5616a2 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -1,3 +1,9 @@ +plugins { + id 'com.android.application' + // Add the Google services Gradle plugin + id 'com.google.gms.google-services' +} + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'jacoco' From a58d3cf10eb2cbef5deafa1811833941f66e7c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <β€œayyanchira.akshay@gmail.com”> Date: Tue, 18 Nov 2025 12:14:07 -0800 Subject: [PATCH 3/3] Final touches --- .../integration/tests/BaseIntegrationTest.kt | 74 +++++- .../tests/PushNotificationIntegrationTest.kt | 241 +++++++++--------- .../integration/tests/MainActivity.kt | 16 +- .../PushNotificationTestActivity.kt | 148 +++++++++++ .../IntegrationFirebaseMessagingService.kt | 5 +- .../tests/utils/IntegrationTestUtils.kt | 2 +- .../activity_push_notification_test.xml | 29 +++ 7 files changed, 377 insertions(+), 138 deletions(-) diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt index 649765852..d19bf31fe 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt @@ -33,6 +33,11 @@ abstract class BaseIntegrationTest { // URL handler tracking for tests private val urlHandlerCalled = AtomicBoolean(false) private val lastHandledUrl = AtomicReference(null) + + // Custom action handler tracking for tests + private val customActionHandlerCalled = AtomicBoolean(false) + private val lastHandledAction = AtomicReference(null) + private val lastHandledActionType = AtomicReference(null) @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 + } + }) .setUrlHandler { url, context -> // Handle URLs during tests Log.d("BaseIntegrationTest", "URL handler triggered: $url") urlHandlerCalled.set(true) lastHandledUrl.set(url.toString()) - true + false } .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() + } + } + /** * Wait for a condition to be true with timeout */ @@ -177,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) + } } \ No newline at end of file diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt index 097944ac5..c546dcf4f 100644 --- a/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/PushNotificationIntegrationTest.kt @@ -7,6 +7,7 @@ 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 @@ -32,32 +33,14 @@ class PushNotificationIntegrationTest : BaseIntegrationTest() { @Before override fun setUp() { - Log.d(TAG, "πŸ”§ Test setup starting...") - uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - - // Call super.setUp() to initialize SDK with BaseIntegrationTest's config - // This sets test mode flag and initializes SDK with test handlers (including urlHandler) super.setUp() - Log.d(TAG, "πŸ”§ Base setup complete, SDK initialized with test handlers") - - // Disable in-app auto display and clear existing messages BEFORE launching app - // This prevents in-app messages from obscuring the push notification test screen - Log.d(TAG, "πŸ”§ Disabling in-app auto display and clearing existing messages...") IterableApi.getInstance().inAppManager.setAutoDisplayPaused(true) - Log.d(TAG, "βœ… In-app auto display paused") - - // Clear all existing in-app messages IterableApi.getInstance().inAppManager.messages.forEach { - Log.d(TAG, "Clearing existing in-app message: ${it.messageId}") IterableApi.getInstance().inAppManager.removeMessage(it) } - Log.d(TAG, "βœ… All in-app messages cleared") - - Log.d(TAG, "πŸ”§ MainActivity will skip initialization due to test mode flag") - // Now launch the app flow with custom handlers already configured launchAppAndNavigateToPushNotificationTesting() } @@ -67,147 +50,151 @@ class PushNotificationIntegrationTest : BaseIntegrationTest() { } private fun launchAppAndNavigateToPushNotificationTesting() { - // Step 1: Launch MainActivity (the home page) - Log.d(TAG, "πŸ”§ Step 1: Launching MainActivity...") + Log.d(TAG, "Step 1: Launching MainActivity and navigating to PushNotificationTestActivity") val mainIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, MainActivity::class.java) mainActivityScenario = ActivityScenario.launch(mainIntent) - // Wait for MainActivity to be ready Awaitility.await() .atMost(5, TimeUnit.SECONDS) .pollInterval(500, TimeUnit.MILLISECONDS) .until { - val state = mainActivityScenario.state - Log.d(TAG, "πŸ”§ MainActivity state: $state") - state == Lifecycle.State.RESUMED + mainActivityScenario.state == Lifecycle.State.RESUMED } - Log.d(TAG, "πŸ”§ MainActivity is ready!") - - // Step 2: Click the "Push Notifications" button to navigate to PushNotificationTestActivity - Log.d(TAG, "πŸ”§ Step 2: Clicking 'Push Notifications' button...") val pushButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnPushNotifications")) - if (pushButton.exists()) { - pushButton.click() - Log.d(TAG, "πŸ”§ Clicked Push Notifications button successfully") - } else { - Log.e(TAG, "❌ Push Notifications button not found!") + if (!pushButton.exists()) { Assert.fail("Push Notifications button not found in MainActivity") } - - // Step 3: Wait for PushNotificationTestActivity to load - Log.d(TAG, "πŸ”§ Step 3: Waiting for PushNotificationTestActivity to load...") - Thread.sleep(2000) // Give time for navigation - - Log.d(TAG, "πŸ”§ App navigation complete: Now on PushNotificationTestActivity!") + pushButton.click() + Thread.sleep(2000) } @Test fun testPushNotificationMVP() { - // Step 1: Ensure user is signed in - Log.d(TAG, "πŸ“§ Step 1: Ensuring user is signed in...") - val userSignedIn = testUtils.ensureUserSignedIn(TestConstants.TEST_USER_EMAIL) - Assert.assertTrue("User should be signed in", userSignedIn) - Log.d(TAG, "βœ… User signed in successfully: ${TestConstants.TEST_USER_EMAIL}") + Assert.assertTrue("User should be signed in", testUtils.ensureUserSignedIn(TestConstants.TEST_USER_EMAIL)) + Assert.assertTrue("Notification permission should be granted", hasNotificationPermission()) - // Step 2: Trigger push notification campaign via API - Log.d(TAG, "🎯 Step 2: Triggering push notification campaign via API...") - Log.d(TAG, "Campaign ID: $TEST_PUSH_CAMPAIGN_ID") - Log.d(TAG, "User Email: ${TestConstants.TEST_USER_EMAIL}") + // 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) - var campaignTriggered = false - val latch = java.util.concurrent.CountDownLatch(1) + uiDevice.openNotification() + Thread.sleep(1000) + val notification1 = findNotification() + Assert.assertNotNull("Notification should be found", notification1) - triggerPushCampaignViaAPI(TEST_PUSH_CAMPAIGN_ID, TestConstants.TEST_USER_EMAIL, null) { success -> - campaignTriggered = success - Log.d(TAG, "🎯 Push campaign trigger result: $success") - if (!success) { - val errorMessage = testUtils.getLastErrorMessage() - Log.w(TAG, "⚠️ Push campaign trigger failed: $errorMessage") - } - latch.countDown() - } + notification1?.click() + Thread.sleep(2000) // Wait for app to open - // Wait for API call to complete (up to 10 seconds for CI) - val apiCallCompleted = latch.await(10, java.util.concurrent.TimeUnit.SECONDS) - Log.d(TAG, "🎯 API call completed: $apiCallCompleted, success: $campaignTriggered") + // 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() - if (!apiCallCompleted) { - Log.e(TAG, "❌ API call did not complete in time") - Assert.fail("Push campaign trigger API call did not complete in time") - return - } + // 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) - if (!campaignTriggered) { - val errorMessage = testUtils.getLastErrorMessage() - Log.e(TAG, "❌ Push campaign trigger FAILED: $errorMessage") - Log.e(TAG, "❌ Cannot proceed with test - no push notification will be available") - Assert.fail("Push campaign trigger failed: $errorMessage. Check API key and campaign configuration.") - return - } + 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) - Log.d(TAG, "βœ… Push campaign triggered successfully, waiting for notification...") + Assert.assertTrue("URL handler should be called", waitForUrlHandler(timeoutSeconds = 5)) + Assert.assertNotNull("Handled URL should not be null", getLastHandledUrl()) - // Step 3: Wait for push notification to arrive (give time for FCM delivery) - Log.d(TAG, "⏳ Step 3: Waiting for push notification to arrive...") - Thread.sleep(5000) // Give time for FCM to deliver the notification + // 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) - // Step 4: Open notification drawer and verify notification is present - Log.d(TAG, "πŸ“± Step 4: Opening notification drawer...") uiDevice.openNotification() - Thread.sleep(2000) // Wait for notification drawer to open - - // Step 5: Find and interact with the notification - Log.d(TAG, "πŸ” Step 5: Looking for push notification in notification drawer...") - - // Try to find notification by text (common notification text patterns) - var notificationFound = false - var notification = uiDevice.findObject(By.textContains("Iterable")) - ?: uiDevice.findObject(By.textContains("iterable")) - ?: uiDevice.findObject(By.textContains("Test")) - - if (notification == null) { - // Try to find any notification that might be from our app - val notifications = uiDevice.findObjects(By.res("com.android.systemui:id/notification_stack_scroller")) - if (notifications.isNotEmpty()) { - notification = notifications.first() - notificationFound = true - } - } else { - notificationFound = true + 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() } - - if (!notificationFound || notification == null) { - Log.e(TAG, "❌ Push notification not found in notification drawer") - uiDevice.pressBack() // Close notification drawer - Assert.fail("Push notification not found in notification drawer. Check FCM configuration and campaign setup.") - return + 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 } - Log.d(TAG, "βœ… Push notification found in notification drawer") - - // Step 6: Click on the notification to open it - Log.d(TAG, "🎯 Step 6: Clicking on push notification...") - notification.click() - Thread.sleep(2000) // Wait for app to open + 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) - // Step 7: Verify URL handler was called (if notification has action) - Log.d(TAG, "🎯 Step 7: Verifying URL handler was called after notification click...") - - val urlHandlerCalled = waitForUrlHandler(timeoutSeconds = 5) - if (urlHandlerCalled) { - // Step 8: Verify the correct URL was handled - val handledUrl = getLastHandledUrl() - Log.d(TAG, "🎯 URL handler received: $handledUrl") - - Assert.assertNotNull("Handled URL should not be null", handledUrl) - Log.d(TAG, "βœ… URL handler was called with URL: $handledUrl") + // 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 { - Log.d(TAG, "ℹ️ URL handler was not called - notification may not have an action URL") - // This is acceptable if the notification doesn't have a deep link action + // 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) } - - Log.d(TAG, "βœ…βœ…βœ… Test completed successfully! All steps passed.") } } diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt index 64f695936..674985665 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt @@ -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 } }) .build() diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt index 43e3d273e..55094cdf0 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/activities/PushNotificationTestActivity.kt @@ -1,18 +1,45 @@ package com.iterable.integration.tests.activities +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.util.Log +import android.widget.Button import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import com.iterable.integration.tests.R +import com.iterable.integration.tests.TestConstants +import com.iterable.integration.tests.utils.IntegrationTestUtils +import com.iterable.iterableapi.IterableApi class PushNotificationTestActivity : AppCompatActivity() { companion object { private const val TAG = "PushNotificationTest" + private const val TEST_PUSH_CAMPAIGN_ID = TestConstants.TEST_PUSH_CAMPAIGN_ID } private lateinit var statusTextView: TextView + private lateinit var permissionStatusTextView: TextView + private lateinit var triggerCampaignButton: Button + private lateinit var requestPermissionButton: Button + private lateinit var testUtils: IntegrationTestUtils + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + updatePermissionStatus() + if (isGranted) { + Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "Notification permission denied. Push notifications may not work.", Toast.LENGTH_LONG).show() + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,12 +47,133 @@ class PushNotificationTestActivity : AppCompatActivity() { Log.d(TAG, "Push Notification Test Activity started") + testUtils = IntegrationTestUtils(this) + initializeViews() + setupButtonListeners() + ensureUserSignedIn() + updatePermissionStatus() updateStatus("Ready to test push notifications") } + override fun onResume() { + super.onResume() + // Update permission status when activity resumes (in case user granted permission) + updatePermissionStatus() + } + private fun initializeViews() { statusTextView = findViewById(R.id.status_text) + permissionStatusTextView = findViewById(R.id.permission_status_text) + triggerCampaignButton = findViewById(R.id.btnTriggerPushCampaign) + requestPermissionButton = findViewById(R.id.btnRequestPermission) + } + + private fun setupButtonListeners() { + triggerCampaignButton.setOnClickListener { + triggerPushCampaign() + } + + requestPermissionButton.setOnClickListener { + requestNotificationPermission() + } + } + + private fun ensureUserSignedIn() { + val success = testUtils.ensureUserSignedIn(TestConstants.TEST_USER_EMAIL) + if (success) { + Log.d(TAG, "User signed in: ${IterableApi.getInstance().getEmail()}") + } else { + Log.e(TAG, "Failed to sign in user") + updateStatus("Failed to sign in user") + } + } + + private fun hasNotificationPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + // For Android 12 and below, check if notifications are enabled + NotificationManagerCompat.from(this).areNotificationsEnabled() + } + } + + private fun updatePermissionStatus() { + val hasPermission = hasNotificationPermission() + val statusText = if (hasPermission) { + "βœ… Notification Permission: Granted" + } else { + "❌ Notification Permission: Not Granted" + } + permissionStatusTextView.text = statusText + + // Enable/disable trigger button based on permission + triggerCampaignButton.isEnabled = hasPermission + if (!hasPermission) { + updateStatus("Please grant notification permission to test push notifications") + } + + // Show/hide request permission button based on Android version and current permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionButton.visibility = if (hasPermission) { + android.view.View.GONE + } else { + android.view.View.VISIBLE + } + } else { + // For Android 12 and below, permission is managed in system settings + requestPermissionButton.visibility = if (hasPermission) { + android.view.View.GONE + } else { + android.view.View.VISIBLE + } + requestPermissionButton.text = "Open Notification Settings" + } + } + + private fun requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + // For Android 12 and below, open notification settings + val intent = android.content.Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, packageName) + } + startActivity(intent) + } + } + + private fun triggerPushCampaign() { + // Double-check permission before triggering + if (!hasNotificationPermission()) { + updateStatus("❌ Notification permission not granted. Please grant permission first.") + Toast.makeText(this, "Notification permission required", Toast.LENGTH_SHORT).show() + updatePermissionStatus() + return + } + + updateStatus("Triggering push campaign...") + triggerCampaignButton.isEnabled = false + + testUtils.triggerPushCampaignViaAPI(TEST_PUSH_CAMPAIGN_ID, TestConstants.TEST_USER_EMAIL) { success -> + runOnUiThread { + updatePermissionStatus() // Refresh permission status + triggerCampaignButton.isEnabled = hasNotificationPermission() + if (success) { + updateStatus("βœ… Push campaign triggered successfully!\nCampaign ID: $TEST_PUSH_CAMPAIGN_ID\nCheck notification drawer in a few seconds.") + Toast.makeText(this@PushNotificationTestActivity, "Push campaign triggered successfully", Toast.LENGTH_LONG).show() + Log.d(TAG, "Push campaign triggered successfully: campaignId=$TEST_PUSH_CAMPAIGN_ID") + } else { + val errorMessage = testUtils.getLastErrorMessage() + updateStatus("❌ Failed to trigger push campaign\n${errorMessage ?: "Unknown error"}") + Toast.makeText(this@PushNotificationTestActivity, "Failed to trigger push campaign", Toast.LENGTH_SHORT).show() + Log.e(TAG, "Failed to trigger push campaign: $errorMessage") + } + } + } } private fun updateStatus(status: String) { diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/services/IntegrationFirebaseMessagingService.kt b/integration-tests/src/main/java/com/iterable/integration/tests/services/IntegrationFirebaseMessagingService.kt index 7dcf43efa..0120f215e 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/services/IntegrationFirebaseMessagingService.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/services/IntegrationFirebaseMessagingService.kt @@ -4,6 +4,7 @@ import android.util.Log import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.iterable.integration.tests.utils.IntegrationTestUtils +import com.iterable.iterableapi.IterableFirebaseMessagingService class IntegrationFirebaseMessagingService : FirebaseMessagingService() { @@ -44,10 +45,8 @@ class IntegrationFirebaseMessagingService : FirebaseMessagingService() { Log.d(TAG, "Received regular push notification") // Regular push notification - Iterable SDK will handle display IntegrationTestUtils(this).setPushNotificationReceived(true) + IterableFirebaseMessagingService.handleMessageReceived(this, remoteMessage) } - - // Let the Iterable SDK handle the message - // The SDK will automatically process the message and display notifications } override fun onMessageSent(msgId: String) { diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/utils/IntegrationTestUtils.kt b/integration-tests/src/main/java/com/iterable/integration/tests/utils/IntegrationTestUtils.kt index 7a36f1f64..71d73e449 100644 --- a/integration-tests/src/main/java/com/iterable/integration/tests/utils/IntegrationTestUtils.kt +++ b/integration-tests/src/main/java/com/iterable/integration/tests/utils/IntegrationTestUtils.kt @@ -143,7 +143,7 @@ class IntegrationTestUtils(private val context: Context) { // Store error message for UI display lastErrorMessage = "HTTP ${response.code}: $errorBody" } - + //TODO: Move callback success inside if(success) callback?.invoke(success) } catch (e: Exception) { Log.e(TAG, "Error triggering push campaign via API", e) diff --git a/integration-tests/src/main/res/layout/activity_push_notification_test.xml b/integration-tests/src/main/res/layout/activity_push_notification_test.xml index 3d11f23cc..0784a1fc5 100644 --- a/integration-tests/src/main/res/layout/activity_push_notification_test.xml +++ b/integration-tests/src/main/res/layout/activity_push_notification_test.xml @@ -20,6 +20,26 @@ android:gravity="center" android:layout_gravity="center" /> + + + + + + \ No newline at end of file