diff --git a/.github/workflows/inapp-e2e-tests.yml b/.github/workflows/inapp-e2e-tests.yml new file mode 100644 index 000000000..77758f4f8 --- /dev/null +++ b/.github/workflows/inapp-e2e-tests.yml @@ -0,0 +1,220 @@ +name: In-App Message E2E Tests + +on: + push: + branches: [ InApp-Display-E2E ] + pull_request: + branches: [ InApp-Display-E2E, master, develop ] + workflow_dispatch: # Allow manual triggering + +jobs: + inapp-e2e-tests: + name: In-App Message E2E Tests + runs-on: macos-13 # Intel host → HVF works, emulator boots + + strategy: + matrix: + api-level: [34] # MVP testing on most relevant API level only + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Android SDK + uses: android-actions/setup-android@v2 + + - name: Create local.properties + run: | + echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties + echo "ndk.dir=$ANDROID_SDK_ROOT/ndk" >> local.properties + + - name: Accept Android SDK Licenses + run: | + echo "Accepting Android SDK licenses..." + yes | sdkmanager --licenses || true + echo "SDK licenses accepted" + + - name: Setup Google Services Configuration + run: | + echo "Setting up Google Services configuration for CI..." + # Ensure the google-services.json file exists for the build + if [ ! -f "integration-tests/google-services.json" ]; then + echo "Creating google-services.json from template..." + cp integration-tests/google-services.json.template integration-tests/google-services.json + fi + echo "Google Services configuration ready" + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Pre-download Gradle and Build (Parallel with Emulator) + run: | + echo "Pre-downloading Gradle and building while emulator boots..." + # Download Gradle wrapper in background + ./gradlew --version & + # Start building APKs in background + ./gradlew :integration-tests:assembleDebug :integration-tests:assembleDebugAndroidTest --no-daemon & + echo "Build started in background..." + + - name: Run UI Tests with Emulator (Intel / x86_64) + uses: ReactiveCircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: google_apis + arch: x86_64 + profile: pixel_6 + cores: 2 + ram-size: 3072M + heap-size: 576M + force-avd-creation: true + disable-animations: true + emulator-boot-timeout: 900 + emulator-options: -no-window -no-snapshot -gpu swiftshader_indirect -no-boot-anim -camera-back none -partition-size 6000 + pre-emulator-launch-script: | + # Clean + start adb after platform-tools exist (avoids tcp:5037 noise) + adb kill-server >/dev/null 2>&1 || true + adb start-server + script: | + echo "Emulator is ready! Running tests..." + echo "Setting up permissions..." + adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS + adb shell pm grant com.iterable.integration.tests android.permission.INTERNET + adb shell pm grant com.iterable.integration.tests android.permission.ACCESS_NETWORK_STATE + adb shell pm grant com.iterable.integration.tests android.permission.WAKE_LOCK + + echo "Running In-App Message MVP test..." + echo "Debug: Checking if APKs are ready..." + ls -la integration-tests/build/outputs/apk/ || echo "APK directory not found" + + echo "Debug: Verifying API keys are set..." + echo "ITERABLE_API_KEY length: ${#ITERABLE_API_KEY}" + echo "ITERABLE_SERVER_API_KEY length: ${#ITERABLE_SERVER_API_KEY}" + echo "ITERABLE_TEST_USER_EMAIL: $ITERABLE_TEST_USER_EMAIL" + + # Start logcat in background for crash debugging + adb logcat > /tmp/test-logcat.log & + LOGCAT_PID=$! + + # Run the specific test with better error handling + ./gradlew :integration-tests:connectedDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.class=com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP \ + --stacktrace --no-daemon || { + echo "Test failed! Collecting crash logs..." + kill $LOGCAT_PID 2>/dev/null || true + echo "=== CRASH LOGS ===" + tail -100 /tmp/test-logcat.log + echo "=== END CRASH LOGS ===" + exit 1 + } + + # Stop logcat + kill $LOGCAT_PID 2>/dev/null || true + env: + ITERABLE_API_KEY: ${{ secrets.BCIT_ITERABLE_API_KEY }} + ITERABLE_SERVER_API_KEY: ${{ secrets.BCIT_ITERABLE_SERVER_API_KEY }} + ITERABLE_TEST_USER_EMAIL: ${{ secrets.BCIT_ITERABLE_TEST_USER_EMAIL }} + + # - name: Generate Test Report + # if: always() + # run: | + # echo "Generating E2E test report..." + # ./gradlew :integration-tests:jacocoIntegrationTestReport + + # - name: Collect Test Logs + # if: always() + # run: | + # echo "Collecting E2E test logs..." + # adb logcat -d > integration-tests/build/e2e-test-logs.txt + + # # Also collect specific test logs + # adb logcat -d | grep -E "(InAppMessageIntegrationTest|BaseIntegrationTest|IterableApi)" > integration-tests/build/inapp-specific-logs.txt + + # - name: Take Screenshots for Debugging + # if: always() + # run: | + # echo "Taking screenshots for debugging..." + # mkdir -p integration-tests/screenshots + # adb shell screencap -p /sdcard/screenshot.png + # adb pull /sdcard/screenshot.png integration-tests/screenshots/final-state-api-${{ matrix.api-level }}.png + + # - name: Upload Test Results + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: inapp-e2e-test-results-api-${{ matrix.api-level }} + # path: | + # integration-tests/build/reports/ + # integration-tests/build/outputs/ + # integration-tests/build/e2e-test-logs.txt + # integration-tests/build/inapp-specific-logs.txt + + # - name: Upload Coverage Report + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: inapp-e2e-coverage-api-${{ matrix.api-level }} + # path: integration-tests/build/reports/jacoco/ + + # - name: Upload Screenshots + # if: always() + # uses: actions/upload-artifact@v4 + # with: + # name: inapp-e2e-screenshots-api-${{ matrix.api-level }} + # path: integration-tests/screenshots/ + + # - name: Cleanup + # if: always() + # run: | + # echo "Test cleanup completed" + + # test-summary: + # name: Test Summary + # runs-on: ubuntu-latest + # needs: inapp-e2e-tests + # if: always() + + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Download Test Results + # uses: actions/download-artifact@v4 + # with: + # name: inapp-e2e-test-results-api-34 + # path: test-results/ + + # - name: Generate Test Summary + # run: | + # echo "## In-App Message E2E Test Results" >> $GITHUB_STEP_SUMMARY + # echo "" >> $GITHUB_STEP_SUMMARY + # echo "### Test Execution Summary" >> $GITHUB_STEP_SUMMARY + # echo "- **Branch**: InApp-Display-E2E" >> $GITHUB_STEP_SUMMARY + # echo "- **API Level Tested**: 34" >> $GITHUB_STEP_SUMMARY + # echo "- **Test Method**: testInAppMessageMVP" >> $GITHUB_STEP_SUMMARY + # echo "- **Test Type**: E2E Integration Test" >> $GITHUB_STEP_SUMMARY + # echo "" >> $GITHUB_STEP_SUMMARY + # echo "### E2E Test Scenarios" >> $GITHUB_STEP_SUMMARY + # echo "- ✅ User Sign-in Verification" >> $GITHUB_STEP_SUMMARY + # echo "- ✅ In-App Message Trigger" >> $GITHUB_STEP_SUMMARY + # echo "- ✅ Message Display Verification" >> $GITHUB_STEP_SUMMARY + # echo "- ✅ Button Click Interaction" >> $GITHUB_STEP_SUMMARY + # echo "- ✅ Message Dismissal" >> $GITHUB_STEP_SUMMARY + # echo "" >> $GITHUB_STEP_SUMMARY + # echo "### Artifacts Available" >> $GITHUB_STEP_SUMMARY + # echo "- Test execution reports" >> $GITHUB_STEP_SUMMARY + # echo "- Code coverage reports" >> $GITHUB_STEP_SUMMARY + # echo "- Debug screenshots" >> $GITHUB_STEP_SUMMARY + # echo "- Detailed test logs" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 000000000..4e80b4960 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,449 @@ +name: Integration Tests + +on: + push: + branches: [ master, develop ] + paths: + - 'integration-tests/**' + - 'iterableapi/**' + - 'iterableapi-ui/**' + pull_request: + branches: [ master, develop ] + paths: + - 'integration-tests/**' + - 'iterableapi/**' + - 'iterableapi-ui/**' + schedule: + # Run integration tests daily at 2 AM UTC + - cron: '0 2 * * *' + +jobs: + integration-tests: + name: Integration Tests + runs-on: macos-latest + + strategy: + matrix: + api-level: [29] # Focus on API 29 for MVP testing + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Android SDK + uses: android-actions/setup-android@v2 + + - name: Create local.properties + run: | + echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties + echo "ndk.dir=$ANDROID_SDK_ROOT/ndk" >> local.properties + + - name: Accept Android SDK Licenses + run: | + echo "Accepting Android SDK licenses..." + yes | sdkmanager --licenses || true + echo "SDK licenses accepted" + + - name: Install Android System Images + run: | + echo "Installing Android system images for API level ${{ matrix.api-level }}..." + sdkmanager --list | grep "system-images;android-${{ matrix.api-level }}" + sdkmanager "system-images;android-${{ matrix.api-level }};google_apis;x86_64" + sdkmanager "system-images;android-${{ matrix.api-level }};google_apis;x86" + echo "System images installed successfully" + + - name: Setup Google Services Configuration + run: | + echo "Setting up Google Services configuration for CI..." + # Ensure the google-services.json file exists for the build + if [ ! -f "integration-tests/google-services.json" ]; then + echo "Creating google-services.json from template..." + cp integration-tests/google-services.json.template integration-tests/google-services.json + fi + echo "Google Services configuration ready" + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Create Android Virtual Device + run: | + echo "Creating AVD for API level ${{ matrix.api-level }}..." + + # List available system images for debugging + echo "Available system images:" + sdkmanager --list | grep "system-images;android-${{ matrix.api-level }}" || echo "No system images found for API ${{ matrix.api-level }}" + + # Try to create AVD with x86_64 first, fallback to x86 + if sdkmanager --list | grep -q "system-images;android-${{ matrix.api-level }};google_apis;x86_64"; then + echo "Creating AVD with x86_64 system image..." + echo "no" | avdmanager create avd \ + -n "test_device_api_${{ matrix.api-level }}" \ + -k "system-images;android-${{ matrix.api-level }};google_apis;x86_64" \ + -c 1536M \ + -f + elif sdkmanager --list | grep -q "system-images;android-${{ matrix.api-level }};google_apis;x86"; then + echo "Creating AVD with x86 system image..." + echo "no" | avdmanager create avd \ + -n "test_device_api_${{ matrix.api-level }}" \ + -k "system-images;android-${{ matrix.api-level }};google_apis;x86" \ + -c 1536M \ + -f + else + echo "Error: No suitable system images found for API level ${{ matrix.api-level }}" + echo "Available system images:" + sdkmanager --list | grep "system-images" | head -20 + exit 1 + fi + + echo "AVD created successfully" + + - name: Start Android Emulator + run: | + echo "Starting emulator for API level ${{ matrix.api-level }}..." + $ANDROID_SDK_ROOT/emulator/emulator \ + -avd test_device_api_${{ matrix.api-level }} \ + -no-audio \ + -no-window \ + -no-snapshot \ + -camera-back none \ + -camera-front none \ + -gpu swiftshader_indirect \ + -memory 1536 \ + -cores 1 \ + -no-boot-anim \ + -no-snapshot-load \ + -no-snapshot-save \ + -wipe-data & + echo "Emulator started in background" + + - name: Wait for emulator + run: | + echo "Waiting for emulator to be ready..." + + # Wait for device to be detected + timeout=60 + counter=0 + while ! adb devices | grep -q "emulator.*device" && [ $counter -lt $timeout ]; do + echo "Waiting for emulator to be detected... ($counter/$timeout)" + sleep 2 + counter=$((counter + 2)) + done + + if ! adb devices | grep -q "emulator.*device"; then + echo "ERROR: Emulator not detected within timeout" + adb devices + exit 1 + fi + + echo "Emulator detected, waiting for boot completion..." + + # Wait for boot completion with shorter intervals + timeout=180 # 3 minutes timeout + counter=0 + while [ "`adb shell getprop sys.boot_completed 2>/dev/null`" != "1" ] && [ $counter -lt $timeout ]; do + echo "Waiting for boot completion... ($counter/$timeout)" + sleep 3 + counter=$((counter + 3)) + + # Check if device is still online + if ! adb devices | grep -q "emulator.*device"; then + echo "ERROR: Emulator went offline during boot" + adb devices + exit 1 + fi + done + + if [ "`adb shell getprop sys.boot_completed 2>/dev/null`" != "1" ]; then + echo "ERROR: Emulator failed to boot within timeout" + echo "Device status:" + adb devices + echo "Last 20 logcat entries:" + adb logcat -d | tail -20 + exit 1 + fi + + echo "Emulator boot completed, setting up..." + + # Wait a bit for system to stabilize + sleep 5 + + # Unlock screen + adb shell input keyevent 82 + sleep 2 + adb shell input keyevent 82 + + # Disable animations for stability + adb shell settings put global window_animation_scale 0.0 + adb shell settings put global transition_animation_scale 0.0 + adb shell settings put global animator_duration_scale 0.0 + + # Additional stability settings + adb shell settings put global window_animation_scale 0 + adb shell settings put global transition_animation_scale 0 + adb shell settings put global animator_duration_scale 0 + + # Final stability wait + sleep 5 + + echo "Emulator setup complete" + + - name: Grant notification permissions + run: | + echo "Granting notification permissions..." + adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS + + - name: Verify Emulator Health + run: | + echo "Verifying emulator health before running tests..." + + # Check if emulator is responsive + adb shell echo "Emulator is responsive" + + # Check system properties + echo "Boot completed: $(adb shell getprop sys.boot_completed)" + echo "System ready: $(adb shell getprop dev.bootcomplete)" + + # Check available memory + adb shell cat /proc/meminfo | grep MemAvailable + + # Check if system is stable + adb shell getprop ro.build.version.release + adb shell getprop ro.product.model + + # Restart ADB to ensure stable connection + echo "Restarting ADB for stable connection..." + adb kill-server + adb start-server + adb wait-for-device + + # Clear any existing installations + echo "Clearing existing app installations..." + adb uninstall com.iterable.integration.tests || true + adb uninstall com.iterable.integration.tests.test || true + + echo "Emulator health check completed" + + - name: Run MVP In-App Message Test + env: + ITERABLE_API_KEY: ${{ secrets.ITERABLE_API_KEY }} + ITERABLE_SERVER_API_KEY: ${{ secrets.ITERABLE_SERVER_API_KEY }} + run: | + echo "Running MVP In-App Message test only..." + + # Check emulator status before running tests + echo "Checking emulator status..." + adb devices + adb shell getprop sys.boot_completed + + # Run the test with simplified command + cd integration-tests + ./gradlew connectedCheck \ + -Pandroid.testInstrumentationRunnerArguments.class="com.iterable.integration.tests.InAppMessageIntegrationTest#testInAppMessageMVP" \ + --info \ + --stacktrace \ + --no-daemon \ + --max-workers=1 + + - name: Generate Test Report + if: always() + run: | + echo "Generating test report..." + ./gradlew :integration-tests:jacocoIntegrationTestReport + + - name: Collect Test Logs + if: always() + run: | + echo "Collecting test logs..." + adb logcat -d > integration-tests/build/test-logs.txt + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-test-results-api-${{ matrix.api-level }} + path: | + integration-tests/build/reports/ + integration-tests/build/outputs/ + integration-tests/build/test-logs.txt + + - name: Upload Coverage Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-test-coverage-api-${{ matrix.api-level }} + path: integration-tests/build/reports/jacoco/ + + - name: Upload Screenshots (if any) + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-test-screenshots-api-${{ matrix.api-level }} + path: integration-tests/screenshots/ + + - name: Stop emulator + if: always() + run: | + echo "Stopping emulator..." + adb emu kill + + integration-tests-nightly: + name: Nightly Integration Tests + runs-on: macos-latest + if: github.event_name == 'schedule' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Set up Android SDK + uses: android-actions/setup-android@v2 + + - name: Create local.properties + run: | + echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties + echo "ndk.dir=$ANDROID_SDK_ROOT/ndk" >> local.properties + + - name: Accept Android SDK Licenses + run: | + echo "Accepting Android SDK licenses..." + yes | sdkmanager --licenses || true + echo "SDK licenses accepted" + + - name: Install Android System Images + run: | + echo "Installing Android system images for API level 34..." + sdkmanager --list | grep "system-images;android-34" + sdkmanager "system-images;android-34;google_apis;x86_64" + sdkmanager "system-images;android-34;google_apis;x86" + echo "System images installed successfully" + + - name: Setup Google Services Configuration + run: | + echo "Setting up Google Services configuration for CI..." + # Ensure the google-services.json file exists for the build + if [ ! -f "integration-tests/google-services.json" ]; then + echo "Creating google-services.json from template..." + cp integration-tests/google-services.json.template integration-tests/google-services.json + fi + echo "Google Services configuration ready" + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Create Android Virtual Device + run: | + echo "Creating AVD for nightly tests..." + + # List available system images for debugging + echo "Available system images:" + sdkmanager --list | grep "system-images;android-34" || echo "No system images found for API 34" + + # Try to create AVD with x86_64 first, fallback to x86 + if sdkmanager --list | grep -q "system-images;android-34;google_apis;x86_64"; then + echo "Creating AVD with x86_64 system image..." + echo "no" | avdmanager create avd \ + -n "nightly_test_device" \ + -k "system-images;android-34;google_apis;x86_64" \ + -c 2048M \ + -f + elif sdkmanager --list | grep -q "system-images;android-34;google_apis;x86"; then + echo "Creating AVD with x86 system image..." + echo "no" | avdmanager create avd \ + -n "nightly_test_device" \ + -k "system-images;android-34;google_apis;x86" \ + -c 2048M \ + -f + else + echo "Error: No suitable system images found for API level 34" + echo "Available system images:" + sdkmanager --list | grep "system-images" | head -20 + exit 1 + fi + + echo "AVD created successfully" + + - name: Start Android Emulator + run: | + echo "Starting emulator..." + $ANDROID_SDK_ROOT/emulator/emulator \ + -avd nightly_test_device \ + -no-audio \ + -no-window \ + -no-snapshot \ + -camera-back none \ + -camera-front none \ + -gpu swiftshader_indirect & + + - name: Wait for emulator + run: | + echo "Waiting for emulator..." + adb wait-for-device + adb shell input keyevent 82 + adb shell input keyevent 82 + + - name: Grant notification permissions + run: | + echo "Granting notification permissions..." + adb shell pm grant com.iterable.integration.tests android.permission.POST_NOTIFICATIONS + + - name: Run All Integration Tests + env: + ITERABLE_API_KEY: ${{ secrets.ITERABLE_API_KEY }} + ITERABLE_SERVER_API_KEY: ${{ secrets.ITERABLE_SERVER_API_KEY }} + run: | + echo "Running all integration tests..." + ./gradlew :integration-tests:connectedCheck --info + + - name: Generate Test Report + if: always() + run: | + echo "Generating test report..." + ./gradlew :integration-tests:jacocoIntegrationTestReport + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: nightly-integration-test-results + path: | + integration-tests/build/reports/ + integration-tests/build/outputs/ + + - name: Upload Coverage Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: nightly-integration-test-coverage + path: integration-tests/build/reports/jacoco/ + + - name: Stop emulator + if: always() + run: | + echo "Stopping emulator..." + adb emu kill \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9606a8213..3826b5eff 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ release.properties jacoco.exec # VSCode settings .vscode/ + +IDE +integration-tests/.idea/ diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle new file mode 100644 index 000000000..03416e6a6 --- /dev/null +++ b/integration-tests/build.gradle @@ -0,0 +1,158 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'jacoco' +apply plugin: 'com.google.gms.google-services' // Add Google Services plugin + +android { + compileSdk 34 + + namespace 'com.iterable.integration.tests' + testNamespace 'com.iterable.integration.tests.test' + + defaultConfig { + applicationId "com.iterable.integration.tests" + minSdkVersion 21 + targetSdkVersion 34 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true + + // Integration test specific configurations + // Read from local.properties first, then fall back to environment variables, then to defaults + def localProperties = new Properties() + def localPropertiesFile = rootProject.file('local.properties') + if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { localProperties.load(it) } + } + + def iterableApiKey = localProperties.getProperty('ITERABLE_API_KEY') ?: System.getenv('ITERABLE_API_KEY') ?: 'test_api_key' + def iterableServerApiKey = localProperties.getProperty('ITERABLE_SERVER_API_KEY') ?: System.getenv('ITERABLE_SERVER_API_KEY') ?: 'test_server_api_key' + def iterableTestUserEmail = localProperties.getProperty('ITERABLE_TEST_USER_EMAIL') ?: System.getenv('ITERABLE_TEST_USER_EMAIL') ?: 'test@abc.com' + + buildConfigField "String", "ITERABLE_API_KEY", "\"$iterableApiKey\"" + buildConfigField "String", "ITERABLE_SERVER_API_KEY", "\"$iterableServerApiKey\"" + buildConfigField "String", "ITERABLE_TEST_USER_EMAIL", "\"$iterableTestUserEmail\"" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + enableAndroidTestCoverage true + } + } + + testOptions { + unitTests.includeAndroidResources = true + animationsDisabled = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain(17) + } +} + +dependencies { + // Core Android dependencies + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.multidex:multidex:2.0.1' + implementation 'androidx.annotation:annotation:1.7.0' + implementation 'androidx.fragment:fragment:1.6.2' + + // Iterable SDK modules + implementation project(':iterableapi') + implementation project(':iterableapi-ui') + + // Firebase for push notifications (app side only) + implementation platform('com.google.firebase:firebase-bom:32.7.0') + implementation 'com.google.firebase:firebase-messaging' + implementation 'com.google.firebase:firebase-analytics' + + // Network and HTTP + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + + // Testing dependencies + testImplementation 'junit:junit:4.13.2' + testImplementation 'androidx.test:runner:1.5.2' + testImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation 'androidx.test.ext:junit:1.1.5' + testImplementation 'androidx.test:rules:1.5.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + testImplementation 'org.robolectric:robolectric:4.11.1' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' + + // Android instrumentation testing + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + androidTestImplementation 'org.mockito:mockito-android:5.3.1' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' + androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' + androidTestImplementation 'androidx.fragment:fragment-testing:1.6.2' + + // Additional testing utilities + androidTestImplementation 'com.squareup.retrofit2:retrofit-mock:2.9.0' + androidTestImplementation 'org.awaitility:awaitility:4.2.0' + androidTestImplementation 'com.google.code.gson:gson:2.8.9' +} + +// Jacoco coverage for integration tests +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + +task jacocoIntegrationTestReport(type: JacocoReport, dependsOn: ['connectedCheck']) { + group = "reporting" + description = "Generate Jacoco code coverage report for integration tests" + + reports { + xml.required = true + html.required = true + csv.required = false + } + + def fileFilter = [ + '**/*Test*.*', + '**/AutoValue_*.*', + '**/*JavascriptBridge.class', + '**/R.class', + '**/R$*.class', + '**/Manifest*.*', + 'android/**/*.*', + '**/BuildConfig.*', + '**/*$ViewBinder*.*', + '**/*$ViewInjector*.*', + '**/Lambda$*.class', + '**/Lambda.class', + '**/*Lambda.class', + '**/*Lambda*.class', + '**/*$InjectAdapter.class', + '**/*$ModuleAdapter.class', + '**/*$ViewInjector*.class', + ] + + def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.from = files([mainSrc]) + classDirectories.from = files([debugTree]) + executionData.from = fileTree(dir: "$buildDir", include: "outputs/code_coverage/debugAndroidTest/connected/**/*.ec") +} \ No newline at end of file diff --git a/integration-tests/google-services.json.template b/integration-tests/google-services.json.template new file mode 100644 index 000000000..ce67839af --- /dev/null +++ b/integration-tests/google-services.json.template @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "YOUR_PROJECT_NUMBER", + "project_id": "YOUR_FIREBASE_PROJECT_ID", + "storage_bucket": "YOUR_FIREBASE_PROJECT_ID.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "YOUR_MOBILE_SDK_APP_ID", + "android_client_info": { + "package_name": "com.iterable.integration.tests" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "YOUR_FIREBASE_API_KEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/integration-tests/proguard-rules.pro b/integration-tests/proguard-rules.pro new file mode 100644 index 000000000..dae2ca962 --- /dev/null +++ b/integration-tests/proguard-rules.pro @@ -0,0 +1,39 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# Keep Iterable SDK classes +-keep class com.iterable.iterableapi.** { *; } +-keep class com.iterable.integration.tests.** { *; } + +# Keep Firebase classes +-keep class com.google.firebase.** { *; } +-keep class com.google.android.gms.** { *; } + +# Keep OkHttp classes +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } + +# Keep Gson classes +-keep class com.google.gson.** { *; } +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer \ No newline at end of file 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 new file mode 100644 index 000000000..d7c8d5e3f --- /dev/null +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/BaseIntegrationTest.kt @@ -0,0 +1,168 @@ +package com.iterable.integration.tests + +import android.content.Context +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.iterable.iterableapi.IterableApi +import com.iterable.iterableapi.IterableConfig +import com.iterable.integration.tests.utils.IntegrationTestUtils +import com.iterable.integration.tests.TestConstants +import org.awaitility.Awaitility +import org.awaitility.core.ConditionTimeoutException +import org.junit.After +import org.junit.Before +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +@RunWith(AndroidJUnit4::class) +abstract class BaseIntegrationTest { + + companion object { + const val TIMEOUT_SECONDS = TestConstants.TIMEOUT_SECONDS + const val POLL_INTERVAL_SECONDS = TestConstants.POLL_INTERVAL_SECONDS + } + + protected lateinit var context: Context + protected lateinit var testUtils: IntegrationTestUtils + + // URL handler tracking for tests + private val urlHandlerCalled = AtomicBoolean(false) + private val lastHandledUrl = AtomicReference(null) + + @Before + open fun setUp() { + context = ApplicationProvider.getApplicationContext() + testUtils = IntegrationTestUtils(context) + + // Reset tracking flags + resetUrlHandlerTracking() + + // Set test mode flag to prevent MainActivity from initializing SDK + // This ensures our test config (with test handlers) is the one used + System.setProperty("iterable.test.mode", "true") + + // Initialize Iterable SDK for testing + initializeIterableSDK() + + // Setup test environment + setupTestEnvironment() + } + + @After + open fun tearDown() { + // Clear test mode flag + System.clearProperty("iterable.test.mode") + } + + private fun initializeIterableSDK() { + val config = IterableConfig.Builder() + .setAutoPushRegistration(true) + .setEnableEmbeddedMessaging(true) + .setLogLevel(Log.VERBOSE) + .setInAppDisplayInterval(3.0) + .setInAppHandler { message -> + // Handle in-app messages during tests + Log.d("BaseIntegrationTest", "In-app message received: ${message.messageId}") + 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 + } + .setUrlHandler { url, context -> + // Handle URLs during tests + Log.d("BaseIntegrationTest", "URL handler triggered: $url") + urlHandlerCalled.set(true) + lastHandledUrl.set(url.toString()) + true + } + .build() + + IterableApi.initialize(context, BuildConfig.ITERABLE_API_KEY, config) + + // Set the user email for integration testing + val userEmail = TestConstants.TEST_USER_EMAIL + IterableApi.getInstance().setEmail(userEmail) + Log.d("BaseIntegrationTest", "User email set to: $userEmail") + Log.d("BaseIntegrationTest", "Iterable SDK initialized with email: $userEmail") + } + + private fun setupTestEnvironment() { + // Grant notification permissions + grantNotificationPermissions() + } + + + private fun grantNotificationPermissions() { + // Grant notification permissions for Android 13+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + val instrumentation = InstrumentationRegistry.getInstrumentation() + instrumentation.uiAutomation.executeShellCommand( + "pm grant ${context.packageName} android.permission.POST_NOTIFICATIONS" + ) + } + } + + /** + * Wait for a condition to be true with timeout + */ + protected fun waitForCondition( + condition: () -> Boolean, + timeoutSeconds: Long = TIMEOUT_SECONDS + ): Boolean { + return try { + Awaitility.await() + .atMost(timeoutSeconds, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL_SECONDS, TimeUnit.SECONDS) + .until { condition() } + true + } catch (e: ConditionTimeoutException) { + false + } + } + + + /** + * Trigger a campaign via Iterable API + */ + protected fun triggerCampaignViaAPI( + campaignId: Int, + recipientEmail: String = TestConstants.TEST_USER_EMAIL, + dataFields: Map? = null, + callback: ((Boolean) -> Unit)? = null + ) { + testUtils.triggerCampaignViaAPI(campaignId, recipientEmail, dataFields, callback) + } + + + /** + * Reset URL handler tracking + */ + protected fun resetUrlHandlerTracking() { + urlHandlerCalled.set(false) + lastHandledUrl.set(null) + } + + /** + * Get the last URL handled by the URL handler + */ + protected fun getLastHandledUrl(): String? { + return lastHandledUrl.get() + } + + /** + * Wait for URL handler to be called + */ + protected fun waitForUrlHandler(timeoutSeconds: Long = TIMEOUT_SECONDS): Boolean { + return waitForCondition({ + urlHandlerCalled.get() + }, timeoutSeconds) + } +} \ No newline at end of file diff --git a/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt new file mode 100644 index 000000000..94d12127e --- /dev/null +++ b/integration-tests/src/androidTest/java/com/iterable/integration/tests/InAppMessageIntegrationTest.kt @@ -0,0 +1,279 @@ +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.espresso.web.sugar.Web +import androidx.test.espresso.web.webdriver.DriverAtoms +import androidx.test.espresso.web.webdriver.Locator +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.By +import com.iterable.iterableapi.IterableApi +import com.iterable.iterableapi.IterableInAppMessage +import com.iterable.iterableapi.IterableInAppLocation +import com.iterable.iterableapi.IterableInAppCloseAction +import com.iterable.iterableapi.IterableConfig +import com.iterable.integration.tests.activities.InAppMessageTestActivity +import com.iterable.iterableapi.IterableApiHelper +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 +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +@RunWith(AndroidJUnit4::class) +class InAppMessageIntegrationTest : BaseIntegrationTest() { + + companion object { + private const val TAG = "InAppMessageIntegrationTest" + private const val TEST_CAMPAIGN_ID = TestConstants.TEST_INAPP_CAMPAIGN_ID + private const val TEST_EVENT_NAME = "test_inapp_event" + } + + private lateinit var uiDevice: UiDevice + private lateinit var mainActivityScenario: ActivityScenario + private lateinit var inAppActivityScenario: 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") + Log.d(TAG, "🔧 MainActivity will skip initialization due to test mode flag") + + // Now launch the app flow with custom handlers already configured + launchAppAndNavigateToInAppTesting() + } + + @After + override fun tearDown() { + + super.tearDown() + } + + private fun launchAppAndNavigateToInAppTesting() { + + // 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 "In-App Messages" button to navigate to InAppMessageTestActivity + Log.d(TAG, "🔧 Step 2: Clicking 'In-App Messages' button...") + val inAppButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnInAppMessages")) + if (inAppButton.exists()) { + inAppButton.click() + Log.d(TAG, "🔧 Clicked In-App Messages button successfully") + } else { + //Take screenshot for debugging +// uiDevice.takeScreenshot(File("/sdcard/Download/InAppButtonNotFound.png")) + Log.e(TAG, "❌ In-App Messages button not found!") + Assert.fail("In-App Messages button not found in MainActivity") + } + + // Step 3: Wait for InAppMessageTestActivity to load + Log.d(TAG, "🔧 Step 3: Waiting for InAppMessageTestActivity to load...") + Thread.sleep(2000) // Give time for navigation + + Log.d(TAG, "🔧 App navigation complete: Now on InAppMessageTestActivity (same as manual flow)!") + } + + @Test + fun testInAppMessageMVP() { + Log.d(TAG, "🚀 Starting MVP in-app message test - GitHub Actions optimized") + + // 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: Debug API key configuration + Log.d(TAG, "🔍 Debug: ITERABLE_API_KEY = ${BuildConfig.ITERABLE_API_KEY}") + Log.d(TAG, "🔍 Debug: ITERABLE_SERVER_API_KEY = ${BuildConfig.ITERABLE_SERVER_API_KEY}") + Log.d(TAG, "🔍 Debug: ITERABLE_TEST_USER_EMAIL = ${BuildConfig.ITERABLE_TEST_USER_EMAIL}") + + // Step 3: Try to trigger campaign via API (but don't fail if it doesn't work) + Log.d(TAG, "🎯 Step 3: Attempting to trigger campaign via API...") + Log.d(TAG, "Campaign ID: $TEST_CAMPAIGN_ID") + Log.d(TAG, "User Email: ${TestConstants.TEST_USER_EMAIL}") + + //TODO: Check if any inapp is being displayed right now and close it if so + + //TODO: Make sure InApp messages are cleared before triggering new one + IterableApi.getInstance().inAppManager.messages.forEach { + Log.d(TAG, "Clearing existing message: ${it.messageId}") + IterableApi.getInstance().inAppManager.removeMessage(it) + } + + var campaignTriggered = false + val latch = java.util.concurrent.CountDownLatch(1) + + triggerCampaignViaAPI(TEST_CAMPAIGN_ID, TestConstants.TEST_USER_EMAIL, null) { success -> + campaignTriggered = success + Log.d(TAG, "🎯 Campaign trigger result: $success") + if (!success) { + val errorMessage = testUtils.getLastErrorMessage() + Assert.fail("Server call failed. Retry after some time") + Log.w(TAG, "⚠️ Campaign trigger failed: $errorMessage") + Log.w(TAG, "⚠️ This is expected in CI if API keys are not configured") + } + 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("Campaign trigger API call did not complete in time") + return + } + + if (!campaignTriggered) { + val errorMessage = testUtils.getLastErrorMessage() + Log.e(TAG, "❌ Campaign trigger FAILED: $errorMessage") + Log.e(TAG, "❌ Cannot proceed with test - no in-app message will be available") + Assert.fail("Campaign trigger failed: $errorMessage. Check API key and campaign configuration.") + return + } + + Log.d(TAG, "✅ Campaign triggered successfully, proceeding with message sync...") + + // Step 4: Sync messages + Log.d(TAG, "🔄 Step 4: Syncing in-app messages...") + Thread.sleep(3000) // Give time for any messages to sync + + // Manually sync + IterableApiHelper().syncInAppMessages() + + // Wait for sync to complete + Thread.sleep(2000) + + val messageCount = IterableApi.getInstance().inAppManager.messages.count() + Log.d(TAG, "🔄 Message count after sync: $messageCount") + + Assert.assertTrue( + "Message count should be 1, but was $messageCount", + messageCount == 1 + ) + + IterableApi.getInstance().inAppManager.showMessage( + IterableApi.getInstance().inAppManager.messages.first() + ) + + //wait for 3 seconds to let the inapp show + Thread.sleep(3000) + + // Get the top activity using ActivityLifecycleMonitorRegistry + var topActivity: android.app.Activity? = null + InstrumentationRegistry.getInstrumentation().runOnMainSync { + topActivity = ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + .firstOrNull() + } + + // Check if topActivity contains IterableInAppFragmentHTMLNotification + var isIterableInAppFragmentView = false + topActivity?.let { activity -> + val fragmentManager = try { + (activity as? androidx.fragment.app.FragmentActivity)?.supportFragmentManager + } catch (e: Exception) { + null + } + fragmentManager?.fragments?.forEach { fragment -> + if (fragment != null && + (fragment.javaClass.simpleName == "IterableInAppFragmentHTMLNotification" || + fragment.javaClass.canonicalName?.endsWith("IterableInAppFragmentHTMLNotification") == true) + ) { + isIterableInAppFragmentView = true + } + } + } + + Assert.assertTrue( + "Top activity should be IterableInAppFragmentHTMLNotification or contain IterableWebView", + isIterableInAppFragmentView + ) + + Log.d(TAG, "✅ In-app message is displayed, now interacting with button...") + + // Step 5: Click the "No Thanks" button in the WebView + Log.d(TAG, "🎯 Step 5: Clicking 'No Thanks' button in the in-app message...") + + // Try to find and click the "No Thanks" button with retry logic + var noThanksButton: androidx.test.uiautomator.UiObject2? = null + var attempts = 0 + val maxAttempts = 5 + + while (noThanksButton == null && attempts < maxAttempts) { + attempts++ + Log.d(TAG, "Attempt $attempts: Looking for 'No Thanks' button...") + + // Try different text variations + noThanksButton = uiDevice.findObject(By.textContains("No Thanks")) + ?: uiDevice.findObject(By.text("No Thanks")) + ?: uiDevice.findObject(By.textContains("no thanks")) + ?: uiDevice.findObject(By.textContains("NO THANKS")) + + if (noThanksButton == null) { + Log.d(TAG, "Button not found, waiting 1 second before retry...") + Thread.sleep(1000) + } + } + + if (noThanksButton != null) { + noThanksButton.click() + Log.d(TAG, "✅ Clicked 'No Thanks' button") + } else { + Assert.fail("'No Thanks' button not found in the in-app message WebView after $maxAttempts attempts") + } + + // Step 6: Verify URL handler was called + Log.d(TAG, "🎯 Step 6: Verifying URL handler was called after button click...") + + val urlHandlerCalled = waitForUrlHandler(timeoutSeconds = 5) + Assert.assertTrue( + "URL handler should have been called after clicking the button", + urlHandlerCalled + ) + + // Step 7: 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") + + Log.d(TAG, "✅✅✅ Test completed successfully! All steps passed.") + } +} \ No newline at end of file diff --git a/integration-tests/src/main/AndroidManifest.xml b/integration-tests/src/main/AndroidManifest.xml new file mode 100644 index 000000000..149cc6ab5 --- /dev/null +++ b/integration-tests/src/main/AndroidManifest.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..64f695936 --- /dev/null +++ b/integration-tests/src/main/java/com/iterable/integration/tests/MainActivity.kt @@ -0,0 +1,115 @@ +package com.iterable.integration.tests + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.iterable.iterableapi.IterableApi +import com.iterable.iterableapi.IterableConfig +import com.iterable.iterableapi.IterableUrlHandler +import com.iterable.integration.tests.activities.* +import com.iterable.integration.tests.utils.IntegrationTestUtils +import com.iterable.integration.tests.TestConstants + +class MainActivity : AppCompatActivity() { + + companion object { + private const val TAG = "IntegrationMainActivity" + const val EXTRA_DEEP_LINK_URL = "deep_link_url" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + initializeIterableSDK() + setupUI() + handleIntent(intent) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private fun initializeIterableSDK() { + try { + // Check if we're in test mode - if so, skip initialization + // The test will handle SDK initialization with custom handlers + val isTestMode = System.getProperty("iterable.test.mode") == "true" + if (isTestMode) { + Log.d(TAG, "🔧 Test mode detected - skipping MainActivity SDK initialization") + Log.d(TAG, "🔧 Test will handle SDK initialization with custom handlers") + return + } + + Log.d(TAG, "Normal mode - initializing SDK with default handlers") + + val config = IterableConfig.Builder() + .setAutoPushRegistration(true) + .setEnableEmbeddedMessaging(true) + .setInAppDisplayInterval(2.0) + .setUrlHandler(object : IterableUrlHandler { + override fun handleIterableURL(url: android.net.Uri, context: com.iterable.iterableapi.IterableActionContext): Boolean { + Log.d(TAG, "Deep link handled: $url") + // Navigate to deep link test activity + val intent = Intent(this@MainActivity, DeepLinkTestActivity::class.java) + intent.putExtra(EXTRA_DEEP_LINK_URL, url.toString()) + startActivity(intent) + return true + } + }) + .build() + + IterableApi.initialize(this, BuildConfig.ITERABLE_API_KEY, config) + + // Set the user email for integration testing + val userEmail = TestConstants.TEST_USER_EMAIL + IterableApi.getInstance().setEmail(userEmail) + + Log.d(TAG, "Iterable SDK initialized successfully with email: $userEmail") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize Iterable SDK", e) + } + } + + private fun setupUI() { + // Set API key text + findViewById(R.id.tvApiKey).text = "API Key: ${BuildConfig.ITERABLE_API_KEY}" + + findViewById(R.id.btnPushNotifications).setOnClickListener { + startActivity(Intent(this@MainActivity, PushNotificationTestActivity::class.java)) + } + + findViewById(R.id.btnInAppMessages).setOnClickListener { + startActivity(Intent(this@MainActivity, InAppMessageTestActivity::class.java)) + } + + findViewById(R.id.btnEmbeddedMessages).setOnClickListener { + startActivity(Intent(this@MainActivity, EmbeddedMessageTestActivity::class.java)) + } + + findViewById(R.id.btnDeepLinking).setOnClickListener { + startActivity(Intent(this@MainActivity, DeepLinkTestActivity::class.java)) + } + + findViewById(R.id.btnCampaignTrigger).setOnClickListener { + startActivity(Intent(this@MainActivity, CampaignTriggerTestActivity::class.java)) + } + + findViewById(R.id.btnRunAllTests).setOnClickListener { + IntegrationTestUtils(this@MainActivity).runAllIntegrationTests(this@MainActivity) + } + } + + private fun handleIntent(intent: Intent?) { + intent?.data?.let { uri -> + Log.d(TAG, "Received deep link: $uri") + // Handle deep link + val intent = Intent(this, DeepLinkTestActivity::class.java) + intent.putExtra(EXTRA_DEEP_LINK_URL, uri.toString()) + startActivity(intent) + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..28a58a480 --- /dev/null +++ b/integration-tests/src/main/java/com/iterable/integration/tests/TestConstants.kt @@ -0,0 +1,22 @@ +package com.iterable.integration.tests + +import com.iterable.integration.tests.BuildConfig + +/** + * Centralized constants for integration tests + */ +object TestConstants { + + // Test user email - centralized location for all test email references + val TEST_USER_EMAIL = BuildConfig.ITERABLE_TEST_USER_EMAIL + + // 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_EMBEDDED_CAMPAIGN_ID = 14332359 + + // Test timeouts + const val TIMEOUT_SECONDS = 5L + const val POLL_INTERVAL_SECONDS = 1L +} + diff --git a/integration-tests/src/main/java/com/iterable/integration/tests/activities/CampaignTriggerTestActivity.kt b/integration-tests/src/main/java/com/iterable/integration/tests/activities/CampaignTriggerTestActivity.kt new file mode 100644 index 000000000..1df37895b --- /dev/null +++ b/integration-tests/src/main/java/com/iterable/integration/tests/activities/CampaignTriggerTestActivity.kt @@ -0,0 +1,245 @@ +package com.iterable.integration.tests.activities + +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.iterable.integration.tests.R +import com.iterable.integration.tests.utils.IntegrationTestUtils +import com.iterable.integration.tests.TestConstants + +class CampaignTriggerTestActivity : AppCompatActivity() { + + companion object { + private const val TAG = "CampaignTriggerTestActivity" + + // Test campaign IDs - these should be configured in your Iterable project + private const val TEST_INAPP_CAMPAIGN_ID = TestConstants.TEST_INAPP_CAMPAIGN_ID + private const val TEST_PUSH_CAMPAIGN_ID = TestConstants.TEST_PUSH_CAMPAIGN_ID + private val TEST_USER_EMAIL = TestConstants.TEST_USER_EMAIL + } + + private lateinit var testUtils: IntegrationTestUtils + private lateinit var logTextView: TextView + private lateinit var campaignIdEditText: EditText + private lateinit var userEmailEditText: EditText + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_campaign_trigger_test) + + testUtils = IntegrationTestUtils(this) + + // Initialize UI components first + logTextView = findViewById(R.id.logTextView) + campaignIdEditText = findViewById(R.id.campaignIdEditText) + userEmailEditText = findViewById(R.id.userEmailEditText) + + // Set default values + campaignIdEditText.setText(TEST_INAPP_CAMPAIGN_ID.toString()) + userEmailEditText.setText(TEST_USER_EMAIL) + + // Setup button click listeners + setupButtonListeners() + + // Now ensure user is signed in (after UI is initialized) + ensureUserSignedIn() + + logMessage("Campaign Trigger Test Activity initialized") + logMessage("Default campaign ID: $TEST_INAPP_CAMPAIGN_ID") + logMessage("Default user email: $TEST_USER_EMAIL") + logMessage("User signed in: ${com.iterable.iterableapi.IterableApi.getInstance().getEmail()}") + } + + private fun ensureUserSignedIn() { + val success = testUtils.ensureUserSignedIn(TEST_USER_EMAIL) + if (success) { + logMessage("✅ User signed in successfully") + } else { + logMessage("❌ Failed to sign in user") + } + } + + private fun setupButtonListeners() { + // Test in-app campaign trigger + findViewById