@@ -38,6 +38,11 @@ abstract class BaseIntegrationTest {
3838 private val customActionHandlerCalled = AtomicBoolean (false )
3939 private val lastHandledAction = AtomicReference < com.iterable.iterableapi.IterableAction ? > (null )
4040 private val lastHandledActionType = AtomicReference <String ?>(null )
41+
42+ // Deep link tracking for tests
43+ private val deepLinkReceived = AtomicBoolean (false )
44+ private val lastDeepLinkUrl = AtomicReference <String ?>(null )
45+ private val lastDeepLinkPath = AtomicReference <String ?>(null )
4146
4247 @Before
4348 open fun setUp () {
@@ -327,4 +332,157 @@ abstract class BaseIntegrationTest {
327332
328333 return syncHappened
329334 }
335+
336+ // ==================== Deep Link Testing Utilities ====================
337+
338+ /* *
339+ * Reset deep link tracking
340+ */
341+ protected fun resetDeepLinkTracking () {
342+ deepLinkReceived.set(false )
343+ lastDeepLinkUrl.set(null )
344+ lastDeepLinkPath.set(null )
345+ }
346+
347+ /* *
348+ * Record that a deep link was received
349+ */
350+ protected fun setDeepLinkReceived (url : String? , path : String? ) {
351+ deepLinkReceived.set(true )
352+ lastDeepLinkUrl.set(url)
353+ lastDeepLinkPath.set(path)
354+ Log .d(" BaseIntegrationTest" , " Deep link received - URL: $url , Path: $path " )
355+ }
356+
357+ /* *
358+ * Check if a deep link was received
359+ */
360+ protected fun isDeepLinkReceived (): Boolean = deepLinkReceived.get()
361+
362+ /* *
363+ * Get the last received deep link URL
364+ */
365+ protected fun getLastDeepLinkUrl (): String? = lastDeepLinkUrl.get()
366+
367+ /* *
368+ * Get the last received deep link path
369+ */
370+ protected fun getLastDeepLinkPath (): String? = lastDeepLinkPath.get()
371+
372+ /* *
373+ * Wait for deep link to be received
374+ */
375+ protected fun waitForDeepLink (timeoutSeconds : Long = TIMEOUT_SECONDS ): Boolean {
376+ return waitForCondition({
377+ deepLinkReceived.get()
378+ }, timeoutSeconds)
379+ }
380+
381+ /* *
382+ * Open a deep link URL using adb shell command.
383+ * This is the Android equivalent of iOS's `xcrun simctl openurl booted <url>`.
384+ *
385+ * The adb command `am start -a android.intent.action.VIEW -d <url>` simulates
386+ * opening a URL from an external source (like a browser or email), which triggers
387+ * the app's intent filters for deep links/app links.
388+ *
389+ * @param url The URL to open (can be https:// for App Links or custom scheme like iterable://)
390+ * @param targetPackage Optional package to target specifically (defaults to integration tests package)
391+ */
392+ protected fun openDeepLinkViaAdb (url : String , targetPackage : String = "com.iterable.integration.tests") {
393+ Log .d(" BaseIntegrationTest" , " Opening deep link via adb: $url " )
394+
395+ val instrumentation = InstrumentationRegistry .getInstrumentation()
396+
397+ // Use adb shell am start to open the URL
398+ // This mimics opening a link from an external source
399+ val command = " am start -a android.intent.action.VIEW -d \" $url \" -n $targetPackage /.activities.DeepLinkTestActivity"
400+
401+ Log .d(" BaseIntegrationTest" , " Executing command: $command " )
402+
403+ try {
404+ instrumentation.uiAutomation.executeShellCommand(command).use { parcelFileDescriptor ->
405+ // Read output if needed for debugging
406+ val inputStream = android.os.ParcelFileDescriptor .AutoCloseInputStream (parcelFileDescriptor)
407+ val output = inputStream.bufferedReader().readText()
408+ Log .d(" BaseIntegrationTest" , " Command output: $output " )
409+ }
410+ } catch (e: Exception ) {
411+ Log .e(" BaseIntegrationTest" , " Failed to execute adb command" , e)
412+ }
413+ }
414+
415+ /* *
416+ * Open a deep link URL that should be handled by the app via App Links or intent filters.
417+ * This opens the URL without specifying a target activity, letting Android's intent
418+ * resolution system determine which app/activity should handle it.
419+ *
420+ * @param url The URL to open
421+ */
422+ protected fun openDeepLinkViaIntent (url : String ) {
423+ Log .d(" BaseIntegrationTest" , " Opening deep link via intent: $url " )
424+
425+ val instrumentation = InstrumentationRegistry .getInstrumentation()
426+
427+ // Use am start without specifying a component to let Android resolve the intent
428+ val command = " am start -a android.intent.action.VIEW -d \" $url \" "
429+
430+ Log .d(" BaseIntegrationTest" , " Executing command: $command " )
431+
432+ try {
433+ instrumentation.uiAutomation.executeShellCommand(command).use { parcelFileDescriptor ->
434+ val inputStream = android.os.ParcelFileDescriptor .AutoCloseInputStream (parcelFileDescriptor)
435+ val output = inputStream.bufferedReader().readText()
436+ Log .d(" BaseIntegrationTest" , " Command output: $output " )
437+ }
438+ } catch (e: Exception ) {
439+ Log .e(" BaseIntegrationTest" , " Failed to execute adb command" , e)
440+ }
441+ }
442+
443+ /* *
444+ * Check if a specific app is in foreground
445+ *
446+ * @param packageName The package name to check
447+ * @return true if the app is in foreground
448+ */
449+ protected fun isAppInForeground (packageName : String ): Boolean {
450+ val instrumentation = InstrumentationRegistry .getInstrumentation()
451+ val command = " dumpsys activity activities | grep mResumedActivity"
452+
453+ return try {
454+ instrumentation.uiAutomation.executeShellCommand(command).use { parcelFileDescriptor ->
455+ val inputStream = android.os.ParcelFileDescriptor .AutoCloseInputStream (parcelFileDescriptor)
456+ val output = inputStream.bufferedReader().readText()
457+ Log .d(" BaseIntegrationTest" , " Foreground activity check: $output " )
458+ output.contains(packageName)
459+ }
460+ } catch (e: Exception ) {
461+ Log .e(" BaseIntegrationTest" , " Failed to check foreground app" , e)
462+ false
463+ }
464+ }
465+
466+ /* *
467+ * Open Chrome browser and navigate to a URL.
468+ * Used for testing that non-app links open in browser instead of the app.
469+ *
470+ * @param url The URL to open in Chrome
471+ */
472+ protected fun openUrlInBrowser (url : String ) {
473+ Log .d(" BaseIntegrationTest" , " Opening URL in browser: $url " )
474+
475+ val instrumentation = InstrumentationRegistry .getInstrumentation()
476+ val command = " am start -a android.intent.action.VIEW -d \" $url \" -n com.android.chrome/com.google.android.apps.chrome.Main"
477+
478+ try {
479+ instrumentation.uiAutomation.executeShellCommand(command).use { parcelFileDescriptor ->
480+ val inputStream = android.os.ParcelFileDescriptor .AutoCloseInputStream (parcelFileDescriptor)
481+ val output = inputStream.bufferedReader().readText()
482+ Log .d(" BaseIntegrationTest" , " Browser command output: $output " )
483+ }
484+ } catch (e: Exception ) {
485+ Log .e(" BaseIntegrationTest" , " Failed to open URL in browser" , e)
486+ }
487+ }
330488}
0 commit comments