From 3797682f08fe3305d889880de218422e7b1f2489 Mon Sep 17 00:00:00 2001 From: Kevin Dice Date: Fri, 2 Oct 2026 09:52:17 -0600 Subject: [PATCH] [RUM-1469] Babel plugin: wrap nested tracked components with their own options getJSXElementActionPaths collected handler attributes with path.traverse, which also visits nested elements, so a tracked component inside another tracked component was wrapped with the outer element's ddValues (useContent/getContent) and then skipped on its own visit. Only accept handler attributes that belong to the element being processed. Fixes #1469 --- .../src/actions/rum/index.ts | 10 ++- .../test/plugin.test.ts | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/react-native-babel-plugin/src/actions/rum/index.ts b/packages/react-native-babel-plugin/src/actions/rum/index.ts index a266e343d..5e9bb9e66 100644 --- a/packages/react-native-babel-plugin/src/actions/rum/index.ts +++ b/packages/react-native-babel-plugin/src/actions/rum/index.ts @@ -600,8 +600,14 @@ export function getJSXElementActionPaths( return; } - // Accumulate handler attributes that we should wrap - const isValidMapping = actionMapList.includes(attrName); + // Accumulate handler attributes that we should wrap. Only this + // element's own attributes qualify: `path.traverse` also visits + // nested elements, and a nested tracked component must be wrapped + // with its own ddValues (useContent/getContent) when its JSXElement + // is visited, not with those of the enclosing tracked element. + const isValidMapping = + actionMapList.includes(attrName) && + subpath.parentPath?.node === path.node.openingElement; if (isValidMapping) { actionPathNames.push(attrName); actionPathList.push(subpath); diff --git a/packages/react-native-babel-plugin/test/plugin.test.ts b/packages/react-native-babel-plugin/test/plugin.test.ts index c4cb123ba..4813fe88a 100644 --- a/packages/react-native-babel-plugin/test/plugin.test.ts +++ b/packages/react-native-babel-plugin/test/plugin.test.ts @@ -1837,6 +1837,74 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )', }); }); +describe('Babel plugin: nested tracked components', () => { + const options: Partial = { + components: { + useContent: true, + useNamePrefix: true, + tracked: [ + { + name: 'Row', + useContent: false, + handlers: [{ event: 'onPress', action: 'TAP' }] + }, + { + name: 'CustomButton', + handlers: [{ event: 'onPress', action: 'TAP' }] + } + ] + } + }; + + const targetObjectFor = (output: string, componentName: string) => { + const marker = `"componentName": "${componentName}"`; + const end = output.indexOf(marker); + expect(end).toBeGreaterThan(-1); + const start = output.lastIndexOf('"options"', end); + return output.slice(start, end + marker.length); + }; + + it('should wrap a tracked component nested in another tracked component with its own options and content', () => { + const input = ` + function Screen({ onRowPress, onButtonPress }) { + return ( + + + + ); + } + `; + const output = transformCode(input, options) ?? ''; + + const row = targetObjectFor(output, 'Row'); + expect(row).toContain('"useContent": false'); + expect(row).not.toContain('"getContent"'); + + const button = targetObjectFor(output, 'CustomButton'); + expect(button).toContain('"useContent": true'); + expect(button).toContain('"getContent"'); + expect(button).toContain('["Open"]'); + }); + + it('should wrap a nested tracked component even when the enclosing tracked component has no handler', () => { + const input = ` + function Screen({ onButtonPress }) { + return ( + + + + ); + } + `; + const output = transformCode(input, options) ?? ''; + expect(output).not.toContain('"componentName": "Row"'); + + const button = targetObjectFor(output, 'CustomButton'); + expect(button).toContain('"useContent": true'); + expect(button).toContain('["Open"]'); + }); +}); + describe('Babel plugin: hyphenated JSX attribute names in getContent', () => { function extractGetContent(output: string | null | undefined): string { if (!output) {