Skip to content

Commit 136e215

Browse files
feat: support react native buttons label extraction in user steps (#1109)
Jira ID: - IBGCRASH-21078 - IBGCRASH-21213
1 parent 28c4e06 commit 136e215

File tree

5 files changed

+315
-13
lines changed

5 files changed

+315
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Added
66

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

910
### Changed
1011

android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.instabug.library.IssueType;
2828
import com.instabug.library.LogLevel;
2929
import com.instabug.library.ReproConfigurations;
30+
import com.instabug.library.core.InstabugCore;
3031
import com.instabug.library.internal.module.InstabugLocale;
3132
import com.instabug.library.invocation.InstabugInvocationEvent;
3233
import com.instabug.library.logging.InstabugLog;
@@ -37,6 +38,7 @@
3738
import com.instabug.reactlibrary.utils.EventEmitterModule;
3839
import com.instabug.reactlibrary.utils.MainThreadHandler;
3940

41+
import com.instabug.reactlibrary.utils.RNTouchedViewExtractor;
4042
import org.json.JSONException;
4143
import org.json.JSONObject;
4244
import org.json.JSONTokener;
@@ -133,6 +135,8 @@ public void init(
133135
MainThreadHandler.runOnMainThread(new Runnable() {
134136
@Override
135137
public void run() {
138+
final RNTouchedViewExtractor rnTouchedViewExtractor = new RNTouchedViewExtractor();
139+
InstabugCore.setTouchedViewExtractorExtension(rnTouchedViewExtractor);
136140
final ArrayList<String> keys = ArrayUtil.parseReadableArrayOfStrings(invocationEventValues);
137141
final ArrayList<InstabugInvocationEvent> parsedInvocationEvents = ArgsRegistry.invocationEvents.getAll(keys);
138142
final InstabugInvocationEvent[] invocationEvents = parsedInvocationEvents.toArray(new InstabugInvocationEvent[0]);
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package com.instabug.reactlibrary.utils;
2+
3+
import android.text.TextUtils;
4+
import android.view.View;
5+
import android.view.ViewParent;
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
import com.facebook.react.views.text.ReactTextView;
9+
import com.facebook.react.views.view.ReactViewGroup;
10+
import com.instabug.library.core.InstabugCore;
11+
import com.instabug.library.visualusersteps.TouchedView;
12+
import com.instabug.library.visualusersteps.TouchedViewExtractor;
13+
import com.instabug.library.visualusersteps.VisualUserStepsHelper;
14+
15+
public class RNTouchedViewExtractor implements TouchedViewExtractor {
16+
17+
private final int depthTraversalLimit = 3;
18+
19+
/**
20+
* Determines whether the native Android SDK should depend on native extraction
21+
* when a label is not found by the RNTouchedViewExtractor.
22+
*
23+
* <p>
24+
* - {@code RNTouchedViewExtractor} tries to find a label.
25+
* <br>
26+
* - If it returns a label, the view is labeled with the one returned.
27+
* <br>
28+
* - If it returns {@code null}:
29+
* <br>
30+
* - If {@code shouldDependOnNative} is {@code true}, the native Android SDK
31+
* will try to extract the label from the view.
32+
* <br>
33+
* - If it's {@code false}, the Android SDK will label it {@code null} as returned
34+
* from {@code RNTouchedViewExtractor} without trying to label it.
35+
* </p>
36+
*
37+
* @return {@code true} if the native Android SDK should depend on native extraction,
38+
* {@code false} otherwise.
39+
*/
40+
@Override
41+
public boolean getShouldDependOnNative() {
42+
return true;
43+
}
44+
45+
46+
@Nullable
47+
@Override
48+
public TouchedView extract(@NonNull View view, @NonNull TouchedView touchedView) {
49+
ReactViewGroup reactViewGroup = findReactButtonViewGroup(view);
50+
// If no button is found return `null` to leave the extraction of the touched view to the native Android SDK.
51+
if (reactViewGroup == null) return null;
52+
return getExtractionStrategy(reactViewGroup).extract(reactViewGroup, touchedView);
53+
}
54+
55+
@Nullable
56+
private ReactViewGroup findReactButtonViewGroup(@NonNull View startView) {
57+
if (isReactButtonViewGroup(startView)) return (ReactViewGroup) startView;
58+
ViewParent currentParent = startView.getParent();
59+
int depth = 1;
60+
do {
61+
if (currentParent == null || isReactButtonViewGroup(currentParent))
62+
return (ReactViewGroup) currentParent;
63+
currentParent = currentParent.getParent();
64+
depth++;
65+
} while (depth < depthTraversalLimit);
66+
return null;
67+
}
68+
69+
private boolean isReactButtonViewGroup(@NonNull View view) {
70+
return (view instanceof ReactViewGroup) && view.isFocusable() && view.isClickable();
71+
}
72+
73+
private boolean isReactButtonViewGroup(@NonNull ViewParent viewParent) {
74+
if (!(viewParent instanceof ReactViewGroup)) return false;
75+
ReactViewGroup group = (ReactViewGroup) viewParent;
76+
return group.isFocusable() && group.isClickable();
77+
}
78+
79+
private ReactButtonExtractionStrategy getExtractionStrategy(ReactViewGroup reactButton) {
80+
boolean isPrivateView = VisualUserStepsHelper.isPrivateView(reactButton);
81+
if (isPrivateView) return new PrivateViewLabelExtractionStrategy();
82+
83+
int labelsCount = 0;
84+
int groupsCount = 0;
85+
for (int index = 0; index < reactButton.getChildCount(); index++) {
86+
View currentView = reactButton.getChildAt(index);
87+
if (currentView instanceof ReactTextView) {
88+
89+
labelsCount++;
90+
continue;
91+
}
92+
if (currentView instanceof ReactViewGroup) {
93+
groupsCount++;
94+
}
95+
}
96+
if (labelsCount > 1 || groupsCount > 0) return new MultiLabelsExtractionStrategy();
97+
if (labelsCount == 1) return new SingleLabelExtractionStrategy();
98+
return new NoLabelsExtractionStrategy();
99+
}
100+
101+
interface ReactButtonExtractionStrategy {
102+
@Nullable
103+
TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView);
104+
}
105+
106+
class MultiLabelsExtractionStrategy implements ReactButtonExtractionStrategy {
107+
private final String MULTI_LABEL_BUTTON_PRE_STRING = "a button that contains \"%s\"";
108+
109+
@Override
110+
@Nullable
111+
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
112+
113+
touchedView.setProminentLabel(
114+
InstabugCore.composeProminentLabelForViewGroup(reactButton, MULTI_LABEL_BUTTON_PRE_STRING)
115+
);
116+
return touchedView;
117+
}
118+
}
119+
120+
class PrivateViewLabelExtractionStrategy implements ReactButtonExtractionStrategy {
121+
122+
private final String PRIVATE_VIEW_LABEL_BUTTON_PRE_STRING = "a button";
123+
124+
@Override
125+
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
126+
touchedView.setProminentLabel(PRIVATE_VIEW_LABEL_BUTTON_PRE_STRING);
127+
return touchedView;
128+
}
129+
}
130+
131+
class SingleLabelExtractionStrategy implements ReactButtonExtractionStrategy {
132+
133+
@Override
134+
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
135+
ReactTextView targetLabel = null;
136+
for (int index = 0; index < reactButton.getChildCount(); index++) {
137+
View currentView = reactButton.getChildAt(index);
138+
if (!(currentView instanceof ReactTextView)) continue;
139+
targetLabel = (ReactTextView) currentView;
140+
break;
141+
}
142+
if (targetLabel == null) return touchedView;
143+
144+
String labelText = getLabelText(targetLabel);
145+
touchedView.setProminentLabel(InstabugCore.composeProminentLabelFor(labelText, false));
146+
return touchedView;
147+
}
148+
149+
@Nullable
150+
private String getLabelText(ReactTextView textView) {
151+
String labelText = null;
152+
if (!TextUtils.isEmpty(textView.getText())) {
153+
labelText = textView.getText().toString();
154+
} else if (!TextUtils.isEmpty(textView.getContentDescription())) {
155+
labelText = textView.getContentDescription().toString();
156+
}
157+
return labelText;
158+
}
159+
}
160+
161+
class NoLabelsExtractionStrategy implements ReactButtonExtractionStrategy {
162+
@Override
163+
public TouchedView extract(ReactViewGroup reactButton, TouchedView touchedView) {
164+
return touchedView;
165+
}
166+
}
167+
}

examples/default/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native';
33

44
import { GestureHandlerRootView } from 'react-native-gesture-handler';
55
import { NavigationContainer } from '@react-navigation/native';
6-
import Instabug, { InvocationEvent, LogLevel } from 'instabug-reactnative';
6+
import Instabug, { InvocationEvent, LogLevel, ReproStepsMode } from 'instabug-reactnative';
77
import { NativeBaseProvider } from 'native-base';
88

99
import { RootTabNavigator } from './navigation/RootTab';
@@ -17,6 +17,10 @@ export const App: React.FC = () => {
1717
invocationEvents: [InvocationEvent.floatingButton],
1818
debugLogsLevel: LogLevel.verbose,
1919
});
20+
21+
Instabug.setReproStepsConfig({
22+
all: ReproStepsMode.enabled,
23+
});
2024
}, []);
2125

2226
return (

0 commit comments

Comments
 (0)