diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
index c2e3023d0269..89a747246c31 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
@@ -950,7 +950,7 @@ private ThemedReactContext getReactContextForView(int reactTag) {
public void sendAccessibilityEvent(int tag, int eventType) {
View view = mTagsToViews.get(tag);
if (view == null) {
- throw new JSApplicationIllegalArgumentException("Could not find view with tag " + tag);
+ throw new RetryableMountingLayerException("Could not find view with tag " + tag);
}
view.sendAccessibilityEvent(eventType);
}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
index 93ccb0a9fbf9..de3ca5db4f18 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
@@ -591,7 +591,18 @@ private SendAccessibilityEvent(int tag, int eventType) {
@Override
public void execute() {
- mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType);
+ try {
+ mNativeViewHierarchyManager.sendAccessibilityEvent(mTag, mEventType);
+ } catch (RetryableMountingLayerException e) {
+ // Accessibility events are similar to commands in that they're imperative
+ // calls from JS, disconnected from the commit lifecycle, and therefore
+ // inherently unpredictable and dangerous. If we encounter a "retryable"
+ // error, that is, a known category of errors that this is likely to hit
+ // due to race conditions (like the view disappearing after the event is
+ // queued and before it executes), we log a soft exception and continue along.
+ // Other categories of errors will still cause a hard crash.
+ ReactSoftExceptionLogger.logSoftException(TAG, e);
+ }
}
}
diff --git a/packages/rn-tester/android/app/gradle.properties b/packages/rn-tester/android/app/gradle.properties
index 838ca18c4c1c..b6ce28ca151c 100644
--- a/packages/rn-tester/android/app/gradle.properties
+++ b/packages/rn-tester/android/app/gradle.properties
@@ -13,6 +13,6 @@ android.enableJetifier=true
FLIPPER_VERSION=0.182.0
# RN-Tester is building with NewArch always enabled
-newArchEnabled=true
+newArchEnabled=false
# RN-Tester is running with Hermes enabled and filtering variants with enableHermesOnlyInVariants
hermesEnabled=true
diff --git a/packages/rn-tester/js/CrashButton.js b/packages/rn-tester/js/CrashButton.js
new file mode 100644
index 000000000000..756d0e02ee4f
--- /dev/null
+++ b/packages/rn-tester/js/CrashButton.js
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { Button, Platform, View } from 'react-native';
+import { setAccessibilityFocus } from './setAccessibilityFocus';
+
+const isAndroid = Platform.OS === 'android';
+const timeoutMs = 250;
+
+export function CrashButton({ onPress }) {
+ const viewRef = React.useRef();
+
+ const handlePress = () => {
+ setAccessibilityFocus(viewRef.current, timeoutMs);
+ onPress();
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/rn-tester/js/RNTesterAppShared.js b/packages/rn-tester/js/RNTesterAppShared.js
index 3cd8e9d9afad..cb79687a5cfb 100644
--- a/packages/rn-tester/js/RNTesterAppShared.js
+++ b/packages/rn-tester/js/RNTesterAppShared.js
@@ -8,193 +8,77 @@
* @flow
*/
-import {RNTesterEmptyBookmarksState} from './components/RNTesterEmptyBookmarksState';
-import RNTesterModuleContainer from './components/RNTesterModuleContainer';
-import RNTesterModuleList from './components/RNTesterModuleList';
-import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar';
-import {RNTesterThemeContext, themes} from './components/RNTesterTheme';
-import RNTTitleBar from './components/RNTTitleBar';
-import RNTesterList from './utils/RNTesterList';
-import {
- RNTesterNavigationActionsType,
- RNTesterNavigationReducer,
-} from './utils/RNTesterNavigationReducer';
-import {
- Screens,
- getExamplesListWithBookmarksAndRecentlyUsed,
- initialNavigationState,
-} from './utils/testerStateUtils';
-import * as React from 'react';
-import {BackHandler, StyleSheet, View, useColorScheme} from 'react-native';
+// import {RNTesterEmptyBookmarksState} from './components/RNTesterEmptyBookmarksState';
+// import RNTesterModuleContainer from './components/RNTesterModuleContainer';
+// import RNTesterModuleList from './components/RNTesterModuleList';
+// import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar';
+// import {RNTesterThemeContext, themes} from './components/RNTesterTheme';
+// import RNTTitleBar from './components/RNTTitleBar';
+// import RNTesterList from './utils/RNTesterList';
+// import {
+// RNTesterNavigationActionsType,
+// RNTesterNavigationReducer,
+// } from './utils/RNTesterNavigationReducer';
+// import {
+// Screens,
+// getExamplesListWithBookmarksAndRecentlyUsed,
+// initialNavigationState,
+// } from './utils/testerStateUtils';
+// import * as React from 'react';
+// import {BackHandler, StyleSheet, View, useColorScheme} from 'react-native';
// RNTester App currently uses in memory storage for storing navigation state
-const RNTesterApp = (): React.Node => {
- const [state, dispatch] = React.useReducer(
- RNTesterNavigationReducer,
- initialNavigationState,
- );
- const colorScheme = useColorScheme();
-
- const {
- activeModuleKey,
- activeModuleTitle,
- activeModuleExampleKey,
- screen,
- bookmarks,
- recentlyUsed,
- } = state;
-
- const examplesList = React.useMemo(
- () =>
- getExamplesListWithBookmarksAndRecentlyUsed({bookmarks, recentlyUsed}),
- [bookmarks, recentlyUsed],
- );
-
- const handleBackPress = React.useCallback(() => {
- if (activeModuleKey != null) {
- dispatch({type: RNTesterNavigationActionsType.BACK_BUTTON_PRESS});
- }
- }, [dispatch, activeModuleKey]);
-
- // Setup hardware back button press listener
- React.useEffect(() => {
- const handleHardwareBackPress = () => {
- if (activeModuleKey) {
- handleBackPress();
- return true;
- }
- return false;
- };
-
- BackHandler.addEventListener('hardwareBackPress', handleHardwareBackPress);
-
- return () => {
- BackHandler.removeEventListener(
- 'hardwareBackPress',
- handleHardwareBackPress,
- );
- };
- }, [activeModuleKey, handleBackPress]);
-
- const handleModuleCardPress = React.useCallback(
- ({exampleType, key, title}: any) => {
- dispatch({
- type: RNTesterNavigationActionsType.MODULE_CARD_PRESS,
- data: {exampleType, key, title},
- });
- },
- [dispatch],
- );
-
- const handleModuleExampleCardPress = React.useCallback(
- (exampleName: string) => {
- dispatch({
- type: RNTesterNavigationActionsType.EXAMPLE_CARD_PRESS,
- data: {key: exampleName},
- });
- },
- [dispatch],
- );
-
- const toggleBookmark = React.useCallback(
- ({exampleType, key}: any) => {
- dispatch({
- type: RNTesterNavigationActionsType.BOOKMARK_PRESS,
- data: {exampleType, key},
- });
- },
- [dispatch],
- );
-
- const handleNavBarPress = React.useCallback(
- (args: {screen: string}) => {
- dispatch({
- type: RNTesterNavigationActionsType.NAVBAR_PRESS,
- data: {screen: args.screen},
- });
- },
- [dispatch],
- );
-
- const theme = colorScheme === 'dark' ? themes.dark : themes.light;
-
- if (examplesList === null) {
- return null;
- }
-
- const activeModule =
- activeModuleKey != null ? RNTesterList.Modules[activeModuleKey] : null;
- const activeModuleExample =
- activeModuleExampleKey != null
- ? activeModule?.examples.find(e => e.name === activeModuleExampleKey)
- : null;
- const title =
- activeModuleTitle != null
- ? activeModuleTitle
- : screen === Screens.COMPONENTS
- ? 'Components'
- : screen === Screens.APIS
- ? 'APIs'
- : 'Bookmarks';
-
- const activeExampleList =
- screen === Screens.COMPONENTS
- ? examplesList.components
- : screen === Screens.APIS
- ? examplesList.apis
- : examplesList.bookmarks;
-
- return (
-
-
-
- {activeModule != null ? (
-
- ) : screen === Screens.BOOKMARKS &&
- examplesList.bookmarks.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
-
- );
-};
-
-export default RNTesterApp;
+import * as React from 'react';
+import {
+ Button,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
+import {CrashButton} from './CrashButton';
+import {Timer} from './Timer';
+
+const isAndroid = Platform.OS === 'android';
const styles = StyleSheet.create({
container: {
flex: 1,
+ justifyContent: 'center',
+ paddingTop: StatusBar.currentHeight,
+ backgroundColor: '#ecf0f1',
+ padding: 8,
},
- bottomNavbar: {
- height: navBarHeight,
- },
- hidden: {
- display: 'none',
+ paragraph: {
+ marginBottom: 24,
+ fontSize: 18,
+ fontWeight: 'bold',
+ textAlign: 'center',
},
});
+
+export default function App() {
+ const [showButton, setShowButton] = React.useState(true);
+
+ return (
+
+
+ {isAndroid
+ ? `Click the button below and a crash will occur`
+ : 'Switch to Android to reproduce this crash'}
+
+ {isAndroid ? : null}
+ {showButton ? (
+ setShowButton(false)} />
+ ) : (
+
+ );
+}
diff --git a/packages/rn-tester/js/Timer.js b/packages/rn-tester/js/Timer.js
new file mode 100644
index 000000000000..82ccb6e7605d
--- /dev/null
+++ b/packages/rn-tester/js/Timer.js
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { Text } from 'react-native';
+
+export function Timer() {
+ const [currentTime, setCurrentTime] = React.useState(0);
+
+ React.useEffect(() => {
+ const intervalId = setInterval(() => {
+ setCurrentTime(time => time + 1);
+ }, 1000);
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, []);
+
+ return Elapsed time {currentTime}s;
+}
diff --git a/packages/rn-tester/js/debounce.js b/packages/rn-tester/js/debounce.js
new file mode 100644
index 000000000000..8cdf1d751175
--- /dev/null
+++ b/packages/rn-tester/js/debounce.js
@@ -0,0 +1,26 @@
+export function debounce(interval, operation) {
+ let lastArgs;
+ let lastResult;
+ let timer = null;
+
+ function cancel() {
+ clearTimeout(timer);
+ timer = null;
+ }
+
+ function runner() {
+ cancel();
+ lastResult = operation(...lastArgs);
+ }
+
+ function debounced(...args) {
+ lastArgs = args;
+ cancel();
+ timer = setTimeout(runner, interval);
+ return lastResult;
+ }
+
+ debounced.cancel = cancel;
+
+ return debounced;
+}
diff --git a/packages/rn-tester/js/setAccessibilityFocus.js b/packages/rn-tester/js/setAccessibilityFocus.js
new file mode 100644
index 000000000000..4e6e392ba2a8
--- /dev/null
+++ b/packages/rn-tester/js/setAccessibilityFocus.js
@@ -0,0 +1,19 @@
+import { AccessibilityInfo, findNodeHandle } from 'react-native';
+import { debounce } from './debounce';
+
+export const setAccessibilityFocus = (node, delayMs = 0) => {
+ const debounced = debounce(delayMs, () => {
+ console.log('executing');
+ if (node) {
+ const reactTag = findNodeHandle(node);
+ console.log('found react tag', reactTag);
+ if (reactTag) {
+ AccessibilityInfo.setAccessibilityFocus(reactTag);
+ }
+ }
+ });
+ // Invoke the timeout
+ debounced();
+ // Return callback op so we can cancel it on unmount
+ return debounced;
+};