Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demonstrate crash behavior #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/rn-tester/android/app/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions packages/rn-tester/js/CrashButton.js
Original file line number Diff line number Diff line change
@@ -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 (
<View accessible={true} ref={viewRef}>
<Button
disabled={!isAndroid}
onPress={handlePress}
title="Trigger Crash"
/>
</View>
);
}
244 changes: 64 additions & 180 deletions packages/rn-tester/js/RNTesterAppShared.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<RNTesterThemeContext.Provider value={theme}>
<RNTTitleBar
title={title}
theme={theme}
onBack={activeModule ? handleBackPress : null}
documentationURL={activeModule?.documentationURL}
/>
<View
style={StyleSheet.compose(styles.container, {
backgroundColor: theme.GroupedBackgroundColor,
})}>
{activeModule != null ? (
<RNTesterModuleContainer
module={activeModule}
example={activeModuleExample}
onExampleCardPress={handleModuleExampleCardPress}
/>
) : screen === Screens.BOOKMARKS &&
examplesList.bookmarks.length === 0 ? (
<RNTesterEmptyBookmarksState />
) : (
<RNTesterModuleList
sections={activeExampleList}
toggleBookmark={toggleBookmark}
handleModuleCardPress={handleModuleCardPress}
/>
)}
</View>
<View style={styles.bottomNavbar}>
<RNTesterNavBar
screen={screen || Screens.COMPONENTS}
isExamplePageOpen={!!activeModule}
handleNavBarPress={handleNavBarPress}
/>
</View>
</RNTesterThemeContext.Provider>
);
};

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 (
<View style={styles.container}>
<Text style={styles.paragraph}>
{isAndroid
? `Click the button below and a crash will occur`
: 'Switch to Android to reproduce this crash'}
</Text>
<Text style={styles.paragraph}>{isAndroid ? <Timer /> : null}</Text>
{showButton ? (
<CrashButton onPress={() => setShowButton(false)} />
) : (
<Button
disabled={!isAndroid}
title="Reset"
onPress={() => setShowButton(true)}
/>
)}
</View>
);
}
17 changes: 17 additions & 0 deletions packages/rn-tester/js/Timer.js
Original file line number Diff line number Diff line change
@@ -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 <Text>Elapsed time {currentTime}s</Text>;
}
26 changes: 26 additions & 0 deletions packages/rn-tester/js/debounce.js
Original file line number Diff line number Diff line change
@@ -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;
}
19 changes: 19 additions & 0 deletions packages/rn-tester/js/setAccessibilityFocus.js
Original file line number Diff line number Diff line change
@@ -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;
};