diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0076de2..d2913f9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -114,6 +114,7 @@ jobs: run: ./gradlew assembleDebug ios-sample-app: + # Keep this display name in sync with branch protection required checks ("iOS Sample App"). name: iOS Sample App runs-on: macos-15 steps: @@ -161,7 +162,7 @@ jobs: working-directory: sample run: bundle install - - name: Build iOS sample app + - name: Build iOS sample app and run commerce mapping unit tests working-directory: sample/ios run: | bundle exec pod install @@ -171,7 +172,9 @@ jobs: -destination 'id=${{ steps.simulator.outputs.udid }}' \ -derivedDataPath ios/build \ -UseModernBuildSystem=YES \ - build | bundle exec xcpretty -k + test \ + -only-testing:MParticleSampleTests/RCTConvertCommerceMappingTests \ + | bundle exec xcpretty -k pr-notify: if: > diff --git a/sample/README.md b/sample/README.md index 6aa87ec..cc68665 100644 --- a/sample/README.md +++ b/sample/README.md @@ -144,6 +144,21 @@ From the sample directory: - `yarn lint` - Run ESLint - `yarn test` - Run Jest tests +## iOS native unit tests (SDK bridge) + +The sample Xcode project includes **`RCTConvertCommerceMappingTests`**, which asserts that JavaScript `ProductActionType` / `PromotionActionType` integers map to the correct Apple SDK enums, and that **`+[RCTConvert MPCommerceEvent:]`** builds `MPCommerceEvent` / `MPPromotionContainer` with those mappings (the object graph used before `-[MParticle logCommerceEvent:]`) — see comments in that file for scope vs. the TurboModule codegen path. + +From `sample/ios` after `pod install`: + +```bash +xcodebuild -workspace MParticleSample.xcworkspace \ + -scheme MParticleSample \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + test -only-testing:MParticleSampleTests/RCTConvertCommerceMappingTests +``` + +Pull requests run these tests in CI (see `.github/workflows/pull-request.yml`). + ## Additional Resources - [mParticle Documentation](https://docs.mparticle.com/) diff --git a/sample/ios/MParticleSample.xcodeproj/project.pbxproj b/sample/ios/MParticleSample.xcodeproj/project.pbxproj index fd90829..c97202f 100644 --- a/sample/ios/MParticleSample.xcodeproj/project.pbxproj +++ b/sample/ios/MParticleSample.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* MParticleSampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* MParticleSampleTests.m */; }; + B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */; }; 0C80B921A6F3F58F76C31292 /* libPods-MParticleSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MParticleSample.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -31,6 +32,7 @@ 00E356EE1AD99517003FC87E /* MParticleSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MParticleSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* MParticleSampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MParticleSampleTests.m; sourceTree = ""; }; + B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* MParticleSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MParticleSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = MParticleSample/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = MParticleSample/AppDelegate.mm; sourceTree = ""; }; @@ -72,6 +74,7 @@ isa = PBXGroup; children = ( 00E356F21AD99517003FC87E /* MParticleSampleTests.m */, + B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = MParticleSampleTests; @@ -389,6 +392,7 @@ buildActionMask = 2147483647; files = ( 00E356F31AD99517003FC87E /* MParticleSampleTests.m in Sources */, + B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sample/ios/MParticleSampleTests/RCTConvertCommerceMappingTests.m b/sample/ios/MParticleSampleTests/RCTConvertCommerceMappingTests.m new file mode 100644 index 0000000..3671b8d --- /dev/null +++ b/sample/ios/MParticleSampleTests/RCTConvertCommerceMappingTests.m @@ -0,0 +1,113 @@ +#import +#import + +// Match RNMParticle.mm / pod umbrella so tests compile against the same SDK the library uses. +#if defined(__has_include) && __has_include() +#import +#elif defined(__has_include) && __has_include() +#import +#else +#import +#endif + +// Implemented on `RCTConvert` in `RNMParticle.mm` (react-native-mparticle pod). +@interface RCTConvert (MPCommerceEvent) ++ (MPCommerceEvent *)MPCommerceEvent:(id)json; ++ (MPCommerceEventAction)MPCommerceEventAction:(id)json; ++ (MPPromotionAction)MPPromotionAction:(id)json; +@end + +/** + * Guards JS → native commerce enum mapping used by the bridge (including New Architecture). + * Constants must stay aligned with `ProductActionType` / `PromotionActionType` in js/index.tsx. + * + * Direct `MPCommerceEventAction` / `MPPromotionAction` tests above validate the table only. + * JSON → `MPCommerceEvent` tests below exercise the same `+[RCTConvert MPCommerceEvent:]` pipeline + * used to assemble an `MPCommerceEvent` before `-[MParticle logCommerceEvent:]` (legacy bridge path), + * including `MPPromotionContainer:` wiring. That catches regressions such as casting JS ints in + * those helpers instead of calling the mappers. The New Architecture TurboModule `logCommerceEvent` + * codegen struct path is still not invoked here (would require generated C++ types in this target). + */ +@interface RCTConvertCommerceMappingTests : XCTestCase +@end + +@implementation RCTConvertCommerceMappingTests + +- (void)testMPCommerceEventAction_mapsReactNativeProductActionTypeConstants +{ + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(1)], MPCommerceEventActionAddToCart); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(2)], MPCommerceEventActionRemoveFromCart); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(3)], MPCommerceEventActionCheckout); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(4)], MPCommerceEventActionCheckoutOptions); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(5)], MPCommerceEventActionClick); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(6)], MPCommerceEventActionViewDetail); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(7)], MPCommerceEventActionPurchase); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(8)], MPCommerceEventActionRefund); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(9)], MPCommerceEventActionAddToWishList); + XCTAssertEqual([RCTConvert MPCommerceEventAction:@(10)], MPCommerceEventActionRemoveFromWishlist); +} + +- (void)testMPPromotionAction_mapsReactNativePromotionActionTypeConstants +{ + // JS: View = 0, Click = 1. Native: Click = 0, View = 1 (MPPromotion.h). + XCTAssertEqual([RCTConvert MPPromotionAction:@(0)], MPPromotionActionView); + XCTAssertEqual([RCTConvert MPPromotionAction:@(1)], MPPromotionActionClick); + XCTAssertEqual([RCTConvert MPPromotionAction:@(99)], MPPromotionActionClick); +} + +#pragma mark - JSON → MPCommerceEvent (integration-style) + +- (NSDictionary *)minimalProductJSON +{ + return @{ + @"name" : @"Test Product", + @"sku" : @"SKU-1", + @"price" : @19.99, + @"quantity" : @1, + @"customAttributes" : @{}, + }; +} + +- (void)testMPCommerceEventFromJSON_productActionFlowsThroughRCTConvertCommerceEvent +{ + NSDictionary *json = @{ + @"productActionType" : @(7), // Purchase in js/index.tsx + @"products" : @[ [self minimalProductJSON] ], + @"impressions" : @[], + }; + + MPCommerceEvent *event = [RCTConvert MPCommerceEvent:json]; + XCTAssertEqual(event.action, MPCommerceEventActionPurchase); +} + +- (void)testMPCommerceEventFromJSON_promotionActionFlowsThroughMPPromotionContainer +{ + NSDictionary *promotion = @{ + @"id" : @"promo-1", + @"name" : @"Sale", + @"creative" : @"banner", + @"position" : @"home-top", + }; + + NSDictionary *jsonView = @{ + @"promotionActionType" : @(0), // JS PromotionActionType.View + @"promotions" : @[ promotion ], + @"products" : @[], + @"impressions" : @[], + }; + MPCommerceEvent *viewEvent = [RCTConvert MPCommerceEvent:jsonView]; + XCTAssertNotNil(viewEvent.promotionContainer); + XCTAssertEqual(viewEvent.promotionContainer.action, MPPromotionActionView); + + NSDictionary *jsonClick = @{ + @"promotionActionType" : @(1), // JS PromotionActionType.Click + @"promotions" : @[ promotion ], + @"products" : @[], + @"impressions" : @[], + }; + MPCommerceEvent *clickEvent = [RCTConvert MPCommerceEvent:jsonClick]; + XCTAssertNotNil(clickEvent.promotionContainer); + XCTAssertEqual(clickEvent.promotionContainer.action, MPPromotionActionClick); +} + +@end