Skip to content

Commit

Permalink
feat: support react native buttons label extraction in user steps (#1109
Browse files Browse the repository at this point in the history
)

Jira ID:
- IBGCRASH-21078
- IBGCRASH-21213
  • Loading branch information
abdelhamid-f-nasser authored and HeshamMegid committed Feb 10, 2024
1 parent 0929a3c commit 1e36d58
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 13 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

### Added

- Support user identification using ID ([#1115](https://github.com/Instabug/Instabug-React-Native/pull/1115))
- Support user identification using ID ([#1115](https://github.com/Instabug/Instabug-React-Native/pull/1115)).
- Support button detection and label extraction for repro steps ([#1109](https://github.com/Instabug/Instabug-React-Native/pull/1109)).

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> keys = ArrayUtil.parseReadableArrayOfStrings(invocationEventValues);
final ArrayList<InstabugInvocationEvent> parsedInvocationEvents = ArgsRegistry.invocationEvents.getAll(keys);
final InstabugInvocationEvent[] invocationEvents = parsedInvocationEvents.toArray(new InstabugInvocationEvent[0]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.instabug.reactlibrary.utils;

import android.text.TextUtils;
import android.view.View;
import android.view.ViewParent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.views.text.ReactTextView;
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;
import com.instabug.library.visualusersteps.VisualUserStepsHelper;

public class RNTouchedViewExtractor implements TouchedViewExtractor {

private final int depthTraversalLimit = 3;

/**
* Determines whether the native Android SDK should depend on native extraction
* when a label is not found by the RNTouchedViewExtractor.
*
* <p>
* - {@code RNTouchedViewExtractor} tries to find a label.
* <br>
* - If it returns a label, the view is labeled with the one returned.
* <br>
* - If it returns {@code null}:
* <br>
* - If {@code shouldDependOnNative} is {@code true}, the native Android SDK
* will try to extract the label from the view.
* <br>
* - If it's {@code false}, the Android SDK will label it {@code null} as returned
* from {@code RNTouchedViewExtractor} without trying to label it.
* </p>
*
* @return {@code true} if the native Android SDK should depend on native extraction,
* {@code false} otherwise.
*/
@Override
public boolean getShouldDependOnNative() {
return true;
}


@Nullable
@Override
public TouchedView extract(@NonNull View view, @NonNull TouchedView touchedView) {
ReactViewGroup reactViewGroup = findReactButtonViewGroup(view);
// If no button is found return `null` to leave the extraction of the touched view to the native Android SDK.
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 depth = 1;
do {
if (currentParent == null || isReactButtonViewGroup(currentParent))
return (ReactViewGroup) currentParent;
currentParent = currentParent.getParent();
depth++;
} while (depth < depthTraversalLimit);
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;
ReactViewGroup group = (ReactViewGroup) viewParent;
return group.isFocusable() && group.isClickable();
}

private ReactButtonExtractionStrategy getExtractionStrategy(ReactViewGroup reactButton) {
boolean isPrivateView = VisualUserStepsHelper.isPrivateView(reactButton);
if (isPrivateView) return new PrivateViewLabelExtractionStrategy();

int labelsCount = 0;
int groupsCount = 0;
for (int index = 0; index < reactButton.getChildCount(); index++) {
View currentView = reactButton.getChildAt(index);
if (currentView instanceof ReactTextView) {

labelsCount++;
continue;
}
if (currentView instanceof ReactViewGroup) {
groupsCount++;
}
}
if (labelsCount > 1 || groupsCount > 0) return new MultiLabelsExtractionStrategy();
if (labelsCount == 1) return new SingleLabelExtractionStrategy();
return new NoLabelsExtractionStrategy();
}

interface ReactButtonExtractionStrategy {
@Nullable
TouchedView extract(ReactViewGroup 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(ReactViewGroup reactButton, TouchedView touchedView) {

touchedView.setProminentLabel(
InstabugCore.composeProminentLabelForViewGroup(reactButton, MULTI_LABEL_BUTTON_PRE_STRING)
);
return touchedView;
}
}

class PrivateViewLabelExtractionStrategy implements ReactButtonExtractionStrategy {

private final String PRIVATE_VIEW_LABEL_BUTTON_PRE_STRING = "a button";

@Override
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
touchedView.setProminentLabel(PRIVATE_VIEW_LABEL_BUTTON_PRE_STRING);
return touchedView;
}
}

class SingleLabelExtractionStrategy implements ReactButtonExtractionStrategy {

@Override
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
ReactTextView targetLabel = null;
for (int index = 0; index < reactButton.getChildCount(); index++) {
View currentView = reactButton.getChildAt(index);
if (!(currentView instanceof ReactTextView)) continue;
targetLabel = (ReactTextView) currentView;
break;
}
if (targetLabel == null) return touchedView;

String labelText = getLabelText(targetLabel);
touchedView.setProminentLabel(InstabugCore.composeProminentLabelFor(labelText, false));
return touchedView;
}

@Nullable
private String getLabelText(ReactTextView 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(ReactViewGroup reactButton, TouchedView touchedView) {
return touchedView;
}
}
}
6 changes: 5 additions & 1 deletion examples/default/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,6 +17,10 @@ export const App: React.FC = () => {
invocationEvents: [InvocationEvent.floatingButton],
debugLogsLevel: LogLevel.verbose,
});

Instabug.setReproStepsConfig({
all: ReproStepsMode.enabled,
});
}, []);

return (
Expand Down
Loading

0 comments on commit 1e36d58

Please sign in to comment.