From bba3569d884f367b5b450e8bac34053ccc9a7f00 Mon Sep 17 00:00:00 2001 From: AbdElHamid Nasser Date: Sun, 28 Jan 2024 14:32:02 +0200 Subject: [PATCH 01/11] feat(android): support repro-steps for button Support extracting the label of the button in repro-steps via TouchedViewExtractor --- .../RNInstabugReactnativeModule.java | 4 + .../utils/RNTouchedViewExtractor.java | 133 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 android/src/main/java/com/instabug/reactlibrary/utils/RNTouchedViewExtractor.java diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index 32bf02a30f..514b98f7d2 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -27,6 +27,7 @@ import com.instabug.library.IssueType; import com.instabug.library.LogLevel; import com.instabug.library.ReproConfigurations; +import com.instabug.library.core.InstabugCore; import com.instabug.library.internal.module.InstabugLocale; import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.logging.InstabugLog; @@ -37,6 +38,7 @@ import com.instabug.reactlibrary.utils.EventEmitterModule; import com.instabug.reactlibrary.utils.MainThreadHandler; +import com.instabug.reactlibrary.utils.RNTouchedViewExtractor; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; @@ -133,6 +135,8 @@ public void init( MainThreadHandler.runOnMainThread(new Runnable() { @Override public void run() { + final RNTouchedViewExtractor rnTouchedViewExtractor = new RNTouchedViewExtractor(); + InstabugCore.setTouchedViewExtractorExtension(rnTouchedViewExtractor); final ArrayList keys = ArrayUtil.parseReadableArrayOfStrings(invocationEventValues); final ArrayList parsedInvocationEvents = ArgsRegistry.invocationEvents.getAll(keys); final InstabugInvocationEvent[] invocationEvents = parsedInvocationEvents.toArray(new InstabugInvocationEvent[0]); diff --git a/android/src/main/java/com/instabug/reactlibrary/utils/RNTouchedViewExtractor.java b/android/src/main/java/com/instabug/reactlibrary/utils/RNTouchedViewExtractor.java new file mode 100644 index 0000000000..ee4ccc53a6 --- /dev/null +++ b/android/src/main/java/com/instabug/reactlibrary/utils/RNTouchedViewExtractor.java @@ -0,0 +1,133 @@ +package com.instabug.reactlibrary.utils; + +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.react.views.view.ReactViewGroup; +import com.instabug.library.core.InstabugCore; +import com.instabug.library.visualusersteps.TouchedView; +import com.instabug.library.visualusersteps.TouchedViewExtractor; + +public class RNTouchedViewExtractor implements TouchedViewExtractor { + @Override + public boolean getShouldDependOnNative() { + return true; + } + + @Nullable + @Override + public TouchedView extract(@NonNull View view, @NonNull TouchedView touchedView) { + ReactViewGroup reactViewGroup = findReactButtonViewGroup(view); + if (reactViewGroup == null) return null; + return getExtractionStrategy(reactViewGroup).extract(reactViewGroup, touchedView); + } + + @Nullable + private ReactViewGroup findReactButtonViewGroup(@NonNull View startView) { + if (isReactButtonViewGroup(startView)) return (ReactViewGroup) startView; + ViewParent currentParent = startView.getParent(); + int iteratorIndex = 0; + do { + if (currentParent == null || isReactButtonViewGroup(currentParent)) + return (ReactViewGroup) currentParent; + currentParent = currentParent.getParent(); + iteratorIndex++; + } while (iteratorIndex < 2); + return null; + } + + private boolean isReactButtonViewGroup(@NonNull View view) { + return (view instanceof ReactViewGroup) && view.isFocusable() && view.isClickable(); + } + + private boolean isReactButtonViewGroup(@NonNull ViewParent viewParent) { + if (!(viewParent instanceof ReactViewGroup)) return false; + ViewGroup group = (ReactViewGroup) viewParent; + return group.isFocusable() && group.isClickable(); + } + + private ReactButtonExtractionStrategy getExtractionStrategy(ReactViewGroup reactButton){ + int labelsCount = 0; + int groupsCount = 0; + for (int index=0; index < reactButton.getChildCount(); index++){ + View currentView = reactButton.getChildAt(index); + if (currentView instanceof TextView) { + + labelsCount++; + continue; + } + if (currentView instanceof ViewGroup) { + groupsCount++; + } + } + if (labelsCount > 1 || groupsCount > 0) return new MultiLabelsExtractionStrategy(); + if (labelsCount == 1) return new SingleLabelExtractionStrategy(); + return new NoLabelsExtractionStrategy(); + } + + interface ReactButtonExtractionStrategy { + @Nullable + TouchedView extract(ViewGroup reactButton, TouchedView touchedView); + } + + class MultiLabelsExtractionStrategy implements ReactButtonExtractionStrategy { + private final String MULTI_LABEL_BUTTON_PRE_STRING = "A button that contains \"%s\""; + + @Override + @Nullable + public TouchedView extract(ViewGroup reactButton, TouchedView touchedView) { + + touchedView.setProminentLabel( + InstabugCore.composeProminentLabelForViewGroup(reactButton, MULTI_LABEL_BUTTON_PRE_STRING) + ); + return touchedView; + } + } + + class SingleLabelExtractionStrategy implements ReactButtonExtractionStrategy { + + @Override + public TouchedView extract(ViewGroup reactButton, TouchedView touchedView) { + TextView targetLabel = null; + for (int index = 0; index < reactButton.getChildCount(); index++) { + View currentView = reactButton.getChildAt(index); + if (!(currentView instanceof TextView)) continue; + targetLabel = (TextView) currentView; + break; + } + if (targetLabel == null) return touchedView; + + String labelText = getLabelText(targetLabel); + touchedView.setProminentLabel(InstabugCore.composeProminentLabelFor(labelText, false)); + return touchedView; + } + + @Nullable + private String getLabelText(TextView textView) { + String labelText = null; + if (!TextUtils.isEmpty(textView.getText())) { + labelText = textView.getText().toString(); + } else if (!TextUtils.isEmpty(textView.getContentDescription())) { + labelText = textView.getContentDescription().toString(); + } + return labelText; + } + } + + class NoLabelsExtractionStrategy implements ReactButtonExtractionStrategy { + + @Override + public TouchedView extract(ViewGroup reactButton, TouchedView touchedView) { + touchedView.setProminentLabel( + InstabugCore.composeProminentLabelFor(null, false) + ); + return touchedView; + } + } +} From ae914ab6f494375ff1418609c57be7e0f056904f Mon Sep 17 00:00:00 2001 From: AbdElHamid Nasser Date: Sun, 28 Jan 2024 17:35:44 +0200 Subject: [PATCH 02/11] feat(example): add repro-steps button samples Added buttons for testing purposes. --- examples/default/src/App.tsx | 6 ++- .../user-steps/BasicComponentsScreen.tsx | 47 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/examples/default/src/App.tsx b/examples/default/src/App.tsx index 6dff03618d..7f302c5d1e 100644 --- a/examples/default/src/App.tsx +++ b/examples/default/src/App.tsx @@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { NavigationContainer } from '@react-navigation/native'; -import Instabug, { InvocationEvent, LogLevel } from 'instabug-reactnative'; +import Instabug, { InvocationEvent, LogLevel, ReproStepsMode } from 'instabug-reactnative'; import { NativeBaseProvider } from 'native-base'; import { RootTabNavigator } from './navigation/RootTab'; @@ -17,6 +17,10 @@ export const App: React.FC = () => { invocationEvents: [InvocationEvent.floatingButton], debugLogsLevel: LogLevel.verbose, }); + + Instabug.setReproStepsConfig({ + all: ReproStepsMode.enabled, + }); }, []); return ( diff --git a/examples/default/src/screens/user-steps/BasicComponentsScreen.tsx b/examples/default/src/screens/user-steps/BasicComponentsScreen.tsx index 16aa55b8c5..4cc53be3cb 100644 --- a/examples/default/src/screens/user-steps/BasicComponentsScreen.tsx +++ b/examples/default/src/screens/user-steps/BasicComponentsScreen.tsx @@ -10,6 +10,7 @@ import { Switch, useWindowDimensions, ActivityIndicator, + View, } from 'react-native'; import Slider from '@react-native-community/slider'; import { Center, HStack, ScrollView, VStack } from 'native-base'; @@ -17,10 +18,11 @@ import { Center, HStack, ScrollView, VStack } from 'native-base'; import { Screen } from '../../components/Screen'; import { Section } from '../../components/Section'; import { nativeBaseTheme } from '../../theme/nativeBaseTheme'; +import Icon from 'react-native-vector-icons/Ionicons'; import { InputField } from '../../components/InputField'; /** - * A screen that demonstates the usage of user steps with basic React Native components. + * A screen that demonstrates the usage of user steps with basic React Native components. * * This specific screen doesn't use NativeBase in some parts since we need to focus on * capturing React Native provided components rather than implementations built on top of it. @@ -62,13 +64,51 @@ export const BasicComponentsScreen: React.FC = () => {