From b9cb3375ec45c631b0a7d9ee2be226190bf1ae4d Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 28 Jul 2025 10:10:30 -0400 Subject: [PATCH 01/16] feat: update Kover plugin to 0.9.1 and normalize paths in coverage reports --- .github/workflows/test.yml | 11 ++++++- library/build.gradle | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 062a6706..81244083 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,16 @@ jobs: cache: gradle - name: Run Unit Tests Coverage - run: ./gradlew koverXmlReportDebug + run: ./gradlew koverXmlReportDebug -Dkover.path.normalization=relative + + - name: Normalize paths in coverage report + run: | + # Create a backup of the original report + cp library/build/reports/kover/reportDebug.xml library/build/reports/kover/reportDebug.xml.bak + + # Replace absolute paths with relative ones + # This matches any paths starting with /Users/ and replaces them with relative paths + sed -i 's|/Users/[^/]*/Code/paypal-messages-android/|./|g' library/build/reports/kover/reportDebug.xml - name: Show Coverage Report XML run: cat library/build/reports/kover/reportDebug.xml diff --git a/library/build.gradle b/library/build.gradle index 6cca607b..2842a100 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,7 +3,7 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - id 'org.jetbrains.kotlinx.kover' version '0.7.6' + id 'org.jetbrains.kotlinx.kover' version '0.9.1' } android { @@ -113,6 +113,11 @@ tasks.withType(Test).configureEach { // Example filter: excludeTestsMatching "com.paypal.messages.PayPalModalActivityTest" } +// Configure all Kover tasks to use relative paths +tasks.withType(org.jetbrains.kotlinx.kover.gradle.plugin.tasks.KoverXmlReportTask).configureEach { + systemProperty 'kover.path.normalization', 'relative' +} + // Optimized approach for handling problematic tests android { testOptions { @@ -170,6 +175,44 @@ tasks.register('testMemoryIntensiveClasses') { description = "Runs memory-intensive tests in isolated JVM processes" } +// Add a task to test the coverage report paths +tasks.register('testKoverPaths') { + dependsOn 'koverXmlReportDebug' + + doLast { + println "\n\n===== Testing Kover paths in coverage report =====\n" + + def reportFile = file("${buildDir}/reports/kover/reportDebug.xml") + if (!reportFile.exists()) { + throw new GradleException("Coverage report file not found at: ${reportFile.absolutePath}") + } + + println "Checking coverage report for absolute paths..." + + def reportContent = reportFile.text + def absolutePathPattern = "/Users/" + def hasAbsolutePaths = reportContent.contains(absolutePathPattern) + + if (hasAbsolutePaths) { + println "WARNING: Found absolute paths in the coverage report!" + println "Creating normalized version..." + + // Create a backup of the original report + def backupFile = file("${buildDir}/reports/kover/reportDebug.xml.bak") + backupFile.text = reportContent + + // Replace absolute paths with relative ones + def normalizedContent = reportContent.replaceAll("/Users/[^/]*/Code/paypal-messages-android/", "./") + reportFile.text = normalizedContent + + println "Normalized coverage report saved to: ${reportFile.absolutePath}" + } else { + println "SUCCESS: No absolute paths found in the coverage report." + } + } + description = "Tests and fixes paths in Kover coverage reports" +} + koverReport { androidReports('debug') { filters { @@ -206,6 +249,20 @@ koverReport { ) } } + + // Add explicit XML report configuration with relative paths + xml { + // Set report file to a location relative to the project + reportFile.set(layout.buildDirectory.file("reports/kover/reportDebug.xml")) + } + + // Define sources using project-relative paths + sources { + sourceDirectories.from( + file("src/main/kotlin"), + file("src/main/java") + ) + } } } From a09a0a5b26dd211f0a76aa88eb48c0944a6ba2e6 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 20 Oct 2025 14:54:47 -0400 Subject: [PATCH 02/16] feat: add manifest merger test module and CI integration - Introduced a new module `manifest-merger-test` to verify that the PayPal Messages library does not cause `android:usesCleartextTraffic` manifest merger conflicts. - Updated `.github/workflows/test.yml` to include a new CI job `test_manifest_merger` for automated testing. - Added necessary files including `build.gradle`, `AndroidManifest.xml`, and a test script to facilitate the testing process. - Updated `settings.gradle` to include the new test module. --- .github/workflows/test.yml | 56 ++++++++ manifest-merger-test/.gitignore | 1 + .../IMPLEMENTATION_SUMMARY.md | 130 ++++++++++++++++++ manifest-merger-test/README.md | 60 ++++++++ manifest-merger-test/build.gradle | 40 ++++++ .../src/main/AndroidManifest.xml | 24 ++++ .../messages/manifesttest/TestActivity.kt | 15 ++ manifest-merger-test/test-manifest-merger.sh | 53 +++++++ settings.gradle | 1 + 9 files changed, 380 insertions(+) create mode 100644 manifest-merger-test/.gitignore create mode 100644 manifest-merger-test/IMPLEMENTATION_SUMMARY.md create mode 100644 manifest-merger-test/README.md create mode 100644 manifest-merger-test/build.gradle create mode 100644 manifest-merger-test/src/main/AndroidManifest.xml create mode 100644 manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt create mode 100755 manifest-merger-test/test-manifest-merger.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b3d7782..0497fcbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -235,6 +235,62 @@ jobs: ./gradlew demo:compileDebugKotlin echo "✅ Reflection test code compiles successfully" + test_manifest_merger: + name: Manifest Merger Conflict Test + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Build Test Module with Manifest Merger + run: | + echo "🔍 Testing manifest merger with usesCleartextTraffic conflict..." + + # Build the test module that declares its own usesCleartextTraffic + # This will fail if the library's manifest causes a merger conflict + ./gradlew :manifest-merger-test:assembleDebug + + if [ $? -eq 0 ]; then + echo "✅ Manifest merger succeeded - no conflict detected!" + else + echo "❌ ERROR: Manifest merger failed - library causes usesCleartextTraffic conflict!" + exit 1 + fi + + - name: Verify Merged Manifest + run: | + echo "🔍 Verifying merged manifest content..." + + # Check that the merged manifest exists + MERGED_MANIFEST="manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml" + + if [ ! -f "$MERGED_MANIFEST" ]; then + echo "❌ ERROR: Merged manifest not found at $MERGED_MANIFEST" + exit 1 + fi + + echo "📄 Merged manifest content:" + cat "$MERGED_MANIFEST" + + # Verify that usesCleartextTraffic is present in merged manifest + if grep -q "usesCleartextTraffic" "$MERGED_MANIFEST"; then + echo "✅ usesCleartextTraffic attribute found in merged manifest" + else + echo "❌ WARNING: usesCleartextTraffic attribute not found in merged manifest" + fi + + echo "🎉 Manifest merger test completed successfully!" + test_publishing_tasks: name: Publishing Tasks Verification runs-on: ubuntu-latest diff --git a/manifest-merger-test/.gitignore b/manifest-merger-test/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/manifest-merger-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/manifest-merger-test/IMPLEMENTATION_SUMMARY.md b/manifest-merger-test/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..b699f13d --- /dev/null +++ b/manifest-merger-test/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,130 @@ +# Manifest Merger Test Implementation Summary + +## Overview + +Created a comprehensive test to verify that the PayPal Messages library doesn't cause `android:usesCleartextTraffic` manifest merger conflicts when integrated into apps. + +## What Was Created + +### 1. Test Module (`manifest-merger-test/`) + +A minimal Android application module that simulates an app integrating the library. + +**Key Files:** +- `build.gradle` - Android app configuration with library dependency +- `src/main/AndroidManifest.xml` - Declares `android:usesCleartextTraffic="false"` +- `src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt` - Minimal activity +- `README.md` - Documentation +- `test-manifest-merger.sh` - Local test script + +### 2. GitHub Actions Integration + +Added new CI job `test_manifest_merger` to `.github/workflows/test.yml` + +**What it does:** +1. Builds the test module with `./gradlew :manifest-merger-test:assembleDebug` +2. Verifies the build succeeds (no merger conflicts) +3. Examines the merged manifest at `manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml` +4. Confirms `usesCleartextTraffic` is properly merged + +### 3. Project Configuration + +Updated `settings.gradle` to include `:manifest-merger-test` module. + +## How It Works + +### The Problem +The library's manifest declares: +```xml + +``` + +When apps declare their own value: +```xml + +``` + +The Android manifest merger must resolve this conflict. + +### The Test +The test module intentionally declares `usesCleartextTraffic="false"` to simulate a real app. If the build succeeds, it means: +- No manifest merger errors occurred +- The Android build system successfully merged the attributes +- Apps can safely integrate the library + +### Expected Behavior +- ✅ Build succeeds without errors +- ✅ Merged manifest contains valid `usesCleartextTraffic` value +- ✅ No attribute conflict warnings + +## Running the Test + +### Locally +```bash +# Option 1: Use Gradle directly +./gradlew :manifest-merger-test:assembleDebug + +# Option 2: Use the test script +./manifest-merger-test/test-manifest-merger.sh + +# View merged manifest +cat manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml +``` + +### In CI/CD +The test runs automatically on every PR and push via the `test_manifest_merger` job in GitHub Actions. + +## Current Status + +**This test is expected to FAIL on the current branch.** + +There is a fix in progress on another branch that resolves the manifest merger conflict. This test will validate that the fix works correctly. + +## What Failure Looks Like (Current Branch) + +On the current branch, you'll see: +``` +❌ ERROR: Manifest merger failed - library causes usesCleartextTraffic conflict! +``` + +This indicates the library's manifest declaration is incompatible with app manifests. + +## What Success Looks Like (After Fix Branch Merged) + +When the fix is applied, the test will pass: +``` +🔍 Testing manifest merger with usesCleartextTraffic conflict... +✅ Manifest merger succeeded - no conflict detected! +📄 Merged manifest content: [shows merged XML] +✅ usesCleartextTraffic attribute found in merged manifest +🎉 Manifest merger test completed successfully! +``` + +## Files Modified + +- `/settings.gradle` - Added `:manifest-merger-test` module +- `/.github/workflows/test.yml` - Added `test_manifest_merger` job + +## Files Created + +- `/manifest-merger-test/build.gradle` +- `/manifest-merger-test/src/main/AndroidManifest.xml` +- `/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt` +- `/manifest-merger-test/.gitignore` +- `/manifest-merger-test/README.md` +- `/manifest-merger-test/test-manifest-merger.sh` +- `/manifest-merger-test/IMPLEMENTATION_SUMMARY.md` (this file) + +## Next Steps + +1. **Commit these changes** to your branch +2. **Push to GitHub** to trigger CI tests +3. **Verify the test passes** in GitHub Actions +4. **Monitor future PRs** - this test will catch any manifest merger issues + +## Additional Notes + +- The test module is minimal by design (one activity, basic manifest) +- It focuses solely on the manifest merger conflict +- The test is fast and runs in parallel with other CI jobs +- No actual app functionality is tested (only build-time merger) diff --git a/manifest-merger-test/README.md b/manifest-merger-test/README.md new file mode 100644 index 00000000..5f64efa3 --- /dev/null +++ b/manifest-merger-test/README.md @@ -0,0 +1,60 @@ +# Manifest Merger Test Module + +## Purpose + +This test module verifies that the PayPal Messages library does not cause Android Manifest merger conflicts when integrated into apps. + +## Problem Being Tested + +The library's `AndroidManifest.xml` declares: +```xml + +``` + +When an app integrates the library and also declares its own `android:usesCleartextTraffic` attribute, the Android manifest merger needs to resolve the conflict. This test ensures that: + +1. The manifest merger succeeds without errors +2. The app's declared value takes precedence (or merges correctly) +3. No build-time conflicts occur + +## Test Approach + +This module simulates a typical Android app that: +- Declares its own `android:usesCleartextTraffic="false"` in its manifest +- Integrates the PayPal Messages library as a dependency + +The CI/CD pipeline builds this module to verify no manifest merger conflicts occur. + +## Running Locally + +```bash +# Build the test module +./gradlew :manifest-merger-test:assembleDebug + +# View the merged manifest +cat manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml +``` + +## CI/CD Integration + +This test runs automatically in GitHub Actions as part of the `test.yml` workflow. + +Job: `test_manifest_merger` + +## Expected Behavior + +**Current Status:** This test is expected to **FAIL** on the current branch. + +There is a fix in progress on another branch that resolves the `usesCleartextTraffic` manifest merger conflict. Once that fix is merged, this test should pass. + +**When the fix is applied:** +- Build should succeed without manifest merger errors +- Merged manifest should contain a valid `usesCleartextTraffic` attribute +- No warnings about attribute conflicts should appear + +## Purpose + +This test validates that the manifest merger conflict has been properly resolved. It serves as: +- A regression test to prevent the issue from being reintroduced +- Validation that the fix branch correctly resolves the conflict +- Continuous verification in CI/CD diff --git a/manifest-merger-test/build.gradle b/manifest-merger-test/build.gradle new file mode 100644 index 00000000..c38db734 --- /dev/null +++ b/manifest-merger-test/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.paypal.messages.manifesttest' + compileSdk 34 + + defaultConfig { + applicationId "com.paypal.messages.manifesttest" + minSdk 23 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } +} + +dependencies { + // This simulates an app integrating the library + implementation project(':library') + + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.appcompat:appcompat:1.6.1' +} diff --git a/manifest-merger-test/src/main/AndroidManifest.xml b/manifest-merger-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..88ca031f --- /dev/null +++ b/manifest-merger-test/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt b/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt new file mode 100644 index 00000000..7816e42c --- /dev/null +++ b/manifest-merger-test/src/main/kotlin/com/paypal/messages/manifesttest/TestActivity.kt @@ -0,0 +1,15 @@ +package com.paypal.messages.manifesttest + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +/** + * Minimal test activity for manifest merger verification. + * This activity is never actually run - it exists only to ensure + * the manifest merger succeeds during build. + */ +class TestActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/manifest-merger-test/test-manifest-merger.sh b/manifest-merger-test/test-manifest-merger.sh new file mode 100755 index 00000000..7259093e --- /dev/null +++ b/manifest-merger-test/test-manifest-merger.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Test script to verify manifest merger doesn't cause conflicts +# This script builds the test module and verifies the merged manifest + +set -e + +echo "🔍 Testing manifest merger with usesCleartextTraffic conflict..." +echo "" + +# Build the test module +echo "Building manifest-merger-test module..." +./gradlew :manifest-merger-test:assembleDebug + +if [ $? -eq 0 ]; then + echo "✅ Manifest merger succeeded - no conflict detected!" +else + echo "❌ ERROR: Manifest merger failed!" + exit 1 +fi + +echo "" +echo "🔍 Verifying merged manifest content..." + +# Path to merged manifest +MERGED_MANIFEST="manifest-merger-test/build/intermediates/merged_manifests/debug/AndroidManifest.xml" + +if [ ! -f "$MERGED_MANIFEST" ]; then + echo "❌ ERROR: Merged manifest not found at $MERGED_MANIFEST" + exit 1 +fi + +echo "" +echo "📄 Merged manifest content:" +echo "---" +cat "$MERGED_MANIFEST" +echo "---" +echo "" + +# Verify usesCleartextTraffic is present +if grep -q "usesCleartextTraffic" "$MERGED_MANIFEST"; then + echo "✅ usesCleartextTraffic attribute found in merged manifest" + + # Show the exact line + echo "" + echo "📋 usesCleartextTraffic declaration:" + grep "usesCleartextTraffic" "$MERGED_MANIFEST" +else + echo "❌ WARNING: usesCleartextTraffic attribute not found in merged manifest" +fi + +echo "" +echo "🎉 Manifest merger test completed successfully!" diff --git a/settings.gradle b/settings.gradle index 539bce41..20bf845f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,3 +24,4 @@ dependencyResolutionManagement { rootProject.name = "PayPalMessages" include ':demo' include ':library' +include ':manifest-merger-test' From fdd0ffb0b4176b30c38cf9e95854ea060f646694 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 20 Oct 2025 15:02:35 -0400 Subject: [PATCH 03/16] fix: downgrade Kover plugin to 0.7.6 and adjust configuration for compatibility - Updated the Kover plugin version in `library/build.gradle` from 0.9.1 to 0.7.6. - Commented out path normalization configuration in Kover tasks for compatibility with the downgraded version. - Added Kover plugin to `settings.gradle` for proper management. --- library/build.gradle | 88 ++++++++++++++++++-------------------------- settings.gradle | 3 ++ 2 files changed, 38 insertions(+), 53 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index aa796cf1..a63ad1c4 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,7 +3,7 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - id 'org.jetbrains.kotlinx.kover' version '0.9.1' + id 'org.jetbrains.kotlinx.kover' version '0.7.6' } group = "com.paypal.messages" @@ -123,9 +123,10 @@ tasks.withType(Test).configureEach { } // Configure all Kover tasks to use relative paths -tasks.withType(org.jetbrains.kotlinx.kover.gradle.plugin.tasks.KoverXmlReportTask).configureEach { - systemProperty 'kover.path.normalization', 'relative' -} +// Commented out for Kover 0.7.6 compatibility +// tasks.withType(org.jetbrains.kotlinx.kover.gradle.plugin.tasks.KoverXmlReportTask).configureEach { +// systemProperty 'kover.path.normalization', 'relative' +// } // Optimized approach for handling problematic tests android { @@ -222,55 +223,36 @@ tasks.register('testKoverPaths') { description = "Tests and fixes paths in Kover coverage reports" } -koverReport { - androidReports('debug') { - filters { - excludes { - annotatedBy( - '*KoverExcludeGenerated' - ) - classes( - // AnalyticsLogger, - 'com.paypal.messages.analytics.AnalyticsLogger*', - // config - 'com.paypal.messages.config.message.PayPalMessageEventsCallbacks\$*', - 'com.paypal.messages.config.message.PayPalMessageViewStateCallbacks\$*', - 'com.paypal.messages.config.modal.ModalEvents\$*', - // extensions, - 'com.paypal.messages.extensions.Int*', - // io - 'com.paypal.messages.io.Api', - 'com.paypal.messages.io.Api\$*', - 'com.paypal.messages.io.LocalStorage*', - 'com.paypal.messages.io.OnActionCompleted', - // utils - 'com.paypal.messages.utils.LogCat*', - // UI Stuff - '*Fragment', - '*Fragment\$*', - '*Activity', - '*Activity\$*', - 'com.paypal.messages.PayPalMessageView*', - 'com.paypal.messages.RoundedWebView', - 'com.paypal.messages.RoundedWebView\$*', - '*.databinding.*', - '*.BuildConfig' - ) - } - } - - // Add explicit XML report configuration with relative paths - xml { - // Set report file to a location relative to the project - reportFile.set(layout.buildDirectory.file("reports/kover/reportDebug.xml")) - } - - // Define sources using project-relative paths - sources { - sourceDirectories.from( - file("src/main/kotlin"), - file("src/main/java") - ) +kover { + filters { + classes { + excludes += [ + // AnalyticsLogger, + 'com.paypal.messages.analytics.AnalyticsLogger*', + // config + 'com.paypal.messages.config.message.PayPalMessageEventsCallbacks\$*', + 'com.paypal.messages.config.message.PayPalMessageViewStateCallbacks\$*', + 'com.paypal.messages.config.modal.ModalEvents\$*', + // extensions, + 'com.paypal.messages.extensions.Int*', + // io + 'com.paypal.messages.io.Api', + 'com.paypal.messages.io.Api\$*', + 'com.paypal.messages.io.LocalStorage*', + 'com.paypal.messages.io.OnActionCompleted', + // utils + 'com.paypal.messages.utils.LogCat*', + // UI Stuff + '*Fragment', + '*Fragment\$*', + '*Activity', + '*Activity\$*', + 'com.paypal.messages.PayPalMessageView*', + 'com.paypal.messages.RoundedWebView', + 'com.paypal.messages.RoundedWebView\$*', + '*.databinding.*', + '*.BuildConfig' + ] } } } diff --git a/settings.gradle b/settings.gradle index 20bf845f..149253cc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,9 @@ pluginManagement { if (requested.id.id == 'com.vanniktech.maven.publish') { useModule("com.vanniktech:gradle-maven-publish-plugin:${requested.version}") } + if (requested.id.id == 'org.jetbrains.kotlinx.kover') { + useModule("org.jetbrains.kotlinx:kover-gradle-plugin:${requested.version}") + } } } } From bccfb9ac579fb37f88bf35505f640101bf038475 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 20 Oct 2025 15:15:23 -0400 Subject: [PATCH 04/16] test: enhance Logo and PayPal composable tests with various scenarios - Added comprehensive unit tests for `LogoAsset` to verify asset creation, equality, and type hierarchy. - Implemented tests for `PayPalMessageConfig` and `ModalConfig` to ensure proper handling of null values, default parameters, and various offer types. - Included tests for callback invocations in `PayPalComposableMessageTest` and `PayPalComposableModalTest` to validate event handling. - Enhanced coverage for edge cases, including different environments and amounts in modal configurations. --- .../java/com/paypal/messages/LogoAssetTest.kt | 149 ++++++++++ .../test/java/com/paypal/messages/LogoTest.kt | 129 +++++++++ .../messages/PayPalComposableMessageTest.kt | 248 ++++++++++++++++ .../messages/PayPalComposableModalTest.kt | 270 ++++++++++++++++++ 4 files changed, 796 insertions(+) diff --git a/library/src/test/java/com/paypal/messages/LogoAssetTest.kt b/library/src/test/java/com/paypal/messages/LogoAssetTest.kt index e643fc17..dc729f92 100644 --- a/library/src/test/java/com/paypal/messages/LogoAssetTest.kt +++ b/library/src/test/java/com/paypal/messages/LogoAssetTest.kt @@ -1,6 +1,8 @@ package com.paypal.messages import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class LogoAssetTest { @@ -21,4 +23,151 @@ class LogoAssetTest { assertEquals(resId, imageAsset.resId) } + + @Test + fun testStringAssetWithDifferentResIds() { + // Test that different string resources create different assets + val creditAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val defaultAsset = LogoAsset.StringAsset(R.string.logo_none_label_default) + + assertNotEquals(creditAsset.resId, defaultAsset.resId) + } + + @Test + fun testImageAssetWithDifferentResIds() { + // Test that different drawable resources create different assets + val standardAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val whiteAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_white) + + assertNotEquals(standardAsset.resId, whiteAsset.resId) + } + + @Test + fun testStringAssetEquality() { + // Test that StringAssets with the same resId are equal + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + + assertEquals(asset1, asset2) + assertEquals(asset1.resId, asset2.resId) + } + + @Test + fun testImageAssetEquality() { + // Test that ImageAssets with the same resId are equal + val asset1 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val asset2 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + assertEquals(asset1, asset2) + assertEquals(asset1.resId, asset2.resId) + } + + @Test + fun testStringAssetInequality() { + // Test that StringAssets with different resIds are not equal + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_default) + + assertNotEquals(asset1, asset2) + } + + @Test + fun testImageAssetInequality() { + // Test that ImageAssets with different resIds are not equal + val asset1 = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val asset2 = LogoAsset.ImageAsset(R.drawable.logo_primary_white) + + assertNotEquals(asset1, asset2) + } + + @Test + fun testStringAssetIsLogoAsset() { + // Test that StringAsset is a subclass of LogoAsset + val stringAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + assertTrue(stringAsset is LogoAsset) + } + + @Test + fun testImageAssetIsLogoAsset() { + // Test that ImageAsset is a subclass of LogoAsset + val imageAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + assertTrue(imageAsset is LogoAsset) + } + + @Test + fun testSealedClassHierarchy() { + // Test that both StringAsset and ImageAsset extend LogoAsset + val stringAsset: LogoAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset: LogoAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + // Verify type hierarchy + assertTrue(stringAsset is LogoAsset.StringAsset) + assertTrue(imageAsset is LogoAsset.ImageAsset) + } + + @Test + fun testWhenExpressionCoverage() { + // Test that sealed class works with when expressions + val stringAsset: LogoAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset: LogoAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + val stringResult = when (stringAsset) { + is LogoAsset.StringAsset -> "string" + is LogoAsset.ImageAsset -> "image" + } + + val imageResult = when (imageAsset) { + is LogoAsset.StringAsset -> "string" + is LogoAsset.ImageAsset -> "image" + } + + assertEquals("string", stringResult) + assertEquals("image", imageResult) + } + + @Test + fun testCopyOfDataClass() { + // Test that data class copy works for StringAsset + val original = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val copied = original.copy(resId = R.string.logo_none_label_default) + + assertNotEquals(original, copied) + assertEquals(R.string.logo_none_label_credit, original.resId) + assertEquals(R.string.logo_none_label_default, copied.resId) + } + + @Test + fun testCopyOfImageDataClass() { + // Test that data class copy works for ImageAsset + val original = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + val copied = original.copy(resId = R.drawable.logo_primary_white) + + assertNotEquals(original, copied) + assertEquals(R.drawable.logo_primary_standard, original.resId) + assertEquals(R.drawable.logo_primary_white, copied.resId) + } + + @Test + fun testHashCodeConsistency() { + // Test that equal objects have the same hash code + val asset1 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val asset2 = LogoAsset.StringAsset(R.string.logo_none_label_credit) + + assertEquals(asset1.hashCode(), asset2.hashCode()) + } + + @Test + fun testToStringOutput() { + // Test that toString provides useful output + val stringAsset = LogoAsset.StringAsset(R.string.logo_none_label_credit) + val imageAsset = LogoAsset.ImageAsset(R.drawable.logo_primary_standard) + + val stringOutput = stringAsset.toString() + val imageOutput = imageAsset.toString() + + assertTrue(stringOutput.contains("StringAsset")) + assertTrue(stringOutput.contains("resId")) + assertTrue(imageOutput.contains("ImageAsset")) + assertTrue(imageOutput.contains("resId")) + } } diff --git a/library/src/test/java/com/paypal/messages/LogoTest.kt b/library/src/test/java/com/paypal/messages/LogoTest.kt index 21dc5e3a..ac0d9e72 100644 --- a/library/src/test/java/com/paypal/messages/LogoTest.kt +++ b/library/src/test/java/com/paypal/messages/LogoTest.kt @@ -222,4 +222,133 @@ class LogoTest { Arguments.of(LogoType.NONE, null, R.string.logo_none_label_default), ) } + + @org.junit.jupiter.api.Test + fun testLogoDefaultConstructor() { + // Test that Logo uses defaults when no parameters provided + val logo = Logo() + val asset = logo.getAsset(Color.BLACK) + + // Should default to PRIMARY logo with PAY_LATER product group + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_primary_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testLogoWithOnlyLogoType() { + // Test Logo with only logoType specified + val logo = Logo(logoType = LogoType.ALTERNATIVE) + val asset = logo.getAsset(Color.BLACK) + + // Should use ALTERNATIVE with default PAY_LATER + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_alternative_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testLogoWithOnlyProductGroup() { + // Test Logo with only productGroup specified + val logo = Logo(productGroup = ProductGroup.PAYPAL_CREDIT) + val asset = logo.getAsset(Color.BLACK) + + // Should use default PRIMARY with PAYPAL_CREDIT + assertEquals(LogoAsset.ImageAsset(R.drawable.logo_credit_primary_standard), asset) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForPrimary() { + // Ensure all color variants work for primary logo + val logo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_primary_standard, blackAsset.resId) + assertEquals(R.drawable.logo_primary_white, whiteAsset.resId) + assertEquals(R.drawable.logo_primary_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_primary_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForAlternative() { + // Ensure all color variants work for alternative logo + val logo = Logo(LogoType.ALTERNATIVE, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_alternative_standard, blackAsset.resId) + assertEquals(R.drawable.logo_alternative_white, whiteAsset.resId) + assertEquals(R.drawable.logo_alternative_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_alternative_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testAllColorVariantsForInline() { + // Ensure all color variants work for inline logo + val logo = Logo(LogoType.INLINE, ProductGroup.PAY_LATER) + + val blackAsset = logo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val whiteAsset = logo.getAsset(Color.WHITE) as LogoAsset.ImageAsset + val monochromeAsset = logo.getAsset(Color.MONOCHROME) as LogoAsset.ImageAsset + val grayscaleAsset = logo.getAsset(Color.GRAYSCALE) as LogoAsset.ImageAsset + + assertEquals(R.drawable.logo_inline_standard, blackAsset.resId) + assertEquals(R.drawable.logo_inline_white, whiteAsset.resId) + assertEquals(R.drawable.logo_inline_monochrome, monochromeAsset.resId) + assertEquals(R.drawable.logo_inline_grayscale, grayscaleAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testNoneLogoReturnsStringAsset() { + // Test that NONE logo type returns StringAsset, not ImageAsset + val logo = Logo(LogoType.NONE, ProductGroup.PAY_LATER) + val asset = logo.getAsset(Color.BLACK) + + // Verify it's a StringAsset + assertTrue(asset is LogoAsset.StringAsset) + assertEquals(R.string.logo_none_label_default, (asset as LogoAsset.StringAsset).resId) + } + + @org.junit.jupiter.api.Test + fun testImageLogoTypesReturnImageAsset() { + // Test that non-NONE logo types return ImageAsset + val logoTypes = listOf(LogoType.PRIMARY, LogoType.ALTERNATIVE, LogoType.INLINE) + + logoTypes.forEach { logoType -> + val logo = Logo(logoType, ProductGroup.PAY_LATER) + val asset = logo.getAsset(Color.BLACK) + + assertTrue(asset is LogoAsset.ImageAsset, "Logo type $logoType should return ImageAsset") + } + } + + @org.junit.jupiter.api.Test + fun testCreditVsPayLaterLogos() { + // Test that PAYPAL_CREDIT and PAY_LATER use different logos + val creditLogo = Logo(LogoType.PRIMARY, ProductGroup.PAYPAL_CREDIT) + val payLaterLogo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val creditAsset = creditLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val payLaterAsset = payLaterLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + + // Should be different resource IDs + assertTrue(creditAsset.resId != payLaterAsset.resId) + assertEquals(R.drawable.logo_credit_primary_standard, creditAsset.resId) + assertEquals(R.drawable.logo_primary_standard, payLaterAsset.resId) + } + + @org.junit.jupiter.api.Test + fun testNullProductGroupDefaultsToPayLater() { + // Test that null product group behaves like PAY_LATER + val nullProductLogo = Logo(LogoType.PRIMARY, null) + val payLaterLogo = Logo(LogoType.PRIMARY, ProductGroup.PAY_LATER) + + val nullAsset = nullProductLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + val payLaterAsset = payLaterLogo.getAsset(Color.BLACK) as LogoAsset.ImageAsset + + assertEquals(payLaterAsset.resId, nullAsset.resId) + } } diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt index 8fe16618..4bb03793 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt @@ -168,6 +168,254 @@ class PayPalComposableMessageTest { // This directly compares the error objects rather than just the message substring } + @Test + fun testLoadingCallback() { + // Arrange + val clientId = "test-client-id" + var loadingInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + viewStateCallbacks = PayPalMessageViewStateCallbacks( + onLoading = { loadingInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke loading callback + val messageView = createPayPalMessageView(mockContext, config) + config.viewStateCallbacks?.onLoading?.invoke() + + // Assert + assertEquals(true, loadingInvoked) + } + + @Test + fun testSuccessCallback() { + // Arrange + val clientId = "test-client-id" + var successInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + viewStateCallbacks = PayPalMessageViewStateCallbacks( + onSuccess = { successInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke success callback + val messageView = createPayPalMessageView(mockContext, config) + config.viewStateCallbacks?.onSuccess?.invoke() + + // Assert + assertEquals(true, successInvoked) + } + + @Test + fun testOnClickCallback() { + // Arrange + val clientId = "test-client-id" + var clickInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + eventsCallbacks = PayPalMessageEventsCallbacks( + onClick = { clickInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke click callback + val messageView = createPayPalMessageView(mockContext, config) + config.eventsCallbacks?.onClick?.invoke() + + // Assert + assertEquals(true, clickInvoked) + } + + @Test + fun testOnApplyCallback() { + // Arrange + val clientId = "test-client-id" + var applyInvoked = false + + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = clientId, + environment = PayPalEnvironment.LIVE, + ), + eventsCallbacks = PayPalMessageEventsCallbacks( + onApply = { applyInvoked = true }, + ), + ) + + // Act - create PayPalMessageView and invoke apply callback + val messageView = createPayPalMessageView(mockContext, config) + config.eventsCallbacks?.onApply?.invoke() + + // Assert + assertEquals(true, applyInvoked) + } + + @Test + fun testDifferentEnvironments() { + // Test SANDBOX environment + val sandboxConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.SANDBOX, + ), + ) + val sandboxView = createPayPalMessageView(mockContext, sandboxConfig) + assertNotNull(sandboxView) + + // Test STAGE environment + val stageConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.STAGE, + ), + ) + val stageView = createPayPalMessageView(mockContext, stageConfig) + assertNotNull(stageView) + + // Test LOCAL environment + val localConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LOCAL, + ), + ) + val localView = createPayPalMessageView(mockContext, localConfig) + assertNotNull(localView) + } + + @Test + fun testVariousOfferTypes() { + // Test PAY_LATER_SHORT_TERM + val shortTermConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_SHORT_TERM, + ), + ) + val shortTermView = createPayPalMessageView(mockContext, shortTermConfig) + assertNotNull(shortTermView) + assertEquals(PayPalMessageOfferType.PAY_LATER_SHORT_TERM, shortTermConfig.data.offerType) + + // Test PAY_LATER_LONG_TERM + val longTermConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_LONG_TERM, + ), + ) + val longTermView = createPayPalMessageView(mockContext, longTermConfig) + assertNotNull(longTermView) + assertEquals(PayPalMessageOfferType.PAY_LATER_LONG_TERM, longTermConfig.data.offerType) + + // Test PAY_LATER_PAY_IN_1 + val payIn1Config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + offerType = PayPalMessageOfferType.PAY_LATER_PAY_IN_1, + ), + ) + val payIn1View = createPayPalMessageView(mockContext, payIn1Config) + assertNotNull(payIn1View) + assertEquals(PayPalMessageOfferType.PAY_LATER_PAY_IN_1, payIn1Config.data.offerType) + } + + @Test + fun testNullOptionalParameters() { + // Test with all optional parameters set to null + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = null, + buyerCountry = null, + offerType = null, + ), + viewStateCallbacks = null, + eventsCallbacks = null, + ) + + val messageView = createPayPalMessageView(mockContext, config) + assertNotNull(messageView) + assertEquals(null, config.data.amount) + assertEquals(null, config.data.buyerCountry) + assertEquals(null, config.data.offerType) + } + + @Test + fun testEdgeCaseAmounts() { + // Test with zero amount + val zeroAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 0.0, + ), + ) + val zeroView = createPayPalMessageView(mockContext, zeroAmountConfig) + assertNotNull(zeroView) + assertEquals(0.0, zeroAmountConfig.data.amount) + + // Test with large amount + val largeAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 999999.99, + ), + ) + val largeView = createPayPalMessageView(mockContext, largeAmountConfig) + assertNotNull(largeView) + assertEquals(999999.99, largeAmountConfig.data.amount) + + // Test with small decimal amount + val smallAmountConfig = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + amount = 0.01, + ), + ) + val smallView = createPayPalMessageView(mockContext, smallAmountConfig) + assertNotNull(smallView) + assertEquals(0.01, smallAmountConfig.data.amount) + } + + @Test + fun testVariousBuyerCountries() { + // Test common country codes + val countries = listOf("US", "GB", "DE", "FR", "CA", "AU", "JP") + + countries.forEach { country -> + val config = PayPalMessageConfig( + data = PayPalMessageData( + clientID = "test-client-id", + environment = PayPalEnvironment.LIVE, + buyerCountry = country, + ), + ) + val view = createPayPalMessageView(mockContext, config) + assertNotNull(view) + assertEquals(country, config.data.buyerCountry) + } + } + // Note: The following tests would need to be run as instrumented tests rather than unit tests // as they require the Android runtime. For unit test coverage, we're focusing on testing // the configuration creation which is the core logic of the composable. diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt index 57081682..ded7f97c 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt @@ -95,4 +95,274 @@ class PayPalComposableModalTest { // Test that the client can be created without errors assertNotNull(testWebViewClient) } + + @Test + fun testModalConfigWithNullValues() { + // Test that we can create a ModalConfig with null values + val config = ModalConfig( + amount = null, + buyerCountry = null, + offer = null, + modalCloseButton = ModalCloseButton(), + ) + + // Verify the config properties + assertNull(config.amount) + assertNull(config.buyerCountry) + assertNull(config.offer) + assertNotNull(config.modalCloseButton) + } + + @Test + fun testModalConfigDefaults() { + // Test that we can create a ModalConfig with default values + val config = ModalConfig() + + // Verify default values + assertNull(config.amount) + assertNull(config.buyerCountry) + assertNull(config.offer) + assertEquals(false, config.ignoreCache) + assertEquals(false, config.devTouchpoint) + assertNull(config.stageTag) + assertNotNull(config.modalCloseButton) + } + + @Test + fun testModalConfigWithAllParameters() { + // Test that we can create a ModalConfig with all parameters + var applyCalled = false + var clickCalled = false + var errorCalled = false + var loadingCalled = false + var successCalled = false + + val events = ModalEvents( + onApply = { applyCalled = true }, + onClick = { clickCalled = true }, + onError = { errorCalled = true }, + onLoading = { loadingCalled = true }, + onSuccess = { successCalled = true }, + ) + + val config = ModalConfig( + amount = 250.50, + buyerCountry = "GB", + offer = PayPalMessageOfferType.PAY_LATER_LONG_TERM, + ignoreCache = true, + devTouchpoint = true, + stageTag = "test-stage", + events = events, + modalCloseButton = ModalCloseButton(alternativeText = "Custom Close"), + ) + + // Verify all properties + assertEquals(250.50, config.amount) + assertEquals("GB", config.buyerCountry) + assertEquals(PayPalMessageOfferType.PAY_LATER_LONG_TERM, config.offer) + assertEquals(true, config.ignoreCache) + assertEquals(true, config.devTouchpoint) + assertEquals("test-stage", config.stageTag) + assertEquals("Custom Close", config.modalCloseButton.alternativeText) + + // Test events still work + config.events?.onApply?.invoke() + assertTrue(applyCalled) + } + + @Test + fun testModalCloseButtonDefaults() { + // Test default ModalCloseButton + val closeButton = ModalCloseButton() + + // Verify defaults + assertNull(closeButton.alternativeText) + } + + @Test + fun testModalCloseButtonWithText() { + // Test ModalCloseButton with custom text + val closeButton = ModalCloseButton(alternativeText = "Dismiss Modal") + + // Verify custom text + assertEquals("Dismiss Modal", closeButton.alternativeText) + } + + @Test + fun testDifferentOfferTypes() { + // Test all offer types can be set in config + val offerTypes = listOf( + PayPalMessageOfferType.PAY_LATER_SHORT_TERM, + PayPalMessageOfferType.PAY_LATER_LONG_TERM, + PayPalMessageOfferType.PAY_LATER_PAY_IN_1, + null, + ) + + offerTypes.forEach { offerType -> + val config = ModalConfig( + amount = 100.0, + offer = offerType, + ) + assertEquals(offerType, config.offer) + } + } + + @Test + fun testEdgeCaseAmountsInModal() { + // Test zero amount + val zeroConfig = ModalConfig(amount = 0.0) + assertEquals(0.0, zeroConfig.amount) + + // Test very large amount + val largeConfig = ModalConfig(amount = 1000000.0) + assertEquals(1000000.0, largeConfig.amount) + + // Test small decimal + val smallConfig = ModalConfig(amount = 0.01) + assertEquals(0.01, smallConfig.amount) + + // Test negative amount (should still be accepted, validation is elsewhere) + val negativeConfig = ModalConfig(amount = -10.0) + assertEquals(-10.0, negativeConfig.amount) + } + + @Test + fun testVariousBuyerCountriesInModal() { + // Test common country codes + val countries = listOf("US", "GB", "DE", "FR", "CA", "AU", "JP", "IT", "ES") + + countries.forEach { country -> + val config = ModalConfig( + amount = 100.0, + buyerCountry = country, + ) + assertEquals(country, config.buyerCountry) + } + } + + @Test + fun testModalEventsIndependence() { + // Test that each event can be called independently + var applyCalled = false + var clickCalled = false + var errorCalled = false + var loadingCalled = false + var successCalled = false + + val events = ModalEvents( + onApply = { applyCalled = true }, + onClick = { clickCalled = true }, + onError = { errorCalled = true }, + onLoading = { loadingCalled = true }, + onSuccess = { successCalled = true }, + ) + + // Call only onApply + events.onApply() + assertTrue(applyCalled) + assertTrue(!clickCalled && !errorCalled && !loadingCalled && !successCalled) + + // Reset and call only onClick + applyCalled = false + events.onClick() + assertTrue(clickCalled) + assertTrue(!applyCalled && !errorCalled && !loadingCalled && !successCalled) + } + + @Test + fun testModalEventsWithNullCallbacks() { + // Test that ModalEvents can be created with minimal callbacks + val minimalEvents = ModalEvents() + + // These should be no-ops if not defined + assertNotNull(minimalEvents) + // Calling these shouldn't throw exceptions + minimalEvents.onApply() + minimalEvents.onClick() + minimalEvents.onLoading() + minimalEvents.onSuccess() + } + + @Test + fun testErrorWithDifferentErrorTypes() { + // Test that different error types can be passed to onError + var capturedError: PayPalErrors.Base? = null + + val events = ModalEvents( + onError = { error -> capturedError = error }, + ) + + // Test with base error + val baseError = PayPalErrors.Base("Base error") + events.onError(baseError) + assertEquals(baseError, capturedError) + + // Test with modal failed to load error + val modalError = PayPalErrors.ModalFailedToLoad("Modal error", null) + events.onError(modalError) + assertEquals(modalError, capturedError) + + // Test with invalid client id error + val clientIdError = PayPalErrors.InvalidClientId("test-id", null) + events.onError(clientIdError) + assertEquals(clientIdError, capturedError) + } + + @Test + fun testCacheBehavior() { + // Test ignoreCache = true + val noCacheConfig = ModalConfig( + amount = 100.0, + ignoreCache = true, + ) + assertTrue(noCacheConfig.ignoreCache) + + // Test ignoreCache = false (default) + val withCacheConfig = ModalConfig( + amount = 100.0, + ignoreCache = false, + ) + assertTrue(!withCacheConfig.ignoreCache) + } + + @Test + fun testDevTouchpointFlag() { + // Test devTouchpoint = true + val devConfig = ModalConfig( + amount = 100.0, + devTouchpoint = true, + ) + assertTrue(devConfig.devTouchpoint) + + // Test devTouchpoint = false (default) + val prodConfig = ModalConfig( + amount = 100.0, + devTouchpoint = false, + ) + assertTrue(!prodConfig.devTouchpoint) + } + + @Test + fun testStageTagValues() { + // Test with stage tag + val stagedConfig = ModalConfig( + amount = 100.0, + stageTag = "v1.2.3", + ) + assertEquals("v1.2.3", stagedConfig.stageTag) + + // Test without stage tag + val unstaged = ModalConfig( + amount = 100.0, + stageTag = null, + ) + assertNull(unstaged.stageTag) + + // Test with empty stage tag + val emptyStageConfig = ModalConfig( + amount = 100.0, + stageTag = "", + ) + assertEquals("", emptyStageConfig.stageTag) + } } From 1b1393f82e8291a14b6e011e4eab48a7b9689cd1 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 20 Oct 2025 15:22:09 -0400 Subject: [PATCH 05/16] test: enhance PayPal composable tests with additional configurations and assertions - Updated `PayPalComposableMessageTest` to test the DEVELOP environment with host and localhost configurations. - Enhanced `PayPalComposableModalTest` by adding `modalCloseButton` to various `ModalConfig` instances and updating assertions for edge cases. - Improved error handling tests by changing `InvalidClientId` to `InvalidClientIdException` for better clarity. - Added assertions to verify default values and configurations in modal tests. --- .../test/java/com/paypal/messages/LogoTest.kt | 1 + .../messages/PayPalComposableMessageTest.kt | 14 +++++------ .../messages/PayPalComposableModalTest.kt | 25 +++++++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/library/src/test/java/com/paypal/messages/LogoTest.kt b/library/src/test/java/com/paypal/messages/LogoTest.kt index ac0d9e72..e9a8cd40 100644 --- a/library/src/test/java/com/paypal/messages/LogoTest.kt +++ b/library/src/test/java/com/paypal/messages/LogoTest.kt @@ -2,6 +2,7 @@ package com.paypal.messages import com.paypal.messages.config.ProductGroup import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt index 4bb03793..111240d4 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableMessageTest.kt @@ -276,21 +276,21 @@ class PayPalComposableMessageTest { val sandboxView = createPayPalMessageView(mockContext, sandboxConfig) assertNotNull(sandboxView) - // Test STAGE environment - val stageConfig = PayPalMessageConfig( + // Test DEVELOP environment with host + val developConfig = PayPalMessageConfig( data = PayPalMessageData( clientID = "test-client-id", - environment = PayPalEnvironment.STAGE, + environment = PayPalEnvironment.DEVELOP("test.paypal.com"), ), ) - val stageView = createPayPalMessageView(mockContext, stageConfig) - assertNotNull(stageView) + val developView = createPayPalMessageView(mockContext, developConfig) + assertNotNull(developView) - // Test LOCAL environment + // Test DEVELOP environment with localhost val localConfig = PayPalMessageConfig( data = PayPalMessageData( clientID = "test-client-id", - environment = PayPalEnvironment.LOCAL, + environment = PayPalEnvironment.DEVELOP(8443), ), ) val localView = createPayPalMessageView(mockContext, localConfig) diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt index ded7f97c..5535fe46 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt @@ -116,7 +116,9 @@ class PayPalComposableModalTest { @Test fun testModalConfigDefaults() { // Test that we can create a ModalConfig with default values - val config = ModalConfig() + val config = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) // Verify default values assertNull(config.amount) @@ -202,6 +204,7 @@ class PayPalComposableModalTest { val config = ModalConfig( amount = 100.0, offer = offerType, + modalCloseButton = ModalCloseButton(), ) assertEquals(offerType, config.offer) } @@ -210,19 +213,19 @@ class PayPalComposableModalTest { @Test fun testEdgeCaseAmountsInModal() { // Test zero amount - val zeroConfig = ModalConfig(amount = 0.0) + val zeroConfig = ModalConfig(amount = 0.0, modalCloseButton = ModalCloseButton()) assertEquals(0.0, zeroConfig.amount) // Test very large amount - val largeConfig = ModalConfig(amount = 1000000.0) + val largeConfig = ModalConfig(amount = 1000000.0, modalCloseButton = ModalCloseButton()) assertEquals(1000000.0, largeConfig.amount) // Test small decimal - val smallConfig = ModalConfig(amount = 0.01) + val smallConfig = ModalConfig(amount = 0.01, modalCloseButton = ModalCloseButton()) assertEquals(0.01, smallConfig.amount) // Test negative amount (should still be accepted, validation is elsewhere) - val negativeConfig = ModalConfig(amount = -10.0) + val negativeConfig = ModalConfig(amount = -10.0, modalCloseButton = ModalCloseButton()) assertEquals(-10.0, negativeConfig.amount) } @@ -235,6 +238,7 @@ class PayPalComposableModalTest { val config = ModalConfig( amount = 100.0, buyerCountry = country, + modalCloseButton = ModalCloseButton(), ) assertEquals(country, config.buyerCountry) } @@ -302,8 +306,8 @@ class PayPalComposableModalTest { events.onError(modalError) assertEquals(modalError, capturedError) - // Test with invalid client id error - val clientIdError = PayPalErrors.InvalidClientId("test-id", null) + // Test with invalid client id exception + val clientIdError = PayPalErrors.InvalidClientIdException("Invalid ID", null) events.onError(clientIdError) assertEquals(clientIdError, capturedError) } @@ -314,6 +318,7 @@ class PayPalComposableModalTest { val noCacheConfig = ModalConfig( amount = 100.0, ignoreCache = true, + modalCloseButton = ModalCloseButton(), ) assertTrue(noCacheConfig.ignoreCache) @@ -321,6 +326,7 @@ class PayPalComposableModalTest { val withCacheConfig = ModalConfig( amount = 100.0, ignoreCache = false, + modalCloseButton = ModalCloseButton(), ) assertTrue(!withCacheConfig.ignoreCache) } @@ -331,6 +337,7 @@ class PayPalComposableModalTest { val devConfig = ModalConfig( amount = 100.0, devTouchpoint = true, + modalCloseButton = ModalCloseButton(), ) assertTrue(devConfig.devTouchpoint) @@ -338,6 +345,7 @@ class PayPalComposableModalTest { val prodConfig = ModalConfig( amount = 100.0, devTouchpoint = false, + modalCloseButton = ModalCloseButton(), ) assertTrue(!prodConfig.devTouchpoint) } @@ -348,6 +356,7 @@ class PayPalComposableModalTest { val stagedConfig = ModalConfig( amount = 100.0, stageTag = "v1.2.3", + modalCloseButton = ModalCloseButton(), ) assertEquals("v1.2.3", stagedConfig.stageTag) @@ -355,6 +364,7 @@ class PayPalComposableModalTest { val unstaged = ModalConfig( amount = 100.0, stageTag = null, + modalCloseButton = ModalCloseButton(), ) assertNull(unstaged.stageTag) @@ -362,6 +372,7 @@ class PayPalComposableModalTest { val emptyStageConfig = ModalConfig( amount = 100.0, stageTag = "", + modalCloseButton = ModalCloseButton(), ) assertEquals("", emptyStageConfig.stageTag) } From dcb7f566db952c13288f571553c5e0733a096ab4 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 20 Oct 2025 15:31:26 -0400 Subject: [PATCH 06/16] test: update PayPalComposableModalTest with additional assertions and temporarily disable Kover plugin - Enhanced `PayPalComposableModalTest` by adding assertions for `closeButton` properties to ensure correct default values. - Commented out Kover plugin in `build.gradle` due to dependency resolution issues, with a note for future re-enablement. --- library/build.gradle | 6 +++++- .../java/com/paypal/messages/PayPalComposableModalTest.kt | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index a63ad1c4..18ddde91 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,7 +3,8 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - id 'org.jetbrains.kotlinx.kover' version '0.7.6' + // Temporarily disabled due to dependency resolution issues + // id 'org.jetbrains.kotlinx.kover' version '0.7.6' } group = "com.paypal.messages" @@ -223,6 +224,8 @@ tasks.register('testKoverPaths') { description = "Tests and fixes paths in Kover coverage reports" } +// Temporarily disabled - Kover plugin commented out +/* kover { filters { classes { @@ -256,6 +259,7 @@ kover { } } } +*/ dependencies { implementation 'androidx.core:core-ktx:1.10.1' diff --git a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt index 5535fe46..9ba6d912 100644 --- a/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt +++ b/library/src/test/java/com/paypal/messages/PayPalComposableModalTest.kt @@ -178,7 +178,13 @@ class PayPalComposableModalTest { val closeButton = ModalCloseButton() // Verify defaults - assertNull(closeButton.alternativeText) + assertEquals("PayPal learn more modal close", closeButton.alternativeText) + assertEquals(26, closeButton.width) + assertEquals(26, closeButton.height) + assertEquals(60, closeButton.availableWidth) + assertEquals(60, closeButton.availableHeight) + assertEquals("#001435", closeButton.color) + assertEquals("dark", closeButton.colorType) } @Test From 994f868c5e8d7f8db75df85895be877f2134c753 Mon Sep 17 00:00:00 2001 From: grablack Date: Wed, 22 Oct 2025 11:19:27 -0400 Subject: [PATCH 07/16] chore: re-enable Kover plugin and update configuration for coverage reporting - Re-enabled the Kover plugin in `build.gradle` after resolving previous dependency issues. - Updated comments to clarify the configuration for Kover tasks and coverage report path normalization. - Added notes for temporarily disabling Kover tasks to address potential SSL/dependency issues. --- library/build.gradle | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index 18ddde91..a04d1bd5 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,8 +3,7 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - // Temporarily disabled due to dependency resolution issues - // id 'org.jetbrains.kotlinx.kover' version '0.7.6' + id 'org.jetbrains.kotlinx.kover' version '0.7.6' } group = "com.paypal.messages" @@ -124,7 +123,7 @@ tasks.withType(Test).configureEach { } // Configure all Kover tasks to use relative paths -// Commented out for Kover 0.7.6 compatibility +// Note: Task type configuration disabled for Kover 0.7.6 compatibility // tasks.withType(org.jetbrains.kotlinx.kover.gradle.plugin.tasks.KoverXmlReportTask).configureEach { // systemProperty 'kover.path.normalization', 'relative' // } @@ -186,36 +185,41 @@ tasks.register('testMemoryIntensiveClasses') { description = "Runs memory-intensive tests in isolated JVM processes" } +// Note: The koverXmlReportDebug task is now provided by the Kover plugin +// If you encounter SSL/dependency issues locally, you can temporarily disable Kover by: +// 1. Commenting out the Kover plugin line above +// 2. Setting SKIP_KOVER=true environment variable + // Add a task to test the coverage report paths tasks.register('testKoverPaths') { dependsOn 'koverXmlReportDebug' - + doLast { println "\n\n===== Testing Kover paths in coverage report =====\n" - + def reportFile = file("${buildDir}/reports/kover/reportDebug.xml") if (!reportFile.exists()) { throw new GradleException("Coverage report file not found at: ${reportFile.absolutePath}") } - + println "Checking coverage report for absolute paths..." - + def reportContent = reportFile.text def absolutePathPattern = "/Users/" def hasAbsolutePaths = reportContent.contains(absolutePathPattern) - + if (hasAbsolutePaths) { println "WARNING: Found absolute paths in the coverage report!" println "Creating normalized version..." - + // Create a backup of the original report def backupFile = file("${buildDir}/reports/kover/reportDebug.xml.bak") backupFile.text = reportContent - + // Replace absolute paths with relative ones def normalizedContent = reportContent.replaceAll("/Users/[^/]*/Code/paypal-messages-android/", "./") reportFile.text = normalizedContent - + println "Normalized coverage report saved to: ${reportFile.absolutePath}" } else { println "SUCCESS: No absolute paths found in the coverage report." @@ -224,8 +228,6 @@ tasks.register('testKoverPaths') { description = "Tests and fixes paths in Kover coverage reports" } -// Temporarily disabled - Kover plugin commented out -/* kover { filters { classes { @@ -259,7 +261,7 @@ kover { } } } -*/ + dependencies { implementation 'androidx.core:core-ktx:1.10.1' @@ -320,7 +322,7 @@ project.ext.version = modules.sdkVersionName project.ext.pom_name = "PayPal Messages" project.ext.pom_desc = "The PayPal Android SDK Messages Module: Promote offers to your customers such as Pay Later and PayPal Credit." -// Optionally disable Kover locally to avoid agent download issues +// Optionally disable Kover tasks locally to avoid agent download issues if (System.getenv('SKIP_KOVER') == 'true' || project.hasProperty('skipKover')) { tasks.matching { it.name.toLowerCase().startsWith('kover') }.configureEach { enabled = false } } From 7909f12bc62b74e771786661415f11bc0254009d Mon Sep 17 00:00:00 2001 From: grablack Date: Wed, 22 Oct 2025 16:44:38 -0400 Subject: [PATCH 08/16] test: add unit tests for Channel, ProductGroup, PayPalMessageConfig, PayPalMessageData, PayPalMessageStyle, and ModalConfig - Introduced new tests for `Channel` to verify string representation and value retrieval. - Enhanced `ProductGroupTest` with additional assertions for value retrieval and size. - Added tests in `PayPalMessageConfigTest` to check equality, data mutability, and default style values. - Expanded `PayPalMessageDataTest` to include tests for default environment, nullable fields, equality, and field mutability. - Implemented tests in `PayPalMessageStyleTest` for default values, equality, and various style combinations. - Added tests in `ModalConfigTest` to verify default values and field mutability. --- .../com/paypal/messages/config/ChannelTest.kt | 23 +++++++++ .../messages/config/ProductGroupTest.kt | 14 ++++++ .../config/message/PayPalMessageConfigTest.kt | 32 +++++++++++++ .../config/message/PayPalMessageDataTest.kt | 47 +++++++++++++++++++ .../config/message/PayPalMessageStyleTest.kt | 40 ++++++++++++++++ .../messages/config/modal/ModalConfigTest.kt | 41 ++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 library/src/test/java/com/paypal/messages/config/ChannelTest.kt diff --git a/library/src/test/java/com/paypal/messages/config/ChannelTest.kt b/library/src/test/java/com/paypal/messages/config/ChannelTest.kt new file mode 100644 index 00000000..e5ed4603 --- /dev/null +++ b/library/src/test/java/com/paypal/messages/config/ChannelTest.kt @@ -0,0 +1,23 @@ +package com.paypal.messages.config + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ChannelTest { + @Test + fun testNative() { + assertEquals(Channel.NATIVE.toString(), "NATIVE") + } + + @Test + fun testValueOf() { + assertEquals(Channel.NATIVE, Channel.valueOf("NATIVE")) + } + + @Test + fun testValues() { + val values = Channel.values() + assertEquals(1, values.size) + assertEquals(Channel.NATIVE, values[0]) + } +} diff --git a/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt b/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt index 5b0f7f13..93d43585 100644 --- a/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt +++ b/library/src/test/java/com/paypal/messages/config/ProductGroupTest.kt @@ -13,4 +13,18 @@ class ProductGroupTest { fun testPayPalCredit() { assertEquals(ProductGroup.PAYPAL_CREDIT.toString(), "PAYPAL_CREDIT") } + + @Test + fun testValueOf() { + assertEquals(ProductGroup.PAY_LATER, ProductGroup.valueOf("PAY_LATER")) + assertEquals(ProductGroup.PAYPAL_CREDIT, ProductGroup.valueOf("PAYPAL_CREDIT")) + } + + @Test + fun testValues() { + val values = ProductGroup.values() + assertEquals(2, values.size) + assertEquals(ProductGroup.PAY_LATER, values[0]) + assertEquals(ProductGroup.PAYPAL_CREDIT, values[1]) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt index 2566a728..6dac306b 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageConfigTest.kt @@ -87,4 +87,36 @@ class PayPalMessageConfigTest { config.data = PayPalMessageData(clientID = "2") assertNotEquals(config, cloneConfig) } + + @Test + fun testEquality() { + val data = PayPalMessageData(clientID = "test_id") + val style = PayPalMessageStyle() + + val config1 = PayPalMessageConfig(data = data, style = style) + val config2 = PayPalMessageConfig(data = data, style = style) + + assertEquals(config1, config2) + assertEquals(config1.hashCode(), config2.hashCode()) + } + + @Test + fun testDataMutability() { + val config = PayPalMessageConfig( + data = PayPalMessageData(clientID = "initial"), + ) + + assertEquals("initial", config.data.clientID) + config.data.clientID = "modified" + assertEquals("modified", config.data.clientID) + } + + @Test + fun testStyleDefaultValue() { + val config = PayPalMessageConfig( + data = PayPalMessageData(clientID = "test"), + ) + + assertEquals(PayPalMessageStyle(), config.style) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt index b4c08807..15f32f87 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageDataTest.kt @@ -52,4 +52,51 @@ class PayPalMessageDataTest { data.amount = 100.00 assertNotEquals(oldData, data) } + + @Test + fun testDefaultEnvironment() { + val data = PayPalMessageData(clientID = initialClientID) + assertEquals(PayPalEnvironment.SANDBOX, data.environment) + } + + @Test + fun testNullableFields() { + val data = PayPalMessageData(clientID = initialClientID) + assertEquals(null, data.merchantID) + assertEquals(null, data.partnerAttributionID) + assertEquals(null, data.amount) + assertEquals(null, data.buyerCountry) + assertEquals(null, data.offerType) + assertEquals(null, data.pageType) + } + + @Test + fun testEquality() { + val data1 = PayPalMessageData( + clientID = initialClientID, + amount = 100.0, + environment = PayPalEnvironment.LIVE, + ) + val data2 = PayPalMessageData( + clientID = initialClientID, + amount = 100.0, + environment = PayPalEnvironment.LIVE, + ) + assertEquals(data1, data2) + assertEquals(data1.hashCode(), data2.hashCode()) + } + + @Test + fun testFieldMutability() { + val data = PayPalMessageData(clientID = initialClientID) + + data.merchantID = "new_merchant" + assertEquals("new_merchant", data.merchantID) + + data.amount = 250.0 + assertEquals(250.0, data.amount) + + data.buyerCountry = "GB" + assertEquals("GB", data.buyerCountry) + } } diff --git a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt index 84b16890..a18911f0 100644 --- a/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt +++ b/library/src/test/java/com/paypal/messages/config/message/PayPalMessageStyleTest.kt @@ -27,4 +27,44 @@ class PayPalMessageStyleTest { assertEquals(messageStyle, clonedMessageStyle) } + + @Test + fun testDefaultValues() { + val messageStyle = PayPalMessageStyle() + + assertEquals(Color.BLACK, messageStyle.color) + assertEquals(LogoType.PRIMARY, messageStyle.logoType) + assertEquals(Align.LEFT, messageStyle.textAlignment) + } + + @Test + fun testEquality() { + val style1 = PayPalMessageStyle( + color = Color.WHITE, + logoType = LogoType.INLINE, + textAlignment = Align.RIGHT, + ) + val style2 = PayPalMessageStyle( + color = Color.WHITE, + logoType = LogoType.INLINE, + textAlignment = Align.RIGHT, + ) + + assertEquals(style1, style2) + assertEquals(style1.hashCode(), style2.hashCode()) + } + + @Test + fun testDifferentCombinations() { + val style1 = PayPalMessageStyle(color = Color.GRAYSCALE) + assertEquals(Color.GRAYSCALE, style1.color) + assertEquals(LogoType.PRIMARY, style1.logoType) + + val style2 = PayPalMessageStyle(logoType = LogoType.NONE) + assertEquals(Color.BLACK, style2.color) + assertEquals(LogoType.NONE, style2.logoType) + + val style3 = PayPalMessageStyle(textAlignment = Align.CENTER) + assertEquals(Align.CENTER, style3.textAlignment) + } } diff --git a/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt b/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt index 44746672..fd1f96a3 100644 --- a/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt +++ b/library/src/test/java/com/paypal/messages/config/modal/ModalConfigTest.kt @@ -46,4 +46,45 @@ class ModalConfigTest { ), ) } + + @Test + fun testDefaultValues() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + assertEquals(null, modalConfig.amount) + assertEquals(null, modalConfig.buyerCountry) + assertEquals(Channel.NATIVE, modalConfig.channel) + assertEquals(false, modalConfig.devTouchpoint) + assertEquals(null, modalConfig.events) + assertEquals(false, modalConfig.ignoreCache) + assertEquals(null, modalConfig.offer) + assertEquals(null, modalConfig.stageTag) + } + + @Test + fun testFieldMutability() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + modalConfig.amount = 500.0 + assertEquals(500.0, modalConfig.amount) + + modalConfig.buyerCountry = "CA" + assertEquals("CA", modalConfig.buyerCountry) + + modalConfig.ignoreCache = true + assertEquals(true, modalConfig.ignoreCache) + } + + @Test + fun testWithMinimalParameters() { + val modalConfig = ModalConfig( + modalCloseButton = ModalCloseButton(), + ) + + assertEquals(Channel.NATIVE, modalConfig.channel) + } } From a2d7f92e30f9033df4409f3429d622138d50e8db Mon Sep 17 00:00:00 2001 From: grablack Date: Thu, 23 Oct 2025 14:35:19 -0400 Subject: [PATCH 09/16] ci: enhance coverage report validation and path normalization in GitHub Actions workflow - Added a step to check for the existence of Kover report files before normalization. - Implemented error handling to ensure the coverage report file is present. - Updated path normalization to accommodate GitHub Actions runner paths and handle additional absolute paths. - Improved logging for successful path normalization completion. --- .github/workflows/test.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0497fcbf..475c4702 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,14 +51,31 @@ jobs: - name: Run Unit Tests Coverage run: ./gradlew koverXmlReportDebug -Dkover.path.normalization=relative + - name: Check if coverage report was generated + run: | + echo "Checking for Kover report files..." + find library/build -name "*.xml" -path "*/kover/*" -ls || echo "No Kover XML files found" + ls -la library/build/reports/kover/ || echo "Kover reports directory does not exist" + - name: Normalize paths in coverage report run: | + # Check if the report file exists + if [ ! -f "library/build/reports/kover/reportDebug.xml" ]; then + echo "ERROR: Coverage report not found at library/build/reports/kover/reportDebug.xml" + exit 1 + fi + # Create a backup of the original report cp library/build/reports/kover/reportDebug.xml library/build/reports/kover/reportDebug.xml.bak - - # Replace absolute paths with relative ones - # This matches any paths starting with /Users/ and replaces them with relative paths - sed -i 's|/Users/[^/]*/Code/paypal-messages-android/|./|g' library/build/reports/kover/reportDebug.xml + + # Replace absolute paths with relative ones for GitHub Actions runners + # GitHub Actions uses /home/runner/work/paypal-messages-android/paypal-messages-android/ + sed -i 's|/home/runner/work/paypal-messages-android/paypal-messages-android/|./|g' library/build/reports/kover/reportDebug.xml + + # Also handle any other absolute paths that might appear + sed -i 's|${{ github.workspace }}/|./|g' library/build/reports/kover/reportDebug.xml + + echo "Path normalization completed successfully" - name: Show Coverage Report XML run: cat library/build/reports/kover/reportDebug.xml From 7097331b160b72d7888d434df379626c69e36796 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 27 Oct 2025 11:16:40 -0400 Subject: [PATCH 10/16] chore(ci): retrigger code coverage report From 8bfc5395d965f5488661fdebda9b96a3d42cf566 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 27 Oct 2025 14:43:19 -0400 Subject: [PATCH 11/16] ci: improve coverage report logging and clarify gradle task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit :library: module prefix to koverXmlReportDebug task - Add new step to extract and display coverage percentage from XML report - Parse coverage counters to help diagnose low coverage issues - Better visibility into coverage calculation process 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/test.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 475c4702..ca361c9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: cache: gradle - name: Run Unit Tests Coverage - run: ./gradlew koverXmlReportDebug -Dkover.path.normalization=relative + run: ./gradlew :library:koverXmlReportDebug -Dkover.path.normalization=relative - name: Check if coverage report was generated run: | @@ -80,6 +80,34 @@ jobs: - name: Show Coverage Report XML run: cat library/build/reports/kover/reportDebug.xml + - name: Extract and Display Coverage Percentage + run: | + echo "Extracting coverage percentage from report..." + + # Extract the overall coverage percentage from the XML report + if [ -f "library/build/reports/kover/reportDebug.xml" ]; then + # Parse XML to find coverage percentage + python3 << 'EOF' + import xml.etree.ElementTree as ET + + tree = ET.parse('library/build/reports/kover/reportDebug.xml') + root = tree.getroot() + + # Find all counter elements + for counter in root.findall('.//counter'): + counter_type = counter.get('type') + covered = int(counter.get('covered', 0)) + missed = int(counter.get('missed', 0)) + total = covered + missed + + if total > 0: + percentage = (covered / total) * 100 + print(f"{counter_type}: {percentage:.2f}% ({covered}/{total})") + EOF + else + echo "Coverage report not found" + fi + - name: Add coverage report to PR id: kover uses: mi-kas/kover-report@v1.8 From f8b9b6e2de60b5b7dc6fe6623bad24ed1e3fd6c9 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 27 Oct 2025 16:18:45 -0400 Subject: [PATCH 12/16] rerun code coverage --- .github/workflows/test.yml | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca361c9b..f3486e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,41 +80,13 @@ jobs: - name: Show Coverage Report XML run: cat library/build/reports/kover/reportDebug.xml - - name: Extract and Display Coverage Percentage - run: | - echo "Extracting coverage percentage from report..." - - # Extract the overall coverage percentage from the XML report - if [ -f "library/build/reports/kover/reportDebug.xml" ]; then - # Parse XML to find coverage percentage - python3 << 'EOF' - import xml.etree.ElementTree as ET - - tree = ET.parse('library/build/reports/kover/reportDebug.xml') - root = tree.getroot() - - # Find all counter elements - for counter in root.findall('.//counter'): - counter_type = counter.get('type') - covered = int(counter.get('covered', 0)) - missed = int(counter.get('missed', 0)) - total = covered + missed - - if total > 0: - percentage = (covered / total) * 100 - print(f"{counter_type}: {percentage:.2f}% ({covered}/{total})") - EOF - else - echo "Coverage report not found" - fi - - name: Add coverage report to PR id: kover uses: mi-kas/kover-report@v1.8 with: path: ${{ github.workspace }}/library/build/reports/kover/reportDebug.xml title: Code Coverage - update-comment: true + update-comment: false min-coverage-overall: 85 min-coverage-changed-files: 85 coverage-counter-type: LINE From 18e130c4f187cd896f5a4836789136bd7aaa1221 Mon Sep 17 00:00:00 2001 From: grablack Date: Mon, 27 Oct 2025 16:22:06 -0400 Subject: [PATCH 13/16] feat: add isolated test task for memory-intensive tests and enhance coverage reporting - Introduced a new Gradle task `testDebugUnitTestIsolated` to run isolated memory-intensive tests, improving resource management and preventing OOM errors. - Configured the task to include specific tests and increased memory limits for better performance. - Updated the `koverXmlReportDebug` task to depend on both standard and isolated test tasks, ensuring comprehensive coverage data collection. --- library/build.gradle | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/library/build.gradle b/library/build.gradle index a04d1bd5..8da0d8b2 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -185,6 +185,31 @@ tasks.register('testMemoryIntensiveClasses') { description = "Runs memory-intensive tests in isolated JVM processes" } +// Proper Test task so Kover can include isolated tests in coverage +tasks.register('testDebugUnitTestIsolated', Test) { + group = 'verification' + description = 'Runs isolated memory-intensive tests for the debug unit test variant' + useJUnitPlatform() + + // Reuse the same classpath and classes as the standard debug unit test task + def base = tasks.named('testDebugUnitTest', Test).get() + testClassesDirs = base.testClassesDirs + classpath = base.classpath + + // Only run the tests we exclude from the main task to avoid OOMs + include '**/PayPalModalActivityTest*.class', '**/ContextCompatWrapperTest*.class' + + // Increase memory and isolate executions + maxHeapSize = '3g' + jvmArgs += ['-XX:MaxMetaspaceSize=1g', '-XX:+HeapDumpOnOutOfMemoryError'] + forkEvery = 1 +} + +// Ensure coverage pulls data from both the standard and isolated test tasks +tasks.named('koverXmlReportDebug') { + dependsOn 'testDebugUnitTest', 'testDebugUnitTestIsolated' +} + // Note: The koverXmlReportDebug task is now provided by the Kover plugin // If you encounter SSL/dependency issues locally, you can temporarily disable Kover by: // 1. Commenting out the Kover plugin line above From 374f3c580be900b4f7789d5b2fbe502523183cd7 Mon Sep 17 00:00:00 2001 From: grablack Date: Tue, 28 Oct 2025 11:56:04 -0400 Subject: [PATCH 14/16] chore: update Kover plugin configuration in build.gradle files - Added the Kover plugin to the root `build.gradle` file without applying it. - Removed the isolated test task configuration from the `library/build.gradle` file, as it is now handled by the Kover plugin. - Updated comments to clarify the usage of Kover for coverage reporting. --- build.gradle | 1 + library/build.gradle | 27 +-------------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 85bd7398..2e59048d 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ plugins { id 'signing' id 'maven-publish' id 'io.codearte.nexus-staging' version '0.30.0' + id 'org.jetbrains.kotlinx.kover' version '0.7.6' apply false } tasks.register('ktLint', LintTask) { diff --git a/library/build.gradle b/library/build.gradle index 8da0d8b2..804a232a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,7 +3,7 @@ plugins { id 'kotlin-android' id 'org.jetbrains.kotlin.android' id 'de.mannodermaus.android-junit5' version "1.9.3.0" - id 'org.jetbrains.kotlinx.kover' version '0.7.6' + id 'org.jetbrains.kotlinx.kover' } group = "com.paypal.messages" @@ -185,31 +185,6 @@ tasks.register('testMemoryIntensiveClasses') { description = "Runs memory-intensive tests in isolated JVM processes" } -// Proper Test task so Kover can include isolated tests in coverage -tasks.register('testDebugUnitTestIsolated', Test) { - group = 'verification' - description = 'Runs isolated memory-intensive tests for the debug unit test variant' - useJUnitPlatform() - - // Reuse the same classpath and classes as the standard debug unit test task - def base = tasks.named('testDebugUnitTest', Test).get() - testClassesDirs = base.testClassesDirs - classpath = base.classpath - - // Only run the tests we exclude from the main task to avoid OOMs - include '**/PayPalModalActivityTest*.class', '**/ContextCompatWrapperTest*.class' - - // Increase memory and isolate executions - maxHeapSize = '3g' - jvmArgs += ['-XX:MaxMetaspaceSize=1g', '-XX:+HeapDumpOnOutOfMemoryError'] - forkEvery = 1 -} - -// Ensure coverage pulls data from both the standard and isolated test tasks -tasks.named('koverXmlReportDebug') { - dependsOn 'testDebugUnitTest', 'testDebugUnitTestIsolated' -} - // Note: The koverXmlReportDebug task is now provided by the Kover plugin // If you encounter SSL/dependency issues locally, you can temporarily disable Kover by: // 1. Commenting out the Kover plugin line above From 1db638c98e635c67fd27f0b686c985adcd087871 Mon Sep 17 00:00:00 2001 From: grablack Date: Tue, 28 Oct 2025 13:38:07 -0400 Subject: [PATCH 15/16] fix: exclude demo module from Kover coverage reports - Add excludeProjects configuration to explicitly exclude :demo module - This ensures coverage reports only include library module sources - Fixes Total Project Coverage showing 21.99% due to demo module inclusion --- build.gradle | 9 +++++++++ library/build.gradle | 3 +++ 2 files changed, 12 insertions(+) diff --git a/build.gradle b/build.gradle index 2e59048d..88348110 100644 --- a/build.gradle +++ b/build.gradle @@ -285,6 +285,15 @@ tasks.register('checkCentralPortalDeployment') { subprojects { group = "com.paypal.messages" + + // Disable Kover for demo module + if (project.name == 'demo') { + pluginManager.withPlugin('org.jetbrains.kotlinx.kover') { + kover { + isDisabled = true + } + } + } } // Configure Nexus Staging to close and release automatically (OSSRH s01) diff --git a/library/build.gradle b/library/build.gradle index 804a232a..54722f9a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -229,6 +229,9 @@ tasks.register('testKoverPaths') { } kover { + // Explicitly include only this module's sources + excludeProjects = [':demo'] + filters { classes { excludes += [ From 6616383591c9e60440279d06a81f9e590416bb06 Mon Sep 17 00:00:00 2001 From: grablack Date: Tue, 28 Oct 2025 13:42:19 -0400 Subject: [PATCH 16/16] fix: remove invalid excludeProjects property from Kover config The excludeProjects property is not supported in Kover 0.7.6. The demo module exclusion is now handled in the root build.gradle via the subprojects block with isDisabled = true. --- library/build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index 54722f9a..804a232a 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -229,9 +229,6 @@ tasks.register('testKoverPaths') { } kover { - // Explicitly include only this module's sources - excludeProjects = [':demo'] - filters { classes { excludes += [