From d955da19abffbb15a56b7217baa68f8a723024a4 Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 11:35:52 +0100 Subject: [PATCH 1/6] Fixed bug caused when changed WriteMap? to WriteMap --- .../MappinteligencePluginModule.kt | 36 +++++------ .../MappintelligencePluginSpec.kt | 2 +- .../mapper/CampaignParametersMapper.kt | 3 +- example/package.json | 4 ++ helper.md | 64 ++++++++++++++++++- ios/MappinteligencePlugin.mm | 40 +++++++----- package.json | 2 + 7 files changed, 110 insertions(+), 41 deletions(-) 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/helper.md b/helper.md index c7402bb..106f7c0 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,7 @@ cd example && npx react-native run-android --- -## 5. Run tests +## 6. Run tests Unit tests are in `src/__tests__/`. From the **repository root**: @@ -160,7 +218,7 @@ yarn test --testPathPattern="__tests__/index" --- -## 6. Optional: manual cache cleanup +## 7. 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", From 4bf609dbae3e6ede66a8f9a52b944c4fd530fad6 Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 11:55:26 +0100 Subject: [PATCH 2/6] feat: add Plugin Integration Test screen and Maestro E2E flow --- .../flows/plugin-integration-test.yaml | 15 + example/package.json | 1 + example/src/App.tsx | 5 + example/src/HomeScreen.tsx | 1 + example/src/PluginIntegrationTest.tsx | 280 ++++++++++++++++++ example/src/Routes.tsx | 3 +- 6 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 example/.maestro/flows/plugin-integration-test.yaml create mode 100644 example/src/PluginIntegrationTest.tsx diff --git a/example/.maestro/flows/plugin-integration-test.yaml b/example/.maestro/flows/plugin-integration-test.yaml new file mode 100644 index 0000000..f6a5fba --- /dev/null +++ b/example/.maestro/flows/plugin-integration-test.yaml @@ -0,0 +1,15 @@ +# Plugin Integration Test - exercises all MappIntelligencePlugin methods +# and asserts no exceptions occurred. +appId: com.mappinteligencepluginexample +--- +- launchApp +- tapOn: + text: "Plugin Integration Test" +- tapOn: + id: "run-plugin-tests" +- extendedWaitUntil: + visible: + id: "plugin-test-results" + timeout: 60000 +- assertVisible: + text: "0 failed" diff --git a/example/package.json b/example/package.json index ae2fba3..56184cc 100755 --- a/example/package.json +++ b/example/package.json @@ -3,6 +3,7 @@ "version": "0.0.2", "private": true, "scripts": { + "test:integration": "maestro test .maestro/flows/plugin-integration-test.yaml", "android": "react-native run-android", "android:clean": "rm -rf android/build android/app/build && react-native run-android", "ios": "react-native run-ios", 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 [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 testResults: Record = {}; + + // Config methods (plugin may already be initialized from App) + testResults.initWithConfiguration = await runTest( + 'initWithConfiguration', + () => + MappIntelligencePlugin.initWithConfiguration( + [794940687426749], + 'http://tracker-int-01.webtrekk.net' + ) + ); + testResults.setLogLevel = await runTest('setLogLevel', () => + MappIntelligencePlugin.setLogLevel(LogLevel.all) + ); + testResults.setBatchSupportEnabled = await runTest( + 'setBatchSupportEnabled', + () => MappIntelligencePlugin.setBatchSupportEnabled(false) + ); + testResults.setBatchSupportSize = await runTest( + 'setBatchSupportSize', + () => MappIntelligencePlugin.setBatchSupportSize(150) + ); + testResults.setRequestInterval = await runTest('setRequestInterval', () => + MappIntelligencePlugin.setRequestInterval(1) + ); + testResults.setAnonymousTracking = await runTest( + 'setAnonymousTracking', + () => MappIntelligencePlugin.setAnonymousTracking(false) + ); + testResults.build = await runTest('build', () => + MappIntelligencePlugin.build() + ); + + // Tracking methods - with full params + testResults.trackPage = await runTest('trackPage', () => + MappIntelligencePlugin.trackPage('Integration Test Page') + ); + testResults.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 + ) + ); + testResults.trackCustomPage_null = await runTest( + 'trackCustomPage (null params)', + () => MappIntelligencePlugin.trackCustomPage('Test Page') + ); + testResults.trackPageWithCustomData = await runTest( + 'trackPageWithCustomData', + () => + MappIntelligencePlugin.trackPageWithCustomData( + 'Test Page', + new Map([['cp1', 'val1']]) + ) + ); + testResults.trackPageWithCustomData_null = await runTest( + 'trackPageWithCustomData (null)', + () => MappIntelligencePlugin.trackPageWithCustomData('Test Page', null) + ); + testResults.trackAction_full = await runTest('trackAction (full)', () => + MappIntelligencePlugin.trackAction( + 'IntegrationTestAction', + { customParameters: new Map() }, + null, + null, + null, + null + ) + ); + testResults.trackAction_null = await runTest( + 'trackAction (null params)', + () => MappIntelligencePlugin.trackAction('Test Action') + ); + testResults.trackUrl = await runTest('trackUrl', () => + MappIntelligencePlugin.trackUrl('https://example.com', 'code') + ); + testResults.trackUrl_nullMedia = await runTest( + 'trackUrl (null mediaCode)', + () => MappIntelligencePlugin.trackUrl('https://example.com') + ); + testResults.trackMedia = await runTest('trackMedia', () => + MappIntelligencePlugin.trackMedia({ + pageName: 'Integration Test', + parameters: { + name: 'test', + action: 'init', + position: 0, + duration: 0, + customCategories: null, + }, + } as MediaEvent) + ); + testResults.trackMedia_null = await runTest( + 'trackMedia (null params)', + () => + MappIntelligencePlugin.trackMedia({ + pageName: 'Test', + parameters: null, + } as MediaEvent) + ); + testResults.trackException = await runTest('trackException', () => + MappIntelligencePlugin.trackException( + new Error('Integration test exception'), + 'stack trace' + ) + ); + testResults.trackExceptionWithName = await runTest( + 'trackExceptionWithName', + () => + MappIntelligencePlugin.trackExceptionWithName( + 'IntegrationTestEx', + 'Test message', + 'stack' + ) + ); + + // Other methods + testResults.getEverId = await runTest('getEverId', () => + MappIntelligencePlugin.getEverId() + ); + testResults.isInitialized = await runTest('isInitialized', () => + MappIntelligencePlugin.isInitialized() + ); + testResults.printCurrentConfig = await runTest('printCurrentConfig', () => + MappIntelligencePlugin.printCurrentConfig() + ); + testResults.sendRequestsAndClean = await runTest( + 'sendRequestsAndClean', + () => MappIntelligencePlugin.sendRequestsAndClean() + ); + + setResults(testResults); + setRunning(false); + }, [runTest]); + + const passed = Object.values(results).filter((r) => r.success).length; + const failed = Object.values(results).filter((r) => !r.success).length; + + return ( + + + + + {running ? 'Running...' : 'Run All Plugin Tests'} + + + + {Object.keys(results).length > 0 && ( + + + {passed} passed, {failed} failed + + {Object.entries(results).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', + }, + scroll: { + flex: 1, + padding: 16, + }, + 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', } From 9cc762664dba39848798ef2e36122e4c1ef2739a Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 12:07:06 +0100 Subject: [PATCH 3/6] feat: add Plugin Integration Test screen and Maestro E2E flow --- .github/workflows/ci.yml | 40 +++++++++++++++++++ .../MappinteligencePluginModule.kt | 10 ++--- helper.md | 25 +++++++++++- 3 files changed, 68 insertions(+), 7 deletions(-) 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 1ea345a..360ac1e 100755 --- a/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt +++ b/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt @@ -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( diff --git a/helper.md b/helper.md index 106f7c0..560aa71 100644 --- a/helper.md +++ b/helper.md @@ -201,7 +201,28 @@ cd example && npx react-native run-android --- -## 6. 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**: @@ -218,7 +239,7 @@ yarn test --testPathPattern="__tests__/index" --- -## 7. Optional: manual cache cleanup +## 8. Optional: manual cache cleanup If you still see odd build or Metro issues, you can clear global caches (optional): From 568b817d7987e4c2503e6f7703f82f4ba0ac7c41 Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 12:07:47 +0100 Subject: [PATCH 4/6] Reverted back to nullable paramters from trackAction function --- .../MappinteligencePluginModule.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt b/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt index 360ac1e..1ea345a 100755 --- a/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt +++ b/android/src/main/java/com/mappinteligenceplugin/MappinteligencePluginModule.kt @@ -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( From a018c55727bf588425cc9e42369e290b3912b5a7 Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 12:10:48 +0100 Subject: [PATCH 5/6] Fixed warning for deprecated SafeArea --- example/src/PluginIntegrationTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/PluginIntegrationTest.tsx b/example/src/PluginIntegrationTest.tsx index 88a0e72..6c6dfca 100644 --- a/example/src/PluginIntegrationTest.tsx +++ b/example/src/PluginIntegrationTest.tsx @@ -5,13 +5,13 @@ import { ScrollView, TouchableOpacity, StyleSheet, - SafeAreaView, } from 'react-native'; import { MappIntelligencePlugin, LogLevel, type MediaEvent, } from 'mapp-intelligence-reactnative-plugin'; +import { SafeAreaView } from 'react-native-safe-area-context'; type TestResult = { success: boolean; error?: string; duration?: number }; From ad9d782c82d2c3e6d1b9547a99ce4069e80ba327 Mon Sep 17 00:00:00 2001 From: tafilovic Date: Mon, 2 Mar 2026 13:22:13 +0100 Subject: [PATCH 6/6] Extended automation test to cover all manual testing sections of example page --- .../flows/plugin-integration-test.yaml | 2 +- example/src/PluginIntegrationTest.tsx | 714 ++++++++++++++++-- 2 files changed, 665 insertions(+), 51 deletions(-) diff --git a/example/.maestro/flows/plugin-integration-test.yaml b/example/.maestro/flows/plugin-integration-test.yaml index f6a5fba..88eec24 100644 --- a/example/.maestro/flows/plugin-integration-test.yaml +++ b/example/.maestro/flows/plugin-integration-test.yaml @@ -12,4 +12,4 @@ appId: com.mappinteligencepluginexample id: "plugin-test-results" timeout: 60000 - assertVisible: - text: "0 failed" + id: "plugin-test-all-passed" diff --git a/example/src/PluginIntegrationTest.tsx b/example/src/PluginIntegrationTest.tsx index 6c6dfca..efcfd1e 100644 --- a/example/src/PluginIntegrationTest.tsx +++ b/example/src/PluginIntegrationTest.tsx @@ -10,17 +10,128 @@ import { MappIntelligencePlugin, LogLevel, type MediaEvent, + type MIProduct, + type EcommerceParameters, + type PageParameters, + type MIBirthday, + type UserCategories, + type SessionParameters, + type EventParameters, + type CampaignParameters, + MIStatus, + MIGender, + MIAction, + MediaAction, } from 'mapp-intelligence-reactnative-plugin'; import { SafeAreaView } from 'react-native-safe-area-context'; type TestResult = { success: boolean; error?: string; duration?: number }; +type SectionResults = { section: string; tests: Record }; + +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 [results, setResults] = useState([]); const [running, setRunning] = useState(false); const runTest = useCallback( - async (name: string, fn: () => Promise): Promise => { + async (_name: string, fn: () => Promise): Promise => { const start = Date.now(); try { await fn(); @@ -39,12 +150,12 @@ export default function PluginIntegrationTest() { const runAllTests = useCallback(async () => { setRunning(true); - setResults({}); + setResults([]); - const testResults: Record = {}; + const pluginTests: Record = {}; // Config methods (plugin may already be initialized from App) - testResults.initWithConfiguration = await runTest( + pluginTests.initWithConfiguration = await runTest( 'initWithConfiguration', () => MappIntelligencePlugin.initWithConfiguration( @@ -52,33 +163,33 @@ export default function PluginIntegrationTest() { 'http://tracker-int-01.webtrekk.net' ) ); - testResults.setLogLevel = await runTest('setLogLevel', () => + pluginTests.setLogLevel = await runTest('setLogLevel', () => MappIntelligencePlugin.setLogLevel(LogLevel.all) ); - testResults.setBatchSupportEnabled = await runTest( + pluginTests.setBatchSupportEnabled = await runTest( 'setBatchSupportEnabled', () => MappIntelligencePlugin.setBatchSupportEnabled(false) ); - testResults.setBatchSupportSize = await runTest( + pluginTests.setBatchSupportSize = await runTest( 'setBatchSupportSize', () => MappIntelligencePlugin.setBatchSupportSize(150) ); - testResults.setRequestInterval = await runTest('setRequestInterval', () => + pluginTests.setRequestInterval = await runTest('setRequestInterval', () => MappIntelligencePlugin.setRequestInterval(1) ); - testResults.setAnonymousTracking = await runTest( + pluginTests.setAnonymousTracking = await runTest( 'setAnonymousTracking', () => MappIntelligencePlugin.setAnonymousTracking(false) ); - testResults.build = await runTest('build', () => + pluginTests.build = await runTest('build', () => MappIntelligencePlugin.build() ); // Tracking methods - with full params - testResults.trackPage = await runTest('trackPage', () => + pluginTests.trackPage = await runTest('trackPage', () => MappIntelligencePlugin.trackPage('Integration Test Page') ); - testResults.trackCustomPage_full = await runTest( + pluginTests.trackCustomPage_full = await runTest( 'trackCustomPage (full params)', () => MappIntelligencePlugin.trackCustomPage( @@ -90,11 +201,11 @@ export default function PluginIntegrationTest() { null ) ); - testResults.trackCustomPage_null = await runTest( + pluginTests.trackCustomPage_null = await runTest( 'trackCustomPage (null params)', () => MappIntelligencePlugin.trackCustomPage('Test Page') ); - testResults.trackPageWithCustomData = await runTest( + pluginTests.trackPageWithCustomData = await runTest( 'trackPageWithCustomData', () => MappIntelligencePlugin.trackPageWithCustomData( @@ -102,11 +213,11 @@ export default function PluginIntegrationTest() { new Map([['cp1', 'val1']]) ) ); - testResults.trackPageWithCustomData_null = await runTest( + pluginTests.trackPageWithCustomData_null = await runTest( 'trackPageWithCustomData (null)', () => MappIntelligencePlugin.trackPageWithCustomData('Test Page', null) ); - testResults.trackAction_full = await runTest('trackAction (full)', () => + pluginTests.trackAction_full = await runTest('trackAction (full)', () => MappIntelligencePlugin.trackAction( 'IntegrationTestAction', { customParameters: new Map() }, @@ -116,18 +227,18 @@ export default function PluginIntegrationTest() { null ) ); - testResults.trackAction_null = await runTest( + pluginTests.trackAction_null = await runTest( 'trackAction (null params)', () => MappIntelligencePlugin.trackAction('Test Action') ); - testResults.trackUrl = await runTest('trackUrl', () => + pluginTests.trackUrl = await runTest('trackUrl', () => MappIntelligencePlugin.trackUrl('https://example.com', 'code') ); - testResults.trackUrl_nullMedia = await runTest( + pluginTests.trackUrl_nullMedia = await runTest( 'trackUrl (null mediaCode)', () => MappIntelligencePlugin.trackUrl('https://example.com') ); - testResults.trackMedia = await runTest('trackMedia', () => + pluginTests.trackMedia = await runTest('trackMedia', () => MappIntelligencePlugin.trackMedia({ pageName: 'Integration Test', parameters: { @@ -139,7 +250,7 @@ export default function PluginIntegrationTest() { }, } as MediaEvent) ); - testResults.trackMedia_null = await runTest( + pluginTests.trackMedia_null = await runTest( 'trackMedia (null params)', () => MappIntelligencePlugin.trackMedia({ @@ -147,13 +258,13 @@ export default function PluginIntegrationTest() { parameters: null, } as MediaEvent) ); - testResults.trackException = await runTest('trackException', () => + pluginTests.trackException = await runTest('trackException', () => MappIntelligencePlugin.trackException( new Error('Integration test exception'), 'stack trace' ) ); - testResults.trackExceptionWithName = await runTest( + pluginTests.trackExceptionWithName = await runTest( 'trackExceptionWithName', () => MappIntelligencePlugin.trackExceptionWithName( @@ -164,30 +275,482 @@ export default function PluginIntegrationTest() { ); // Other methods - testResults.getEverId = await runTest('getEverId', () => + pluginTests.getEverId = await runTest('getEverId', () => MappIntelligencePlugin.getEverId() ); - testResults.isInitialized = await runTest('isInitialized', () => + pluginTests.isInitialized = await runTest('isInitialized', () => MappIntelligencePlugin.isInitialized() ); - testResults.printCurrentConfig = await runTest('printCurrentConfig', () => + pluginTests.printCurrentConfig = await runTest('printCurrentConfig', () => MappIntelligencePlugin.printCurrentConfig() ); - testResults.sendRequestsAndClean = await runTest( + pluginTests.sendRequestsAndClean = await runTest( 'sendRequestsAndClean', () => MappIntelligencePlugin.sendRequestsAndClean() ); - setResults(testResults); + 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 passed = Object.values(results).filter((r) => r.success).length; - const failed = Object.values(results).filter((r) => !r.success).length; + 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 ( - + - {Object.keys(results).length > 0 && ( + {results.length > 0 && ( {passed} passed, {failed} failed - {Object.entries(results).map(([name, r]) => ( + {failed === 0 ? ( - - {r.success ? '✓' : '✗'} {name} - {r.duration != null && ( - ({r.duration}ms) - )} - - {r.error != null && ( - - {r.error} - - )} - - ))} + testID="plugin-test-all-passed" + collapsable={false} + style={styles.successMarker} + /> + ) : ( + + )} )} + + + + {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} + + )} + + ))} + + ))} ); @@ -234,10 +826,32 @@ const styles = StyleSheet.create({ 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,