Skip to content

Add testID prop for E2E testing support with prefixed internal component IDs#43

Description

@cyril36

Hi! 馃憢

Firstly, thanks for your work on this project! 馃檪

Today I used patch-package to patch react-native-google-places-textinput@0.9.1 for the project I'm working on.

To be able to create proper testing flow with maestro I needed to add testID props to the suggestion dropdown.
I have updated the code to add testID to GooglePlacesTextInputProps and propagate it to each component of GooglePlacesTextInput with the component name + testID value

if testID==location
container-location - Main container View
input-wrapper-location - Input wrapper View
textinput-location - TextInput
clear-button-location - Clear button TouchableOpacity
loading-indicator-location - ActivityIndicator
suggestions-container-location - Suggestions container View
suggestions-list-location - FlatList
suggestion-touchableopacity-0-location - Suggestion item TouchableOpacity (index 0)
suggestion-text-0-location - Suggestion main text (index 0)
suggestion-secondarytext-0-location - Suggestion secondary text (index 0)

Here is the diff that solved my problem:

diff --git a/node_modules/react-native-google-places-textinput/lib/module/GooglePlacesTextInput.js b/node_modules/react-native-google-places-textinput/lib/module/GooglePlacesTextInput.js
index b12e7bf..ee8e06c 100644
--- a/node_modules/react-native-google-places-textinput/lib/module/GooglePlacesTextInput.js
+++ b/node_modules/react-native-google-places-textinput/lib/module/GooglePlacesTextInput.js
@@ -42,6 +42,7 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
   onBlur,
   accessibilityLabels = {},
   suggestionTextProps = {},
+  testID,
   ...restTextInputProps
 }, ref) => {
   const [predictions, setPredictions] = useState([]);
@@ -288,6 +289,8 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
       }
     }, 10);
   };
+  // Test ID suffix for all components
+  const testIDSuffix = testID ? `-${testID}` : '';
   const renderSuggestion = ({
     item,
     index
@@ -303,6 +306,7 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
     const defaultAccessibilityLabel = `${mainText.text}${secondaryText ? `, ${secondaryText.text}` : ''}`;
     const accessibilityLabel = accessibilityLabels.suggestionItem?.(item.placePrediction) || defaultAccessibilityLabel;
     return /*#__PURE__*/_jsxs(TouchableOpacity, {
+      testID: `suggestion-touchableopacity-${index}${testIDSuffix}`,
       accessibilityRole: "button",
       accessibilityLabel: accessibilityLabel,
       accessibilityHint: "Double tap to select this place",
@@ -321,11 +325,13 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
         }
       }),
       children: [/*#__PURE__*/_jsx(Text, {
+        testID: `suggestion-text-${index}${testIDSuffix}`,
         style: [styles.mainText, style.suggestionText?.main, getTextAlign()],
         numberOfLines: suggestionTextProps.mainTextNumberOfLines,
         ellipsizeMode: suggestionTextProps.ellipsizeMode || 'tail',
         children: mainText.text
       }), secondaryText && /*#__PURE__*/_jsx(Text, {
+        testID: `suggestion-secondarytext-${index}${testIDSuffix}`,
         style: [styles.secondaryText, style.suggestionText?.secondary, getTextAlign()],
         numberOfLines: suggestionTextProps.secondaryTextNumberOfLines,
         ellipsizeMode: suggestionTextProps.ellipsizeMode || 'tail',
@@ -388,10 +394,13 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   return /*#__PURE__*/_jsxs(View, {
+    testID: `container${testIDSuffix}`,
     style: [styles.container, style.container],
     children: [/*#__PURE__*/_jsxs(View, {
+      testID: `input-wrapper${testIDSuffix}`,
       children: [/*#__PURE__*/_jsx(TextInput, {
         ...restTextInputProps,
+        testID: `textinput${testIDSuffix}`,
         ref: inputRef,
         style: [styles.input, style.input, getPadding(), getTextAlign()],
         placeholder: placeHolderText,
@@ -405,6 +414,7 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
         accessibilityRole: "search",
         accessibilityLabel: accessibilityLabels.input || placeHolderText
       }), showClearButton && inputText !== '' && /*#__PURE__*/_jsx(TouchableOpacity, {
+        testID: `clear-button${testIDSuffix}`,
         style: [styles.clearButton, getIconPosition(12)],
         onPress: () => {
           if (debounceTimeout.current) {
@@ -433,6 +443,7 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
           })
         })
       }), (loading || detailsLoading) && showLoadingIndicator && /*#__PURE__*/_jsx(ActivityIndicator, {
+        testID: `loading-indicator${testIDSuffix}`,
         style: [styles.loadingIndicator, getIconPosition(45)],
         size: 'small',
         color: style.loadingIndicator?.color || '#000000',
@@ -440,8 +451,10 @@ const GooglePlacesTextInput = /*#__PURE__*/forwardRef(({
         accessibilityLabel: accessibilityLabels.loadingIndicator || 'Loading suggestions'
       })]
     }), showSuggestions && predictions.length > 0 && /*#__PURE__*/_jsx(View, {
+      testID: `suggestions-container${testIDSuffix}`,
       style: [styles.suggestionsContainer, style.suggestionsContainer],
       children: /*#__PURE__*/_jsx(FlatList, {
+        testID: `suggestions-list${testIDSuffix}`,
         data: predictions,
         renderItem: renderSuggestion,
         keyExtractor: item => item.placePrediction.placeId,
diff --git a/node_modules/react-native-google-places-textinput/src/GooglePlacesTextInput.tsx b/node_modules/react-native-google-places-textinput/src/GooglePlacesTextInput.tsx
index 8c7fd26..c8134fa 100644
--- a/node_modules/react-native-google-places-textinput/src/GooglePlacesTextInput.tsx
+++ b/node_modules/react-native-google-places-textinput/src/GooglePlacesTextInput.tsx
@@ -152,6 +152,11 @@ interface GooglePlacesTextInputProps
   enableDebug?: boolean;
   accessibilityLabels?: GooglePlacesAccessibilityLabels;
   suggestionTextProps?: SuggestionTextProps;
+  /**
+   * Test ID prefix for all internal components (for E2E testing)
+   * Components will be prefixed: textinput-{testID}, suggestion-touchableopacity-{index}-{testID}, etc.
+   */
+  testID?: string;
 }
 
 interface GooglePlacesTextInputRef {
@@ -204,6 +209,7 @@ const GooglePlacesTextInput = forwardRef<
       onBlur,
       accessibilityLabels = {},
       suggestionTextProps = {},
+      testID,
       ...restTextInputProps
     },
     ref
@@ -508,6 +514,9 @@ const GooglePlacesTextInput = forwardRef<
       }, 10);
     };
 
+    // Test ID suffix for all components
+    const testIDSuffix = testID ? `-${testID}` : '';
+
     const renderSuggestion = ({
       item,
       index,
@@ -533,6 +542,7 @@ const GooglePlacesTextInput = forwardRef<
 
       return (
         <TouchableOpacity
+          testID={`suggestion-touchableopacity-${index}${testIDSuffix}`}
           accessibilityRole="button"
           accessibilityLabel={accessibilityLabel}
           accessibilityHint="Double tap to select this place"
@@ -555,6 +565,7 @@ const GooglePlacesTextInput = forwardRef<
             } as any))}
         >
           <Text
+            testID={`suggestion-text-${index}${testIDSuffix}`}
             style={[
               styles.mainText,
               style.suggestionText?.main,
@@ -567,6 +578,7 @@ const GooglePlacesTextInput = forwardRef<
           </Text>
           {secondaryText && (
             <Text
+              testID={`suggestion-secondarytext-${index}${testIDSuffix}`}
               style={[
                 styles.secondaryText,
                 style.suggestionText?.secondary,
@@ -631,10 +643,11 @@ const GooglePlacesTextInput = forwardRef<
     }, []);
 
     return (
-      <View style={[styles.container, style.container]}>
-        <View>
+      <View testID={`container${testIDSuffix}`} style={[styles.container, style.container]}>
+        <View testID={`input-wrapper${testIDSuffix}`}>
           <TextInput
             {...restTextInputProps}
+            testID={`textinput${testIDSuffix}`}
             ref={inputRef}
             style={[styles.input, style.input, getPadding(), getTextAlign()]}
             placeholder={placeHolderText}
@@ -651,6 +664,7 @@ const GooglePlacesTextInput = forwardRef<
           {/* Clear button - shown only if showClearButton is true */}
           {showClearButton && inputText !== '' && (
             <TouchableOpacity
+              testID={`clear-button${testIDSuffix}`}
               style={[styles.clearButton, getIconPosition(12)]}
               onPress={() => {
                 if (debounceTimeout.current) {
@@ -692,6 +706,7 @@ const GooglePlacesTextInput = forwardRef<
           {/* Loading indicator */}
           {(loading || detailsLoading) && showLoadingIndicator && (
             <ActivityIndicator
+              testID={`loading-indicator${testIDSuffix}`}
               style={[styles.loadingIndicator, getIconPosition(45)]}
               size={'small'}
               color={style.loadingIndicator?.color || '#000000'}
@@ -706,9 +721,11 @@ const GooglePlacesTextInput = forwardRef<
         {/* Suggestions */}
         {showSuggestions && predictions.length > 0 && (
           <View
+            testID={`suggestions-container${testIDSuffix}`}
             style={[styles.suggestionsContainer, style.suggestionsContainer]}
           >
             <FlatList
+              testID={`suggestions-list${testIDSuffix}`}
               data={predictions}
               renderItem={renderSuggestion}
               keyExtractor={(item) => item.placePrediction.placeId}

This issue body was partially generated by patch-package.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions