diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fea66c9..3cfe942 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,3 +149,43 @@ jobs: - name: Build example for iOS run: | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" + + plugin-integration-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + export PATH="$HOME/.maestro/bin:$PATH" + maestro --version + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + echo "MAESTRO=$HOME/.maestro/bin/maestro" >> $GITHUB_ENV + + - name: Run Plugin Integration Test + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: default + arch: x86_64 + script: | + cd $GITHUB_WORKSPACE + yarn install --immutable + cd example + npx react-native start & + sleep 30 + npx react-native run-android --no-packager + sleep 15 + export PATH="$HOME/.maestro/bin:$PATH" + maestro test .maestro/flows/plugin-integration-test.yaml diff --git a/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt b/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt index f3d5d6a..1ea345a 100755 --- a/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt +++ b/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt @@ -263,11 +263,11 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont @ReactMethod override fun trackCustomPage( pageTitle: String?, - pageParams: ReadableMap, - sessionParams: ReadableMap, - userCategoryParams: ReadableMap, - ecommerceParams: ReadableMap, - campaignParams: ReadableMap, + pageParams: ReadableMap?, + sessionParams: ReadableMap?, + userCategoryParams: ReadableMap?, + ecommerceParams: ReadableMap?, + campaignParams: ReadableMap?, promise: Promise ) { runOnPlugin( @@ -294,10 +294,10 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont /** Track page with a provided [PageViewEvent] */ @ReactMethod - override fun trackPageWithCustomData(params: ReadableMap, pageTitle: String, promise: Promise) { + override fun trackPageWithCustomData(params: ReadableMap?, pageTitle: String, promise: Promise) { runOnPlugin( whenInitialized = { - instance?.trackCustomPage(pageTitle, params.toMap(keyTransform = { it.toString() })) + instance?.trackCustomPage(pageTitle, params?.toMap(keyTransform = { it.toString() }) ?: emptyMap()) } ) promise.resolve(true) @@ -319,11 +319,11 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont @ReactMethod override fun trackAction( name: String, - eventParameters: ReadableMap, - sessionParameters: ReadableMap, - userCategories: ReadableMap, - eCommerceParameters: ReadableMap, - campaignParameters: ReadableMap, + eventParameters: ReadableMap?, + sessionParameters: ReadableMap?, + userCategories: ReadableMap?, + eCommerceParameters: ReadableMap?, + campaignParameters: ReadableMap?, promise: Promise ) { runOnPlugin( @@ -367,10 +367,11 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont } @ReactMethod - fun trackException(exception: ReadableMap, promise: Promise) { + fun trackException(exception: ReadableMap?, promise: Promise) { runOnPlugin( whenInitialized = { - val innerException = Exception(exception.getString("message")) + val message = exception?.getString("message") ?: "Unknown exception" + val innerException = Exception(message) instance?.trackException(innerException) } ) @@ -378,7 +379,7 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont } @ReactMethod - override fun trackMedia(readableMap: ReadableMap, promise: Promise) { + override fun trackMedia(readableMap: ReadableMap?, promise: Promise) { runOnPlugin( whenInitialized = { MediaEventMapper(readableMap).getData()?.let { instance?.trackMedia(it) } @@ -508,11 +509,6 @@ class MappinteligencePluginModule(private val reactContext: ReactApplicationCont throw Exception("Native crash") } - @ReactMethod - fun nativeCrash() { - throw Exception("Native crash"); - } - override fun getName(): String { return NAME } diff --git a/android/src/main/java/com/mappinteligenceplugin/MappintelligencePluginSpec.kt b/android/src/main/java/com/mappinteligenceplugin/MappintelligencePluginSpec.kt index dce1822..4bed21f 100644 --- a/android/src/main/java/com/mappinteligenceplugin/MappintelligencePluginSpec.kt +++ b/android/src/main/java/com/mappinteligenceplugin/MappintelligencePluginSpec.kt @@ -47,7 +47,7 @@ interface MappintelligencePluginSpec : TurboModule { promise: Promise ) fun trackException(name: String, message: String, stacktrace: String?, promise: Promise) - fun trackException(exception: ReadableMap, promise: Promise) + fun trackException(exception: ReadableMap?, promise: Promise) fun trackMedia(readableMap: ReadableMap?, promise: Promise) fun trackUrl(url: String, mediaCode: String?, promise: Promise) fun trackExceptionWithName(name: String, message: String, stacktrace: String?, promise: Promise) diff --git a/android/src/main/java/com/mappinteligenceplugin/mapper/CampaignParametersMapper.kt b/android/src/main/java/com/mappinteligenceplugin/mapper/CampaignParametersMapper.kt index e0d2234..0bca17b 100755 --- a/android/src/main/java/com/mappinteligenceplugin/mapper/CampaignParametersMapper.kt +++ b/android/src/main/java/com/mappinteligenceplugin/mapper/CampaignParametersMapper.kt @@ -4,6 +4,7 @@ import com.facebook.react.bridge.ReadableMap import com.mappinteligenceplugin.mapper.Util.optBoolean import com.mappinteligenceplugin.mapper.Util.optString import com.mappinteligenceplugin.mapper.Util.toMap +import com.mappinteligenceplugin.mapper.Util.optMap import webtrekk.android.sdk.events.eventParams.CampaignParameters class CampaignParametersMapper(private val readableMap: ReadableMap?) : Mapper { @@ -20,7 +21,7 @@ class CampaignParametersMapper(private val readableMap: ReadableMap?) : Mapper=18" } diff --git a/example/src/App.tsx b/example/src/App.tsx index 5c163d8..baedfa2 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -23,6 +23,7 @@ import { LogLevel, } from 'mapp-intelligence-reactnative-plugin'; import FetchExample from './FetchExample'; +import PluginIntegrationTest from './PluginIntegrationTest'; const Stack = createNativeStackNavigator(); @@ -82,6 +83,10 @@ const App = () => { }} > + { }; + +const product: MIProduct = { + name: 'Product 1', + cost: 13, + quantity: 4, + productSoldOut: false, + categories: new Map([ + [1, 'ProductCat1'], + [2, 'ProductCat2'], + ]), +}; + +const baseEcommerceParams: Omit< + EcommerceParameters, + 'status' | 'orderID' | 'orderValue' | 'orderStatus' +> = { + products: [product], + currency: 'EUR', + returningOrNewCustomer: 'new customer', + returnValue: 3, + cancellationValue: 2, + couponValue: 33, + paymentMethod: 'cash', + shippingServiceProvider: 'DHL', + shippingSpeed: 'highest', + shippingCost: 35, + markUp: 1, + customParameters: new Map([ + [1, 'ProductParam1'], + [2, 'ProductParam2'], + ]), +}; + +const buildManualMediaEvent = ( + action: MediaAction +): MediaEvent => { + const customMediaCategories = new Map([[20, 'mediaCat']]); + + const mediaParams = { + name: 'Sample test video for React Native', + action: action.valueOf(), + position: 0, + duration: 0, + customCategories: customMediaCategories, + }; + + const eventParameters: EventParameters = { + customParameters: new Map([[1, 'MediaParam1']]), + }; + + const product: MIProduct = { + name: 'Product 1', + cost: 13, + quantity: 4, + productSoldOut: false, + categories: new Map([ + [1, 'ProductCat1'], + [2, 'ProductCat2'], + ]), + }; + + const ecommerceParam: EcommerceParameters = { + products: [product], + status: MIStatus.noneStatus, + currency: 'EUR', + orderID: 'ud679adn', + orderValue: 456, + returningOrNewCustomer: 'new customer', + returnValue: 3, + cancellationValue: 2, + couponValue: 33, + paymentMethod: 'cash', + shippingServiceProvider: 'DHL', + shippingSpeed: 'highest', + shippingCost: 35, + markUp: 1, + orderStatus: 'order received', + customParameters: new Map([ + [1, 'ProductParam1'], + [2, 'ProductParam2'], + ]), + }; + + const sessionParameters: SessionParameters = { + parameters: new Map().set(10, 'sessionParam1'), + }; + + const customParams = new Map([[1, 'Param1']]); + + return { + pageName: 'Manual Media Tracking', + parameters: mediaParams, + customParameters: customParams, + eCommerceParameters: ecommerceParam, + eventParameters, + sessionParameters, + }; +}; + +export default function PluginIntegrationTest() { + const [results, setResults] = useState([]); + const [running, setRunning] = useState(false); + + const runTest = useCallback( + async (_name: string, fn: () => Promise): Promise => { + const start = Date.now(); + try { + await fn(); + return { success: true, duration: Date.now() - start }; + } catch (e: unknown) { + const err = e as Error; + return { + success: false, + error: err?.message || String(e), + duration: Date.now() - start, + }; + } + }, + [] + ); + + const runAllTests = useCallback(async () => { + setRunning(true); + setResults([]); + + const pluginTests: Record = {}; + + // Config methods (plugin may already be initialized from App) + pluginTests.initWithConfiguration = await runTest( + 'initWithConfiguration', + () => + MappIntelligencePlugin.initWithConfiguration( + [794940687426749], + 'http://tracker-int-01.webtrekk.net' + ) + ); + pluginTests.setLogLevel = await runTest('setLogLevel', () => + MappIntelligencePlugin.setLogLevel(LogLevel.all) + ); + pluginTests.setBatchSupportEnabled = await runTest( + 'setBatchSupportEnabled', + () => MappIntelligencePlugin.setBatchSupportEnabled(false) + ); + pluginTests.setBatchSupportSize = await runTest( + 'setBatchSupportSize', + () => MappIntelligencePlugin.setBatchSupportSize(150) + ); + pluginTests.setRequestInterval = await runTest('setRequestInterval', () => + MappIntelligencePlugin.setRequestInterval(1) + ); + pluginTests.setAnonymousTracking = await runTest( + 'setAnonymousTracking', + () => MappIntelligencePlugin.setAnonymousTracking(false) + ); + pluginTests.build = await runTest('build', () => + MappIntelligencePlugin.build() + ); + + // Tracking methods - with full params + pluginTests.trackPage = await runTest('trackPage', () => + MappIntelligencePlugin.trackPage('Integration Test Page') + ); + pluginTests.trackCustomPage_full = await runTest( + 'trackCustomPage (full params)', + () => + MappIntelligencePlugin.trackCustomPage( + 'Integration Test Page', + { params: new Map(), categories: new Map(), searchTerm: '' }, + { parameters: new Map() }, + null, + null, + null + ) + ); + pluginTests.trackCustomPage_null = await runTest( + 'trackCustomPage (null params)', + () => MappIntelligencePlugin.trackCustomPage('Test Page') + ); + pluginTests.trackPageWithCustomData = await runTest( + 'trackPageWithCustomData', + () => + MappIntelligencePlugin.trackPageWithCustomData( + 'Test Page', + new Map([['cp1', 'val1']]) + ) + ); + pluginTests.trackPageWithCustomData_null = await runTest( + 'trackPageWithCustomData (null)', + () => MappIntelligencePlugin.trackPageWithCustomData('Test Page', null) + ); + pluginTests.trackAction_full = await runTest('trackAction (full)', () => + MappIntelligencePlugin.trackAction( + 'IntegrationTestAction', + { customParameters: new Map() }, + null, + null, + null, + null + ) + ); + pluginTests.trackAction_null = await runTest( + 'trackAction (null params)', + () => MappIntelligencePlugin.trackAction('Test Action') + ); + pluginTests.trackUrl = await runTest('trackUrl', () => + MappIntelligencePlugin.trackUrl('https://example.com', 'code') + ); + pluginTests.trackUrl_nullMedia = await runTest( + 'trackUrl (null mediaCode)', + () => MappIntelligencePlugin.trackUrl('https://example.com') + ); + pluginTests.trackMedia = await runTest('trackMedia', () => + MappIntelligencePlugin.trackMedia({ + pageName: 'Integration Test', + parameters: { + name: 'test', + action: 'init', + position: 0, + duration: 0, + customCategories: null, + }, + } as MediaEvent) + ); + pluginTests.trackMedia_null = await runTest( + 'trackMedia (null params)', + () => + MappIntelligencePlugin.trackMedia({ + pageName: 'Test', + parameters: null, + } as MediaEvent) + ); + pluginTests.trackException = await runTest('trackException', () => + MappIntelligencePlugin.trackException( + new Error('Integration test exception'), + 'stack trace' + ) + ); + pluginTests.trackExceptionWithName = await runTest( + 'trackExceptionWithName', + () => + MappIntelligencePlugin.trackExceptionWithName( + 'IntegrationTestEx', + 'Test message', + 'stack' + ) + ); + + // Other methods + pluginTests.getEverId = await runTest('getEverId', () => + MappIntelligencePlugin.getEverId() + ); + pluginTests.isInitialized = await runTest('isInitialized', () => + MappIntelligencePlugin.isInitialized() + ); + pluginTests.printCurrentConfig = await runTest('printCurrentConfig', () => + MappIntelligencePlugin.printCurrentConfig() + ); + pluginTests.sendRequestsAndClean = await runTest( + 'sendRequestsAndClean', + () => MappIntelligencePlugin.sendRequestsAndClean() + ); + + const ecommerceTests: Record = {}; + + ecommerceTests.viewProduct = await runTest('View Product', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.viewed, + orderID: 'ud679adn', + orderValue: 456, + orderStatus: 'order received', + } + ) + ); + + ecommerceTests.addToBasket = await runTest('Add to Basket', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - add to basket', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.addedToBasket, + currency: 'USD', + orderID: 'ud679adn', + orderValue: 549, + returningOrNewCustomer: 'returning customer', + returnValue: 1, + couponValue: 10, + paymentMethod: 'credit card', + shippingSpeed: 'normal', + shippingCost: 15, + markUp: 2, + orderStatus: 'order added', + } + ) + ); + + ecommerceTests.purchased = await runTest('Purchased', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - purchased', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.purchased, + currency: '$', + orderID: 'ud679adn', + orderValue: 695, + returningOrNewCustomer: 'returning customer', + returnValue: 1, + couponValue: 10, + paymentMethod: 'credit card', + shippingSpeed: 'lower', + shippingCost: 15, + markUp: 2, + orderStatus: 'order sent', + } + ) + ); + + ecommerceTests.deleteFromCart = await runTest('Delete from Cart', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - delete from basket', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.deletedFromBasket, + currency: '$', + orderID: 'ud679adn', + orderValue: 695, + returningOrNewCustomer: 'returning customer', + returnValue: 1, + couponValue: 10, + paymentMethod: 'credit card', + shippingSpeed: 'lower', + shippingCost: 15, + markUp: 2, + orderStatus: 'order deleted from basket', + } + ) + ); + + ecommerceTests.addToWhishlist = await runTest('Add to Whishlist', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - added to whishlist', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.addedToWishlist, + currency: '$', + orderID: '124kire43', + orderValue: 235, + returnValue: 2, + cancellationValue: 4, + couponValue: 13, + paymentMethod: 'credit card', + shippingSpeed: 'lower', + shippingCost: 23, + markUp: 1, + orderStatus: 'order received', + } + ) + ); + + ecommerceTests.deleteFromWhishlist = await runTest( + 'Delete from Whishlist', + () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - purchased', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.deletedFromWishlist, + currency: '$', + orderID: '12ief45', + orderValue: 345, + returnValue: 1, + couponValue: 18, + paymentMethod: 'credit card', + shippingSpeed: 'lower', + shippingCost: 20, + markUp: 4, + orderStatus: 'order removed from whishlist', + } + ) + ); + + ecommerceTests.checkout = await runTest('Checkout', () => + MappIntelligencePlugin.trackCustomPage( + 'ECommerce Tracking - checkout', + null, + null, + null, + { + ...baseEcommerceParams, + status: MIStatus.checkout, + currency: '$', + orderID: 'ij485o', + orderValue: 423, + returningOrNewCustomer: 'new customer', + returnValue: 1, + cancellationValue: 3, + couponValue: 45, + paymentMethod: 'credit card', + shippingSpeed: 'lower', + shippingCost: 22, + markUp: 1, + orderStatus: 'order received', + } + ) + ); + + const pageTrackingTests: Record = {}; + + pageTrackingTests.trackPage = await runTest('Page: Track Page', () => + MappIntelligencePlugin.trackPage('Page 1') + ); + + pageTrackingTests.trackCustomPage = await runTest( + 'Page: Track Custom Page', + async () => { + const paramsDict = new Map().set(20, 'cp20'); + const categoriesDict = new Map().set(10, 'test'); + + const pageParameters: PageParameters = { + params: paramsDict, + categories: categoriesDict, + searchTerm: 'testSearchTerm', + }; + + const birthday: MIBirthday = { + day: 7, + month: 12, + year: 1991, + }; + + const customCategoriesDict = new Map().set( + 20, + 'userParam1' + ); + + const userCategories: UserCategories = { + birthday, + city: 'Paris', + country: 'France', + gender: MIGender.female, + customerId: 'CustomerID', + newsletterSubscribed: false, + customCategories: customCategoriesDict, + }; + + const customSessionDict = new Map().set( + 10, + 'sessionParam1' + ); + const sessionParameters: SessionParameters = { + parameters: customSessionDict, + }; + + const prod1: MIProduct = { + name: 'Product 1', + cost: 110.56, + quantity: 1, + productAdvertiseID: 12345, + productSoldOut: true, + productVariant: 'a', + categories: new Map([[1, 'group 1']]), + ecommerceParameters: null, + }; + + const products: MIProduct[] = [prod1]; + const ecommerceParameters: EcommerceParameters = { + products, + status: MIStatus.purchased, + currency: 'EUR', + orderID: '1234nb5', + orderValue: 120.56, + returningOrNewCustomer: 'new customer', + returnValue: 0, + cancellationValue: 0, + couponValue: 10, + paymentMethod: 'Credit Card', + shippingServiceProvider: 'DHL', + shippingSpeed: 'express', + shippingCost: 20, + markUp: 0, + orderStatus: 'order received', + customParameters: new Map(), + }; + + const campaignParameters: CampaignParameters = { + campaignId: 'email.newsletter.nov2020.thursday', + action: MIAction.view, + mediaCode: 'abc', + oncePerSession: true, + customParameters: new Map([[12, 'camParam1']]), + }; + + await MappIntelligencePlugin.trackCustomPage( + 'Page Tracking Example 1', + pageParameters, + sessionParameters, + userCategories, + ecommerceParameters, + campaignParameters + ); + } + ); + + pageTrackingTests.trackPageWithCustomData = await runTest( + 'Page: Track Page With Custom Data', + () => { + const customParameters = new Map(); + customParameters.set('cp10', 'Override'); + customParameters.set('cg10', 'test'); + return MappIntelligencePlugin.trackPageWithCustomData( + 'testTitle1', + customParameters + ); + } + ); + + const actionTrackingTests: Record = {}; + + actionTrackingTests.trackAction = await runTest( + 'Action: Track Action', + async () => { + const eventDict = new Map().set(20, 'ck20Param1'); + const eventParameters: EventParameters = { + customParameters: eventDict, + }; + await MappIntelligencePlugin.trackAction('TestAction', eventParameters); + } + ); + + actionTrackingTests.trackCustomAction = await runTest( + 'Action: Track Custom Action', + async () => { + const eventDict = new Map().set(20, 'ck20Param1'); + const eventParamters: EventParameters = { + customParameters: eventDict, + }; + const birthday: MIBirthday = { + day: 12, + month: 1, + year: 1993, + }; + const customCategoriesDict = new Map().set( + 20, + "( $', /:?@=&+ !.;()-_" + ); + const userCategories: UserCategories = { + birthday, + city: 'Paris', + country: 'France', + emailReceiverId: 'testd598378532', + gender: MIGender.unknown, + customerId: 'CustomerID', + customCategories: customCategoriesDict, + }; + const customSessionDict = new Map().set( + 10, + 'sessionParam1' + ); + const sessionParameters: SessionParameters = { + parameters: customSessionDict, + }; + await MappIntelligencePlugin.trackAction( + 'TestAction', + eventParamters, + sessionParameters, + userCategories + ); + } + ); + + const campaignTrackingTests: Record = {}; + + campaignTrackingTests.trackCampaign = await runTest( + 'Campaign: Track Campaign', + async () => { + const campaignParameters: CampaignParameters = { + campaignId: 'email.newsletter.nov2020.thursday', + action: MIAction.view, + mediaCode: 'abc', + oncePerSession: true, + customParameters: new Map([[12, 'campParam1']]), + }; + + await MappIntelligencePlugin.trackCustomPage( + 'Test Campaign', + null, + null, + null, + null, + campaignParameters + ); + + await MappIntelligencePlugin.sendRequestsAndClean(); + } + ); + + campaignTrackingTests.link1 = await runTest( + 'Campaign: Test Link1', + async () => { + const url = + 'https://testurl.com/?wt_mc=email.newsletter.nov2020.thursday&cc45=parameter45'; + + await MappIntelligencePlugin.trackUrl(url); + await MappIntelligencePlugin.trackPage('Campaign Tracking 1'); + await MappIntelligencePlugin.sendRequestsAndClean(); + } + ); + + campaignTrackingTests.link2 = await runTest( + 'Campaign: Test Link2', + async () => { + const url = + 'https://testurl.com/?abc=email.newsletter.nov2020.thursday&wt_cc12=parameter12'; + + await MappIntelligencePlugin.trackUrl(url, 'abc'); + await MappIntelligencePlugin.trackPage('Campaign Tracking 2'); + await MappIntelligencePlugin.sendRequestsAndClean(); + } + ); + + const mediaManualTests: Record = {}; + + mediaManualTests.init = await runTest('Media: INIT', () => + MappIntelligencePlugin.trackMedia(buildManualMediaEvent(MediaAction.init)) + ); + + mediaManualTests.play = await runTest('Media: PLAY', () => + MappIntelligencePlugin.trackMedia(buildManualMediaEvent(MediaAction.play)) + ); + + mediaManualTests.pause = await runTest('Media: PAUSE', () => + MappIntelligencePlugin.trackMedia( + buildManualMediaEvent(MediaAction.pause) + ) + ); + + mediaManualTests.stop = await runTest('Media: STOP', () => + MappIntelligencePlugin.trackMedia(buildManualMediaEvent(MediaAction.stop)) + ); + + mediaManualTests.position = await runTest('Media: POSITION', () => + MappIntelligencePlugin.trackMedia(buildManualMediaEvent(MediaAction.pos)) + ); + + mediaManualTests.seek = await runTest('Media: SEEK', () => + MappIntelligencePlugin.trackMedia( + buildManualMediaEvent(MediaAction.seek) + ) + ); + + mediaManualTests.eof = await runTest('Media: EOF', () => + MappIntelligencePlugin.trackMedia(buildManualMediaEvent(MediaAction.eof)) + ); + + const exceptionTests: Record = {}; + + exceptionTests.trackExceptionWithName = await runTest( + 'Exception: trackExceptionWithName', + async () => { + try { + JSON.parse('Invalid JSON String'); + } catch (e) { + const error = e as Error; + await MappIntelligencePlugin.trackExceptionWithName( + error.name, + error.message + ); + } + } + ); + + exceptionTests.trackException = await runTest( + 'Exception: trackException', + async () => { + try { + JSON.parse('Invalid JSON String'); + } catch (e) { + const error = e as Error; + await MappIntelligencePlugin.trackException(error); + } + } + ); + + setResults([ + { section: 'Plugin API', tests: pluginTests }, + { section: 'ECommerce Tracking', tests: ecommerceTests }, + { section: 'Page Tracking', tests: pageTrackingTests }, + { section: 'Action Tracking', tests: actionTrackingTests }, + { section: 'Campaign Tracking', tests: campaignTrackingTests }, + { section: 'Media Tracking (Manual)', tests: mediaManualTests }, + { section: 'Exception Tracking', tests: exceptionTests }, + ]); + setRunning(false); + }, [runTest]); + + const allTests = results.flatMap((section) => + Object.values(section.tests) + ); + const passed = allTests.filter((r) => r.success).length; + const failed = allTests.filter((r) => !r.success).length; + + return ( + + + + + {running ? 'Running...' : 'Run All Plugin Tests'} + + + + {results.length > 0 && ( + + + {passed} passed, {failed} failed + + {failed === 0 ? ( + + ) : ( + + )} + + )} + + + + {results.length > 0 && + results.map((section, index) => ( + + + {section.section} + + {Object.entries(section.tests).map(([name, r]) => ( + + + {r.success ? '✓' : '✗'} {name} + {r.duration != null && ( + ({r.duration}ms) + )} + + {r.error != null && ( + + {r.error} + + )} + + ))} + + ))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + header: { + padding: 16, + paddingBottom: 0, + }, + scroll: { + flex: 1, + padding: 16, + }, + sectionHeader: { + fontSize: 16, + fontWeight: '600', + marginTop: 16, + marginBottom: 8, + color: '#333', + }, + sectionHeaderFirst: { + marginTop: 0, + }, + successMarker: { + height: 1, + width: 1, + }, + failureMarker: { + height: 1, + width: 1, + }, + button: { + backgroundColor: '#007AFF', + padding: 16, + borderRadius: 8, + marginBottom: 16, + }, + buttonDisabled: { + opacity: 0.5, + }, + buttonText: { + color: 'white', + fontSize: 16, + textAlign: 'center', + }, + summary: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + }, + row: { + padding: 12, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + rowFailed: { + backgroundColor: '#ffebee', + }, + rowText: { + fontSize: 14, + }, + duration: { + fontSize: 12, + color: '#666', + }, + error: { + color: '#c62828', + fontSize: 12, + marginTop: 4, + }, +}); diff --git a/example/src/Routes.tsx b/example/src/Routes.tsx index 4f9a4a6..2f86633 100644 --- a/example/src/Routes.tsx +++ b/example/src/Routes.tsx @@ -11,5 +11,6 @@ export enum Routes { STREAMING_VIDEO_EXAMPLE = 'Streaming Video Example', VIDEO_EXAMPLE = 'Video Example', MANUAL_MEDIA_TRACKING = 'Manual Media Tracking', - FETCH_EXAMPLE="Fetch Example", + FETCH_EXAMPLE = 'Fetch Example', + PLUGIN_INTEGRATION_TEST = 'Plugin Integration Test', } diff --git a/helper.md b/helper.md index c7402bb..560aa71 100644 --- a/helper.md +++ b/helper.md @@ -129,7 +129,65 @@ npx react-native run-android # or run-ios --- -## 4. Full reset workflow +## 4. Testing with the published plugin + +By default, the example app uses the **local** plugin via `workspace:*`. To test with the **published** version from npm instead: + +### 1. Switch the dependency + +In `example/package.json`, change: + +```json +"mapp-intelligence-reactnative-plugin": "workspace:*", +``` + +to the published version (e.g. `1.1.1`): + +```json +"mapp-intelligence-reactnative-plugin": "1.1.1", +``` + +### 2. Reinstall dependencies + +From the **repository root**: + +```bash +yarn install +``` + +### 3. Rebuild native projects + +Because the plugin has native code, rebuild the apps: + +**Android:** +```bash +cd example +npx react-native run-android +# or for a clean build: +yarn android:clean +``` + +**iOS:** +```bash +cd example +npx pod-install +npx react-native run-ios +``` + +### 4. Clear Metro cache (if needed) + +If you see stale behavior: + +```bash +cd example +npx react-native start --reset-cache +``` + +**To switch back to the local plugin**, change the dependency back to `"workspace:*"` and run `yarn install` again. + +--- + +## 5. Full reset workflow Use this when you want a completely fresh environment (e.g. after pulling changes or switching branches): @@ -143,7 +201,28 @@ cd example && npx react-native run-android --- -## 5. Run tests +## 6. Plugin Integration Test + +The example app includes a **Plugin Integration Test** screen that exercises all plugin methods (with full and null params) and detects exceptions. Use it to catch regressions like nullable→non-nullable parameter changes. + +**Run manually:** +1. Launch the example app (`cd example && npx react-native run-android`) +2. Tap **Plugin Integration Test** on the home screen +3. Tap **Run All Plugin Tests** +4. Verify all tests pass (0 failed) + +**Run with Maestro (E2E):** Install [Maestro](https://maestro.mobile.dev/) then: + +```bash +cd example +yarn test:integration +``` + +Or: `maestro test example/.maestro/flows/plugin-integration-test.yaml` + +--- + +## 7. Run tests Unit tests are in `src/__tests__/`. From the **repository root**: @@ -160,7 +239,7 @@ yarn test --testPathPattern="__tests__/index" --- -## 6. Optional: manual cache cleanup +## 8. Optional: manual cache cleanup If you still see odd build or Metro issues, you can clear global caches (optional): diff --git a/ios/MappinteligencePlugin.mm b/ios/MappinteligencePlugin.mm index 0313ca1..aca33ca 100755 --- a/ios/MappinteligencePlugin.mm +++ b/ios/MappinteligencePlugin.mm @@ -291,12 +291,13 @@ @implementation MappinteligencePlugin pageTitle:(NSString*)pageTitle resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [[MappIntelligence shared] trackCustomPage:pageTitle trackingParams:pageParameters]; - }); - resolve(@1); - } +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSDictionary* params = [pageParameters isKindOfClass:[NSNull class]] ? nil : (NSDictionary*)pageParameters; + [[MappIntelligence shared] trackCustomPage:pageTitle trackingParams:params]; + }); + resolve(@1); +} RCT_EXPORT_METHOD(trackAction:(NSString*)name eventParameters:(id)eventParameters @@ -369,20 +370,27 @@ @implementation MappinteligencePlugin reject:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_main_queue(), ^{ - if(!mediaEventDictionary[@"parameters"]) { - NSLog(@"Media event must have page name"); - } NSDictionary* mp = [mediaEventDictionary isKindOfClass:[NSNull class]] ? nil : (NSDictionary*)mediaEventDictionary; - MIMediaParameters* mParameters = [self prepareMediaParameters:mp[@"parameters"]]; - MIMediaEvent* mediaEvent = [[MIMediaEvent alloc] initWithPageName:mediaEventDictionary[@"parameters"][@"name"] parameters:mParameters]; - [mediaEvent setEventParameters:[self prepareEventParamters:mediaEventDictionary[@"eventParameters"]]]; - [mediaEvent setPageName:mediaEventDictionary[@"pageName"]]; - [mediaEvent setSessionParameters:[self prepareSessionParameters:mediaEventDictionary[@"sessionParameters"]]]; - [mediaEvent setEcommerceParameters:[self prepareEcommerceParameters:mediaEventDictionary[@"eCommerceParameters"]]]; + if (!mp || !mp[@"parameters"]) { + NSLog(@"Media event must have valid dictionary with parameters"); + resolve(@0); + return; + } + NSDictionary* params = mp[@"parameters"]; + NSString* pageName = mp[@"pageName"] ?: @""; + MIMediaParameters* mParameters = [self prepareMediaParameters:params]; + if (params[@"name"]) { + mParameters.name = params[@"name"]; + } + MIMediaEvent* mediaEvent = [[MIMediaEvent alloc] initWithPageName:pageName parameters:mParameters]; + [mediaEvent setEventParameters:[self prepareEventParamters:mp[@"eventParameters"]]]; + [mediaEvent setPageName:pageName]; + [mediaEvent setSessionParameters:[self prepareSessionParameters:mp[@"sessionParameters"]]]; + [mediaEvent setEcommerceParameters:[self prepareEcommerceParameters:mp[@"eCommerceParameters"]]]; [[MappIntelligence shared] trackMedia:mediaEvent]; + resolve(@1); }); - resolve(@1); } RCT_EXPORT_METHOD(trackException:(NSString*)name diff --git a/package.json b/package.json index ce7ff59..e6bbfc0 100755 --- a/package.json +++ b/package.json @@ -61,6 +61,8 @@ "@react-native-community/cli": "^18.0.0", "@react-native/codegen": "0.83.1", "@react-native/gradle-plugin": "0.83.1", + "@types/react": "^19.2.14", + "@types/react-native": "^0.73.0", "jest": "^29.7.0", "react": "^19.2.0", "react-native": "0.83.1",