Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: callstack/react-native-testing-library
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v12.0.1
Choose a base ref
...
head repository: callstack/react-native-testing-library
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v12.1.0
Choose a head ref
  • 11 commits
  • 50 files changed
  • 4 contributors

Commits on Mar 31, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2c8909f View commit details

Commits on Apr 4, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c6f976f View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7eb1114 View commit details

Commits on Apr 5, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7b02f51 View commit details

Commits on Apr 21, 2023

  1. fix: docs link to GitHub (#1390)

    Stephen Hanson authored Apr 21, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4ac2833 View commit details

Commits on Apr 24, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    78194d1 View commit details

Commits on Apr 25, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e2073e2 View commit details
  2. fix: experiments app

    mdjastrzebski committed Apr 25, 2023
    Copy the full SHA
    5f770cb View commit details

Commits on Apr 27, 2023

  1. feat: Render element tree in query error messages (#1378)

    Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
    Stephen Hanson and mdjastrzebski authored Apr 27, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f7c8400 View commit details
  2. fix: proper stack trace for findBy* and findAllBy* queries (#1394)

    * fix: fix stacktrace for findBy* queries
    * chore: cleanup
    mdjastrzebski authored Apr 27, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1764963 View commit details

Commits on Apr 28, 2023

  1. v12.1.0

    mdjastrzebski committed Apr 28, 2023
    Copy the full SHA
    63bc512 View commit details
Showing with 9,144 additions and 631 deletions.
  1. +1 −1 CONTRIBUTING.md
  2. +17 −0 experiments-app/.gitignore
  3. +5 −0 experiments-app/.prettierrc.js
  4. +30 −0 experiments-app/app.json
  5. BIN experiments-app/assets/adaptive-icon.png
  6. BIN experiments-app/assets/favicon.png
  7. BIN experiments-app/assets/icon.png
  8. BIN experiments-app/assets/splash.png
  9. +6 −0 experiments-app/babel.config.js
  10. +4 −0 experiments-app/index.js
  11. +31 −0 experiments-app/package.json
  12. +31 −0 experiments-app/src/App.tsx
  13. +51 −0 experiments-app/src/MainScreen.tsx
  14. +17 −0 experiments-app/src/experiments.ts
  15. +54 −0 experiments-app/src/screens/TextInputEventPropagation.tsx
  16. +50 −0 experiments-app/src/screens/TextInputEvents.tsx
  17. +8 −0 experiments-app/src/utils/helpers.ts
  18. +6 −0 experiments-app/tsconfig.json
  19. +6,913 −0 experiments-app/yarn.lock
  20. +3 −0 jest-setup.ts
  21. +5 −2 package.json
  22. +9 −9 src/__tests__/__snapshots__/render-debug.test.tsx.snap
  23. +0 −20 src/__tests__/fireEvent.test.tsx
  24. +7 −24 src/__tests__/host-component-names.test.tsx
  25. +7 −14 src/__tests__/react-native-api.test.tsx
  26. +9 −0 src/__tests__/waitFor.test.tsx
  27. +0 −8 src/__tests__/within.test.tsx
  28. +3 −4 src/fireEvent.ts
  29. +3 −7 src/helpers/__tests__/component-tree.test.tsx
  30. +114 −0 src/helpers/__tests__/format-default.tsx
  31. +72 −0 src/helpers/format-default.ts
  32. +7 −2 src/helpers/format.ts
  33. +15 −14 src/helpers/host-component-names.tsx
  34. +88 −3 src/queries/__tests__/a11yState.test.tsx
  35. +101 −25 src/queries/__tests__/a11yValue.test.tsx
  36. +102 −14 src/queries/__tests__/displayValue.test.tsx
  37. +54 −5 src/queries/__tests__/hintText.test.tsx
  38. +54 −5 src/queries/__tests__/labelText.test.tsx
  39. +235 −0 src/queries/__tests__/makeQueries.test.tsx
  40. +53 −5 src/queries/__tests__/placeholderText.test.tsx
  41. +91 −18 src/queries/__tests__/role-value.test.tsx
  42. +94 −34 src/queries/__tests__/role.test.tsx
  43. +53 −5 src/queries/__tests__/testId.test.tsx
  44. +57 −10 src/queries/__tests__/text.test.tsx
  45. +97 −18 src/queries/makeQueries.ts
  46. +12 −4 src/waitFor.ts
  47. +1 −1 typings/index.flow.js
  48. +6 −6 website/docs/MigrationV12.md
  49. +1 −1 website/docusaurus.config.js
  50. +567 −372 yarn.lock
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ The core team works directly on GitHub and all work is public.

### Development workflow

> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
1. Fork the repo and create your branch from `main` (a guide on [how to fork a repository](https://help.github.com/articles/fork-a-repo/)).
2. Run `yarn` to setup the development environment.
17 changes: 17 additions & 0 deletions experiments-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/

# macOS
.DS_Store

# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
5 changes: 5 additions & 0 deletions experiments-app/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// added for Jest inline snapshots to not use default Prettier config
module.exports = {
singleQuote: true,
trailingComma: "es5"
}
30 changes: 30 additions & 0 deletions experiments-app/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expo": {
"name": "experiments-app",
"slug": "experiments-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file added experiments-app/assets/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added experiments-app/assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added experiments-app/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added experiments-app/assets/splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions experiments-app/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};
4 changes: 4 additions & 0 deletions experiments-app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import registerRootComponent from 'expo/build/launch/registerRootComponent';
import App from './src/App';

registerRootComponent(App);
31 changes: 31 additions & 0 deletions experiments-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "experiments-app",
"private": true,
"description": "Expo app for conducting experiments of React Native behaviour.",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"typecheck": "tsc -noEmit",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"add": "^2.0.6",
"expo": "~48.0.11",
"expo-status-bar": "~1.4.4",
"react": "18.2.0",
"react-native": "0.71.6",
"react-native-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0",
"yarn": "^1.22.19"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.0.14",
"typescript": "^4.9.4"
}
}
31 changes: 31 additions & 0 deletions experiments-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { MainScreen } from './MainScreen';
import { experiments } from './experiments';

const Stack = createNativeStackNavigator();

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="main"
component={MainScreen}
options={{ title: 'Experiments' }}
/>
{experiments.map((exp) => (
<Stack.Screen
key={exp.key}
name={exp.key}
component={exp.component}
options={{ title: exp.title }}
/>
))}
</Stack.Navigator>
<StatusBar style="auto" />
</NavigationContainer>
);
}
51 changes: 51 additions & 0 deletions experiments-app/src/MainScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import {
StyleSheet,
SafeAreaView,
Text,
FlatList,
Pressable,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { Experiment, experiments } from './experiments';

export function MainScreen() {
return (
<SafeAreaView style={styles.container}>
<FlatList
data={experiments}
renderItem={({ item }) => <ListItem item={item} />}
/>
</SafeAreaView>
);
}

interface ListItemProps {
item: Experiment;
}

function ListItem({ item }: ListItemProps) {
const navigation = useNavigation();

const handlePress = () => {
navigation.navigate(item.key);
};

return (
<Pressable style={styles.item} onPress={handlePress}>
<Text style={styles.itemTitle}>{item.title}</Text>
</Pressable>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
padding: 20,
},
itemTitle: {
fontSize: 20,
},
});
17 changes: 17 additions & 0 deletions experiments-app/src/experiments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TextInputEventPropagation } from './screens/TextInputEventPropagation';
import { TextInputEvents } from './screens/TextInputEvents';

export type Experiment = (typeof experiments)[number];

export const experiments = [
{
key: 'textInputEvents',
title: 'TextInput Events',
component: TextInputEvents,
},
{
key: 'textInputEventPropagation',
title: 'TextInput Event Propagation',
component: TextInputEventPropagation,
},
];
54 changes: 54 additions & 0 deletions experiments-app/src/screens/TextInputEventPropagation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import { StyleSheet, SafeAreaView, TextInput, Pressable } from 'react-native';
import { buildEventLogger } from '../utils/helpers';

const handlePressIn = buildEventLogger('TextInput.pressIn');
const handlePressOut = buildEventLogger('TextInput.pressOut');
const handleFocus = buildEventLogger('TextInput.focus');
const handleBlur = buildEventLogger('TextInput.blur');
const handleChange = buildEventLogger('TextInput.change');
const handleSubmitEditing = buildEventLogger('TextInput.submitEditing');

const handlePressablePress = buildEventLogger('Pressable.press');

export function TextInputEventPropagation() {
const [value, setValue] = React.useState('');

const handleChangeText = (value: string) => {
setValue(value);
console.log(`Event: changeText`, value);
};

return (
<SafeAreaView style={styles.container}>
<Pressable onPress={handlePressablePress}>
<TextInput
style={styles.textInput}
value={value}
editable={true}
onChangeText={handleChangeText}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleChange}
onSubmitEditing={handleSubmitEditing}
/>
</Pressable>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
textInput: {
backgroundColor: 'white',
margin: 20,
padding: 8,
fontSize: 18,
borderWidth: 1,
borderColor: 'grey',
},
});
50 changes: 50 additions & 0 deletions experiments-app/src/screens/TextInputEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { StyleSheet, SafeAreaView, TextInput } from 'react-native';
import { buildEventLogger } from '../utils/helpers';

const handlePressIn = buildEventLogger('pressIn');
const handlePressOut = buildEventLogger('pressOut');
const handleFocus = buildEventLogger('focus');
const handleBlur = buildEventLogger('blur');
const handleChange = buildEventLogger('change');
const handleSubmitEditing = buildEventLogger('submitEditing');

export function TextInputEvents() {
const [value, setValue] = React.useState('');

const handleChangeText = (value: string) => {
setValue(value);
console.log(`Event: changeText`, value);
};

return (
<SafeAreaView style={styles.container}>
<TextInput
style={styles.textInput}
value={value}
editable={true}
onChangeText={handleChangeText}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleChange}
onSubmitEditing={handleSubmitEditing}
/>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
textInput: {
backgroundColor: 'white',
margin: 20,
padding: 8,
fontSize: 18,
borderWidth: 1,
borderColor: 'grey',
},
});
8 changes: 8 additions & 0 deletions experiments-app/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NativeSyntheticEvent } from 'react-native/types';

export function buildEventLogger(name: string) {
return (event: NativeSyntheticEvent<unknown>) => {
const eventData = event?.nativeEvent ?? event;
console.log(`Event: ${name}`, eventData);
};
}
6 changes: 6 additions & 0 deletions experiments-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}
6,913 changes: 6,913 additions & 0 deletions experiments-app/yarn.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
@@ -5,3 +5,6 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
beforeEach(() => {
resetToDefaults();
});

// Disable colors in our local tests in order to generate clear snapshots
process.env.COLORS = 'false';
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@testing-library/react-native",
"version": "12.0.1",
"version": "12.1.0",
"description": "Simple and complete React Native testing utilities that encourage good testing practices.",
"main": "build/index.js",
"types": "build/index.d.ts",
@@ -60,7 +60,10 @@
"typescript": "^5.0.2"
},
"dependencies": {
"pretty-format": "^29.0.0"
"@react-navigation/native": "^6.1.6",
"pretty-format": "^29.0.0",
"react-native-safe-area-context": "^4.5.1",
"react-native-screens": "^3.20.0"
},
"peerDependencies": {
"jest": ">=28.0.0",
18 changes: 9 additions & 9 deletions src/__tests__/__snapshots__/render-debug.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ exports[`debug 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
@@ -39,7 +39,7 @@ exports[`debug 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
@@ -109,7 +109,7 @@ exports[`debug changing component: bananaFresh button message should now be "fre
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
@@ -118,7 +118,7 @@ exports[`debug changing component: bananaFresh button message should now be "fre
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
@@ -169,7 +169,7 @@ exports[`debug should use debugOptions from config when no option is specified 1
exports[`debug should use given options over config debugOptions 1`] = `
"<View
style={
Object {
{
"backgroundColor": "red",
}
}
@@ -315,7 +315,7 @@ exports[`debug: another custom message 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
@@ -324,7 +324,7 @@ exports[`debug: another custom message 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
@@ -498,7 +498,7 @@ exports[`debug: with message 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
@@ -507,7 +507,7 @@ exports[`debug: with message 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
20 changes: 0 additions & 20 deletions src/__tests__/fireEvent.test.tsx
Original file line number Diff line number Diff line change
@@ -217,25 +217,6 @@ test('should not fire on disabled Pressable', () => {
});

test('should not fire on non-editable TextInput', () => {
const placeholder = 'Test placeholder';
const onChangeTextMock = jest.fn();
const NEW_TEXT = 'New text';

const { getByPlaceholderText } = render(
<View>
<TextInput
editable={false}
placeholder={placeholder}
onChangeText={onChangeTextMock}
/>
</View>
);

fireEvent.changeText(getByPlaceholderText(placeholder), NEW_TEXT);
expect(onChangeTextMock).not.toHaveBeenCalled();
});

test('should not fire on non-editable host TextInput', () => {
const testID = 'my-text-input';
const onChangeTextMock = jest.fn();
const NEW_TEXT = 'New text';
@@ -245,7 +226,6 @@ test('should not fire on non-editable host TextInput', () => {
editable={false}
testID={testID}
onChangeText={onChangeTextMock}
placeholder="placeholder"
/>
);

31 changes: 7 additions & 24 deletions src/__tests__/host-component-names.test.tsx
Original file line number Diff line number Diff line change
@@ -6,14 +6,13 @@ import {
getHostComponentNames,
configureHostComponentNamesIfNeeded,
} from '../helpers/host-component-names';
import * as within from '../within';
import { act, render } from '..';

const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock;
const mockGetQueriesForElements = jest.spyOn(
within,
'getQueriesForElement'
) as jest.Mock;

beforeEach(() => {
mockCreate.mockReset();
});

describe('getHostComponentNames', () => {
test('returns host component names from internal config', () => {
@@ -79,8 +78,10 @@ describe('configureHostComponentNamesIfNeeded', () => {
});

test('throw an error when autodetection fails', () => {
const renderer = TestRenderer.create(<View />);

mockCreate.mockReturnValue({
root: { type: View, children: [], props: {} },
root: renderer.root,
});

expect(() => configureHostComponentNamesIfNeeded())
@@ -93,22 +94,4 @@ describe('configureHostComponentNamesIfNeeded', () => {
Please check if you are using compatible versions of React Native and React Native Testing Library."
`);
});

test('throw an error when autodetection fails due to getByTestId returning non-host component', () => {
mockGetQueriesForElements.mockReturnValue({
getByTestId: () => {
return { type: View };
},
});

expect(() => configureHostComponentNamesIfNeeded())
.toThrowErrorMatchingInlineSnapshot(`
"Trying to detect host component names triggered the following error:
getByTestId returned non-host component
There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library."
`);
});
});
21 changes: 7 additions & 14 deletions src/__tests__/react-native-api.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import { View, Text, TextInput } from 'react-native';
import { render } from '..';
import { getHostSelf } from '../helpers/component-tree';

/**
* Tests in this file are intended to give us an proactive warning that React Native behavior has
@@ -10,8 +9,6 @@ import { getHostSelf } from '../helpers/component-tree';

test('React Native API assumption: <View> renders single host element', () => {
const view = render(<View testID="test" />);
const hostView = view.getByTestId('test');
expect(getHostSelf(hostView)).toBe(hostView);

expect(view.toJSON()).toMatchInlineSnapshot(`
<View
@@ -22,9 +19,7 @@ test('React Native API assumption: <View> renders single host element', () => {

test('React Native API assumption: <Text> renders single host element', () => {
const view = render(<Text testID="test">Hello</Text>);
const compositeView = view.getByText('Hello');
const hostView = view.getByTestId('test');
expect(getHostSelf(compositeView)).toBe(hostView);
expect(view.getByText('Hello')).toBe(view.getByTestId('test'));

expect(view.toJSON()).toMatchInlineSnapshot(`
<Text
@@ -45,11 +40,9 @@ test('React Native API assumption: nested <Text> renders single host element', (
</Text>
</Text>
);
expect(getHostSelf(view.getByText(/Hello/))).toBe(view.getByTestId('test'));
expect(getHostSelf(view.getByText('Before'))).toBe(
view.getByTestId('before')
);
expect(getHostSelf(view.getByText('Deeply nested'))).toBe(
expect(view.getByText(/Hello/)).toBe(view.getByTestId('test'));
expect(view.getByText('Before')).toBe(view.getByTestId('before'));
expect(view.getByText('Deeply nested')).toBe(
view.getByTestId('deeplyNested')
);

@@ -85,9 +78,9 @@ test('React Native API assumption: <TextInput> renders single host element', ()
placeholder="Placeholder"
/>
);
const compositeView = view.getByPlaceholderText('Placeholder');
const hostView = view.getByTestId('test');
expect(getHostSelf(compositeView)).toBe(hostView);
expect(view.getByPlaceholderText('Placeholder')).toBe(
view.getByTestId('test')
);

expect(view.toJSON()).toMatchInlineSnapshot(`
<TextInput
9 changes: 9 additions & 0 deletions src/__tests__/waitFor.test.tsx
Original file line number Diff line number Diff line change
@@ -316,3 +316,12 @@ test.each([
expect(onPress).toHaveBeenCalledWith('red');
}
);

test('waitFor throws if expectation is not a function', async () => {
await expect(
// @ts-expect-error intentionally passing non-function
waitFor('not a function')
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Received \`expectation\` arg must be a function"`
);
});
8 changes: 0 additions & 8 deletions src/__tests__/within.test.tsx
Original file line number Diff line number Diff line change
@@ -94,11 +94,3 @@ test('within() exposes a11y queries', async () => {
test('getQueriesForElement is alias to within', () => {
expect(getQueriesForElement).toBe(within);
});

test('within allows searching for text within a composite component', () => {
const view = render(<Text testID="subject">Hello</Text>);
// view.getByTestId('subject') returns a host component, contrary to text queries returning a composite component
// we want to be sure that this doesn't interfere with the way text is searched
const hostTextQueries = within(view.getByTestId('subject'));
expect(hostTextQueries.getByText('Hello')).toBeTruthy();
});
7 changes: 3 additions & 4 deletions src/fireEvent.ts
Original file line number Diff line number Diff line change
@@ -12,11 +12,10 @@ const isTextInput = (element?: ReactTestInstance) => {
return false;
}

// We have to test if the element type is either the TextInput component
// (which would if it is a composite component) or the string
// TextInput (which would be true if it is a host component)
// We have to test if the element type is either the `TextInput` component
// (for composite component) or the string "TextInput" (for host component)
// All queries return host components but since fireEvent bubbles up
// it would trigger the parent prop without the composite component check
// it would trigger the parent prop without the composite component check.
return (
filterNodeByType(element, TextInput) ||
filterNodeByType(element, getHostComponentNames().textInput)
10 changes: 3 additions & 7 deletions src/helpers/__tests__/component-tree.test.tsx
Original file line number Diff line number Diff line change
@@ -142,17 +142,13 @@ describe('getHostSelf()', () => {
</View>
);

const compositeText = view.getByText('Text');
const compositeText = view.UNSAFE_getByType(Text);
const hostText = view.getByTestId('text');
expect(getHostSelf(compositeText)).toEqual(hostText);

const compositeTextInputByValue = view.getByDisplayValue('TextInputValue');
const compositeTextInputByPlaceholder = view.getByPlaceholderText(
'TextInputPlaceholder'
);
const compositeTextInput = view.UNSAFE_getByType(TextInput);
const hostTextInput = view.getByTestId('textInput');
expect(getHostSelf(compositeTextInputByValue)).toEqual(hostTextInput);
expect(getHostSelf(compositeTextInputByPlaceholder)).toEqual(hostTextInput);
expect(getHostSelf(compositeTextInput)).toEqual(hostTextInput);
});

it('throws on non-single host children elements for custom composite components', () => {
114 changes: 114 additions & 0 deletions src/helpers/__tests__/format-default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { ReactTestRendererJSON } from 'react-test-renderer';
import { defaultMapProps } from '../format-default';

const node: ReactTestRendererJSON = {
type: 'View',
props: {},
children: null,
};

describe('mapPropsForQueryError', () => {
test('preserves props that are helpful for debugging', () => {
const props = {
accessibilityElementsHidden: true,
accessibilityViewIsModal: true,
importantForAccessibility: 'yes',
testID: 'TEST_ID',
nativeID: 'NATIVE_ID',
accessibilityLabel: 'LABEL',
accessibilityLabelledBy: 'LABELLED_BY',
accessibilityRole: 'ROLE',
accessibilityHint: 'HINT',
placeholder: 'PLACEHOLDER',
value: 'VALUE',
defaultValue: 'DEFAULT_VALUE',
};

const result = defaultMapProps(props, node);

expect(result).toStrictEqual(props);
});

test('does not preserve less helpful props', () => {
const result = defaultMapProps(
{
style: [{ flex: 1 }, { display: 'flex' }],
onPress: () => null,
key: 'foo',
},
node
);

expect(result).toStrictEqual({});
});

test('preserves "display: none" style but no other style', () => {
const result = defaultMapProps(
{ style: [{ flex: 1 }, { display: 'none', flex: 2 }] },
node
);

expect(result).toStrictEqual({
style: { display: 'none' },
});
});

test('removes undefined keys from accessibilityState', () => {
const result = defaultMapProps(
{ accessibilityState: { checked: undefined, selected: false } },
node
);

expect(result).toStrictEqual({
accessibilityState: { selected: false },
});
});

test('removes accessibilityState if all keys are undefined', () => {
const result = defaultMapProps(
{ accessibilityState: { checked: undefined, selected: undefined } },
node
);

expect(result).toStrictEqual({});
});

test('does not fail if accessibilityState is a string, passes through', () => {
const result = defaultMapProps({ accessibilityState: 'foo' }, node);
expect(result).toStrictEqual({ accessibilityState: 'foo' });
});

test('does not fail if accessibilityState is an array, passes through', () => {
const result = defaultMapProps({ accessibilityState: [1] }, node);
expect(result).toStrictEqual({ accessibilityState: [1] });
});

test('does not fail if accessibilityState is null, passes through', () => {
const result = defaultMapProps({ accessibilityState: null }, node);
expect(result).toStrictEqual({ accessibilityState: null });
});

test('does not fail if accessibilityState is nested object, passes through', () => {
const accessibilityState = { 1: { 2: 3 }, 2: undefined };
const result = defaultMapProps({ accessibilityState }, node);
expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } });
});

test('removes undefined keys from accessibilityValue', () => {
const result = defaultMapProps(
{ accessibilityValue: { min: 1, max: undefined } },
node
);

expect(result).toStrictEqual({ accessibilityValue: { min: 1 } });
});

test('removes accessibilityValue if all keys are undefined', () => {
const result = defaultMapProps(
{ accessibilityValue: { min: undefined } },
node
);

expect(result).toStrictEqual({});
});
});
72 changes: 72 additions & 0 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { StyleSheet, ViewStyle } from 'react-native';
import { MapPropsFunction } from './format';

const propsToDisplay = [
'testID',
'nativeID',
'accessibilityElementsHidden',
'accessibilityViewIsModal',
'importantForAccessibility',
'accessibilityRole',
'accessibilityLabel',
'accessibilityLabelledBy',
'accessibilityHint',
'placeholder',
'value',
'defaultValue',
'title',
];

/**
* Preserve props that are helpful in diagnosing test failures, while stripping rest
*/
export const defaultMapProps: MapPropsFunction = (props) => {
const result: Record<string, unknown> = {};

const styles = StyleSheet.flatten(props.style as ViewStyle);
if (styles?.display === 'none') {
result.style = { display: 'none' };
}

const accessibilityState = removeUndefinedKeys(props.accessibilityState);
if (accessibilityState !== undefined) {
result.accessibilityState = accessibilityState;
}

const accessibilityValue = removeUndefinedKeys(props.accessibilityValue);
if (accessibilityValue !== undefined) {
result.accessibilityValue = accessibilityValue;
}

propsToDisplay.forEach((propName) => {
if (propName in props) {
result[propName] = props[propName];
}
});

return result;
};

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function removeUndefinedKeys(prop: unknown) {
if (!isObject(prop)) {
return prop;
}

const result: Record<string, unknown> = {};
Object.entries(prop).forEach(([key, value]) => {
if (value !== undefined) {
result[key] = value;
}
});

// If object does not have any props we will ignore it.
if (Object.keys(result).length === 0) {
return undefined;
}

return result;
}
9 changes: 7 additions & 2 deletions src/helpers/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactTestRendererJSON } from 'react-test-renderer';
import prettyFormat, { NewPlugin, plugins } from 'pretty-format';

type MapPropsFunction = (
export type MapPropsFunction = (
props: Record<string, unknown>,
node: ReactTestRendererJSON
) => Record<string, unknown>;
@@ -16,7 +16,8 @@ const format = (
) =>
prettyFormat(input, {
plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement],
highlight: true,
highlight: shouldHighlight(),
printBasicPrototype: false,
});

const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => {
@@ -39,4 +40,8 @@ const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => {
};
};

function shouldHighlight() {
return process?.env?.COLORS !== 'false';
}

export default format;
29 changes: 15 additions & 14 deletions src/helpers/host-component-names.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { ReactTestInstance } from 'react-test-renderer';
import { Text, TextInput, View } from 'react-native';
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { renderWithAct } from '../render-act';
import { getQueriesForElement } from '../within';

const userConfigErrorMessage = `There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library.`;
@@ -35,21 +35,10 @@ function detectHostComponentNames(): HostComponentNames {
<TextInput testID="textInput" />
</View>
);
const { getByTestId } = getQueriesForElement(renderer.root);
const textHostName = getByTestId('text').type;
const textInputHostName = getByTestId('textInput').type;

// This code path should not happen as getByTestId always returns host elements.
if (
typeof textHostName !== 'string' ||
typeof textInputHostName !== 'string'
) {
throw new Error('getByTestId returned non-host component');
}

return {
text: textHostName,
textInput: textInputHostName,
text: getByTestId(renderer.root, 'text').type as string,
textInput: getByTestId(renderer.root, 'textInput').type as string,
};
} catch (error) {
const errorMessage =
@@ -62,3 +51,15 @@ function detectHostComponentNames(): HostComponentNames {
);
}
}

function getByTestId(instance: ReactTestInstance, testID: string) {
const nodes = instance.findAll(
(node) => typeof node.type === 'string' && node.props.testID === testID
);

if (nodes.length === 0) {
throw new Error(`Unable to find an element with testID: ${testID}`);
}

return nodes[0];
}
91 changes: 88 additions & 3 deletions src/queries/__tests__/a11yState.test.tsx
Original file line number Diff line number Diff line change
@@ -254,9 +254,26 @@ test('byA11yState queries support hidden option', () => {
).toBeFalsy();
expect(() =>
getByA11yState({ expanded: false }, { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with expanded state: false"`
);
).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with expanded state: false
<View
accessibilityState={
{
"expanded": false,
}
}
style={
{
"display": "none",
}
}
>
<Text>
Hidden from accessibility
</Text>
</View>"
`);
});

test('*ByA11yState deprecation warnings', () => {
@@ -352,3 +369,71 @@ test('*ByAccessibilityState deprecation warnings', () => {
Use findAllByRole(role, { disabled, selected, checked, busy, expanded }) query or expect(...).toHaveAccessibilityState(...) matcher from "@testing-library/jest-native" package instead."
`);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(
<Text accessibilityState={{ checked: false }} onPress={() => null}>
Some text
</Text>
);

expect(() => view.getByA11yState({ checked: true }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with checked state: true
<Text
accessibilityState={
{
"checked": false,
}
}
>
Some text
</Text>"
`);

expect(() => view.getAllByA11yState({ checked: true }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with checked state: true
<Text
accessibilityState={
{
"checked": false,
}
}
>
Some text
</Text>"
`);

await expect(view.findByA11yState({ checked: true })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with checked state: true
<Text
accessibilityState={
{
"checked": false,
}
}
>
Some text
</Text>"
`);

await expect(view.findAllByA11yState({ checked: true })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with checked state: true
<Text
accessibilityState={
{
"checked": false,
}
}
>
Some text
</Text>"
`);
});
126 changes: 101 additions & 25 deletions src/queries/__tests__/a11yValue.test.tsx
Original file line number Diff line number Diff line change
@@ -107,35 +107,53 @@ test('byA11yValue queries support hidden option', () => {
expect(
queryByA11yValue({ max: 10 }, { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByA11yValue({ max: 10 }, { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with max value: 10"`
);
expect(() => getByA11yValue({ max: 10 }, { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with max value: 10
<Text
accessibilityValue={
{
"max": 10,
}
}
style={
{
"display": "none",
}
}
>
Hidden from accessibility
</Text>"
`);
});

test('byA11yValue error messages', () => {
const { getByA11yValue } = render(<View />);
expect(() =>
getByA11yValue({ min: 10, max: 10 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 10, max value: 10"`
);
expect(() =>
getByA11yValue({ max: 20, now: 5 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with max value: 20, now value: 5"`
);
expect(() =>
getByA11yValue({ min: 1, max: 2, now: 3 })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 1, max value: 2, now value: 3"`
);
expect(() =>
getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i"`
);
expect(() => getByA11yValue({ min: 10, max: 10 }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 10, max value: 10
<View />"
`);
expect(() => getByA11yValue({ max: 20, now: 5 }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with max value: 20, now value: 5
<View />"
`);
expect(() => getByA11yValue({ min: 1, max: 2, now: 3 }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1, max value: 2, now value: 3
<View />"
`);
expect(() => getByA11yValue({ min: 1, max: 2, now: 3, text: /foo/i }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1, max value: 2, now value: 3, text value: /foo/i
<View />"
`);
});

test('*ByA11yValue deprecation warnings', () => {
@@ -231,3 +249,61 @@ test('*ByAccessibilityValue deprecation warnings', () => {
Use expect(...).toHaveAccessibilityValue(...) matcher from "@testing-library/jest-native" package or findAllByRole(role, { value: ... }) query instead."
`);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(
<View accessibilityValue={{ min: 2 }} key="NOT_RELEVANT" />
);

expect(() => view.getByA11yValue({ min: 1 }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1
<View
accessibilityValue={
{
"min": 2,
}
}
/>"
`);

expect(() => view.getAllByA11yValue({ min: 1 }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1
<View
accessibilityValue={
{
"min": 2,
}
}
/>"
`);

await expect(view.findByA11yValue({ min: 1 })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1
<View
accessibilityValue={
{
"min": 2,
}
}
/>"
`);

await expect(view.findAllByA11yValue({ min: 1 })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with min value: 1
<View
accessibilityValue={
{
"min": 2,
}
}
/>"
`);
});
116 changes: 102 additions & 14 deletions src/queries/__tests__/displayValue.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, TextInput } from 'react-native';
import { TextInput, View } from 'react-native';

import { render } from '../..';

@@ -50,16 +50,57 @@ test('getByDisplayValue, queryByDisplayValue', () => {

test('getByDisplayValue, queryByDisplayValue get element by default value only when value is undefined', () => {
const { getByDisplayValue, queryByDisplayValue } = render(<Banana />);
expect(() =>
getByDisplayValue(DEFAULT_INPUT_CHEF)
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: What did you inspect?"`
);
expect(() => getByDisplayValue(DEFAULT_INPUT_CHEF))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: What did you inspect?
<View>
<TextInput
placeholder="Add custom freshness"
testID="bananaCustomFreshness"
value="Custom Freshie"
/>
<TextInput
defaultValue="What did you inspect?"
placeholder="Who inspected freshness?"
testID="bananaChef"
value="I inspected freshie"
/>
<TextInput
defaultValue="What banana?"
/>
<TextInput
defaultValue="hello"
value=""
/>
</View>"
`);
expect(queryByDisplayValue(DEFAULT_INPUT_CHEF)).toBeNull();

expect(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: hello"`
);
expect(() => getByDisplayValue('hello')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: hello
<View>
<TextInput
placeholder="Add custom freshness"
testID="bananaCustomFreshness"
value="Custom Freshie"
/>
<TextInput
defaultValue="What did you inspect?"
placeholder="Who inspected freshness?"
testID="bananaChef"
value="I inspected freshie"
/>
<TextInput
defaultValue="What banana?"
/>
<TextInput
defaultValue="hello"
value=""
/>
</View>"
`);
expect(queryByDisplayValue('hello')).toBeNull();

expect(getByDisplayValue(DEFAULT_INPUT_CUSTOMER)).toBeTruthy();
@@ -119,15 +160,62 @@ test('byDisplayValue queries support hidden option', () => {
expect(
queryByDisplayValue('hidden', { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByDisplayValue('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with displayValue: hidden"`
);
expect(() => getByDisplayValue('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: hidden
<TextInput
style={
{
"display": "none",
}
}
value="hidden"
/>"
`);
});

test('byDisplayValue should return host component', () => {
const { getByDisplayValue } = render(<TextInput value="value" />);

expect(getByDisplayValue('value').type).toBe('TextInput');
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<TextInput value="1" key="3" />);

expect(() => view.getByDisplayValue('2')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: 2
<TextInput
value="1"
/>"
`);

expect(() => view.getAllByDisplayValue('2'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: 2
<TextInput
value="1"
/>"
`);

await expect(view.findByDisplayValue('2')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: 2
<TextInput
value="1"
/>"
`);

await expect(view.findAllByDisplayValue('2')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with displayValue: 2
<TextInput
value="1"
/>"
`);
});
59 changes: 54 additions & 5 deletions src/queries/__tests__/hintText.test.tsx
Original file line number Diff line number Diff line change
@@ -120,9 +120,58 @@ test('byHintText queries support hidden option', () => {
expect(
queryByHintText('hidden', { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByHintText('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with accessibilityHint: hidden"`
);
expect(() => getByHintText('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityHint: hidden
<Text
accessibilityHint="hidden"
style={
{
"display": "none",
}
}
>
Hidden from accessiblity
</Text>"
`);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<TouchableOpacity accessibilityHint="HINT" key="3" />);

expect(() => view.getByHintText('FOO')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityHint: FOO
<View
accessibilityHint="HINT"
/>"
`);

expect(() => view.getAllByHintText('FOO'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityHint: FOO
<View
accessibilityHint="HINT"
/>"
`);

await expect(view.findByHintText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityHint: FOO
<View
accessibilityHint="HINT"
/>"
`);

await expect(view.findAllByHintText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityHint: FOO
<View
accessibilityHint="HINT"
/>"
`);
});
59 changes: 54 additions & 5 deletions src/queries/__tests__/labelText.test.tsx
Original file line number Diff line number Diff line change
@@ -159,11 +159,21 @@ test('byLabelText queries support hidden option', () => {
expect(
queryByLabelText('hidden', { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByLabelText('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with accessibilityLabel: hidden"`
);
expect(() => getByLabelText('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: hidden
<Text
accessibilityLabel="hidden"
style={
{
"display": "none",
}
}
>
Hidden from accessibility
</Text>"
`);
});

test('getByLabelText supports accessibilityLabelledBy', async () => {
@@ -191,3 +201,42 @@ test('getByLabelText supports nested accessibilityLabelledBy', async () => {
expect(getByLabelText('Label for input')).toBe(getByTestId('textInput'));
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<TouchableOpacity accessibilityLabel="LABEL" key="3" />);

expect(() => view.getByLabelText('FOO')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
<View
accessibilityLabel="LABEL"
/>"
`);

expect(() => view.getAllByLabelText('FOO'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
<View
accessibilityLabel="LABEL"
/>"
`);

await expect(view.findByLabelText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
<View
accessibilityLabel="LABEL"
/>"
`);

await expect(view.findAllByLabelText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with accessibilityLabel: FOO
<View
accessibilityLabel="LABEL"
/>"
`);
});
235 changes: 235 additions & 0 deletions src/queries/__tests__/makeQueries.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import * as React from 'react';
import { Text, TextInput, View } from 'react-native';
import { render, screen } from '../..';

describe('printing element tree', () => {
test('includes element tree on error with less-helpful props stripped', async () => {
const { getByText } = render(<Text onPress={() => null}>Some text</Text>);

expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<Text>
Some text
</Text>"
`);
});

test('prints helpful props but not others', async () => {
const { getByText } = render(
<View
accessibilityElementsHidden
accessibilityViewIsModal
importantForAccessibility="yes"
testID="TEST_ID"
nativeID="NATIVE_ID"
accessibilityLabel="LABEL"
accessibilityLabelledBy="LABELLED_BY"
accessibilityRole="summary"
accessibilityHint="HINT"
key="this is filtered"
>
<TextInput
placeholder="PLACEHOLDER"
value="VALUE"
defaultValue="DEFAULT_VALUE"
/>
<Text>Some Text</Text>
</View>
);

expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityElementsHidden={true}
accessibilityHint="HINT"
accessibilityLabel="LABEL"
accessibilityLabelledBy="LABELLED_BY"
accessibilityRole="summary"
accessibilityViewIsModal={true}
importantForAccessibility="yes"
nativeID="NATIVE_ID"
testID="TEST_ID"
>
<TextInput
defaultValue="DEFAULT_VALUE"
placeholder="PLACEHOLDER"
value="VALUE"
/>
<Text>
Some Text
</Text>
</View>"
`);
});

test('prints tree and filters props with getBy, getAllBy, findBy, findAllBy', async () => {
const view = render(
<View accessibilityViewIsModal key="this is filtered" />
);

expect(() => view.getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

expect(() => view.getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

await expect(view.findByText(/foo/)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

await expect(view.findAllByText(/foo/)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);
});

test('only appends element tree on last failure with findBy', async () => {
const { findByText } = render(
<View accessibilityViewIsModal key="this is filtered" />
);

jest.spyOn(screen, 'toJSON');

await expect(findByText(/foo/)).rejects.toThrow();

expect(screen.toJSON).toHaveBeenCalledTimes(1);
});

test('onTimeout with findBy receives error without element tree', async () => {
const { findByText } = render(<View />);

const onTimeout = jest.fn((_: Error) => new Error('Replacement error'));

await expect(() =>
findByText(/foo/, undefined, { onTimeout })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Replacement error"`);

expect(onTimeout).toHaveBeenCalledTimes(1);
expect(onTimeout.mock.calls[0][0].message).not.toMatch(/View/);
expect(onTimeout.mock.calls[0][0].message).toMatchInlineSnapshot(
`"Unable to find an element with text: /foo/"`
);
});

test('onTimeout with findAllBy receives error without element tree', async () => {
const { findAllByText } = render(<View />);

const onTimeout = jest.fn((_: Error) => new Error('Replacement error'));

await expect(() =>
findAllByText(/foo/, undefined, { onTimeout })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Replacement error"`);

expect(onTimeout).toHaveBeenCalledTimes(1);
expect(onTimeout.mock.calls[0][0].message).not.toMatch(/View/);
expect(onTimeout.mock.calls[0][0].message).toMatchInlineSnapshot(
`"Unable to find an element with text: /foo/"`
);
});

test('does not strip display: none from "style" prop, but does strip other styles', () => {
const { getByText } = render(
<View style={{ display: 'flex', position: 'absolute' }}>
<Text
style={[
{ display: 'flex', position: 'relative' },
{ display: 'none', flex: 1 },
]}
>
Some text
</Text>
</View>
);

expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View>
<Text
style={
{
"display": "none",
}
}
>
Some text
</Text>
</View>"
`);
});

test('strips undefined values from accessibilityState', () => {
const { getByText } = render(
<View accessibilityState={{ checked: true, busy: false }}>
<View accessibilityState={{ checked: undefined }} />
</View>
);

expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityState={
{
"busy": false,
"checked": true,
}
}
>
<View />
</View>"
`);
});

test('strips undefined values from accessibilityValue', () => {
const { getByText } = render(
<View accessibilityValue={{ min: 1 }}>
<View accessibilityState={{}} />
</View>
);

expect(() => getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityValue={
{
"min": 1,
}
}
>
<View />
</View>"
`);
});

test('does not render element tree when toJSON() returns null', () => {
const view = render(<View />);

jest.spyOn(screen, 'toJSON').mockImplementation(() => null);
expect(() => view.getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with text: /foo/"`
);
});
});
58 changes: 53 additions & 5 deletions src/queries/__tests__/placeholderText.test.tsx
Original file line number Diff line number Diff line change
@@ -72,11 +72,19 @@ test('byPlaceholderText queries support hidden option', () => {
expect(
queryByPlaceholderText('hidden', { includeHiddenElements: false })
).toBeFalsy();
expect(() =>
getByPlaceholderText('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with placeholder: hidden"`
);
expect(() => getByPlaceholderText('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with placeholder: hidden
<TextInput
placeholder="hidden"
style={
{
"display": "none",
}
}
/>"
`);
});

test('byPlaceHolderText should return host component', () => {
@@ -86,3 +94,43 @@ test('byPlaceHolderText should return host component', () => {

expect(getByPlaceholderText('placeholder').type).toBe('TextInput');
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<TextInput placeholder="PLACEHOLDER" key="3" />);

expect(() => view.getByPlaceholderText('FOO'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with placeholder: FOO
<TextInput
placeholder="PLACEHOLDER"
/>"
`);

expect(() => view.getAllByPlaceholderText('FOO'))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with placeholder: FOO
<TextInput
placeholder="PLACEHOLDER"
/>"
`);

await expect(view.findByPlaceholderText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with placeholder: FOO
<TextInput
placeholder="PLACEHOLDER"
/>"
`);

await expect(view.findAllByPlaceholderText('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with placeholder: FOO
<TextInput
placeholder="PLACEHOLDER"
/>"
`);
});
109 changes: 91 additions & 18 deletions src/queries/__tests__/role-value.test.tsx
Original file line number Diff line number Diff line change
@@ -79,25 +79,98 @@ describe('accessibility value', () => {
getByRole('adjustable', { disabled: true, value: { min: 10 } })
).toBeTruthy();

expect(() =>
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
);
expect(() =>
getByRole('adjustable', { name: 'World', value: { min: 10 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "World", min value: 10"`
);
expect(() =>
getByRole('adjustable', { name: 'Hello', value: { min: 5 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", name: "Hello", min value: 5"`
);
expect(() => getByRole('adjustable', { name: 'Hello', value: { min: 5 } }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", name: "Hello", min value: 5
<Text
accessibilityRole="adjustable"
accessibilityState={
{
"disabled": true,
}
}
accessibilityValue={
{
"max": 20,
"min": 10,
"now": 12,
"text": "Hello",
}
}
>
Hello
</Text>"
`);
expect(() => getByRole('adjustable', { name: 'World', value: { min: 10 } }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", name: "World", min value: 10
<Text
accessibilityRole="adjustable"
accessibilityState={
{
"disabled": true,
}
}
accessibilityValue={
{
"max": 20,
"min": 10,
"now": 12,
"text": "Hello",
}
}
>
Hello
</Text>"
`);
expect(() => getByRole('adjustable', { name: 'Hello', value: { min: 5 } }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", name: "Hello", min value: 5
<Text
accessibilityRole="adjustable"
accessibilityState={
{
"disabled": true,
}
}
accessibilityValue={
{
"max": 20,
"min": 10,
"now": 12,
"text": "Hello",
}
}
>
Hello
</Text>"
`);
expect(() =>
getByRole('adjustable', { selected: true, value: { min: 10 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", selected state: true, min value: 10"`
);
).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", selected state: true, min value: 10
<Text
accessibilityRole="adjustable"
accessibilityState={
{
"disabled": true,
}
}
accessibilityValue={
{
"max": 20,
"min": 10,
"now": 12,
"text": "Hello",
}
}
>
Hello
</Text>"
`);
});
});
128 changes: 94 additions & 34 deletions src/queries/__tests__/role.test.tsx
Original file line number Diff line number Diff line change
@@ -652,67 +652,77 @@ describe('error messages', () => {
test('gives a descriptive error message when querying with a role', () => {
const { getByRole } = render(<View />);

expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button""`
);
expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button"
<View />"
`);
});

test('gives a descriptive error message when querying with a role and a name', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('button', { name: 'Save' })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button", name: "Save""`
);
expect(() => getByRole('button', { name: 'Save' }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button", name: "Save"
<View />"
`);
});

test('gives a descriptive error message when querying with a role, a name and accessibility state', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('button', { name: 'Save', disabled: true })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button", name: "Save", disabled state: true"`
);
expect(() => getByRole('button', { name: 'Save', disabled: true }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button", name: "Save", disabled state: true
<View />"
`);
});

test('gives a descriptive error message when querying with a role, a name and several accessibility state', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('button', { name: 'Save', disabled: true, selected: true })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true"`
);
).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button", name: "Save", disabled state: true, selected state: true
<View />"
`);
});

test('gives a descriptive error message when querying with a role and an accessibility state', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('button', { disabled: true })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button", disabled state: true"`
);
expect(() => getByRole('button', { disabled: true }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button", disabled state: true
<View />"
`);
});

test('gives a descriptive error message when querying with a role and an accessibility value', () => {
const { getByRole } = render(<View />);

expect(() =>
getByRole('adjustable', { value: { min: 1 } })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", min value: 1"`
);
expect(() => getByRole('adjustable', { value: { min: 1 } }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", min value: 1
<View />"
`);

expect(() =>
getByRole('adjustable', {
value: { min: 1, max: 2, now: 1, text: /hello/ },
})
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/"`
);
).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "adjustable", min value: 1, max value: 2, now value: 1, text value: /hello/
<View />"
`);
});
});

@@ -727,11 +737,23 @@ test('byRole queries support hidden option', () => {

expect(queryByRole('button')).toBeFalsy();
expect(queryByRole('button', { includeHiddenElements: false })).toBeFalsy();
expect(() =>
getByRole('button', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with role: "button""`
);
expect(() => getByRole('button', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "button"
<View
accessibilityRole="button"
style={
{
"display": "none",
}
}
>
<Text>
Hidden from accessibility
</Text>
</View>"
`);
});

describe('matches only accessible elements', () => {
@@ -762,3 +784,41 @@ describe('matches only accessible elements', () => {
expect(queryByRole('menu', { name: 'Action' })).toBeFalsy();
});
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<View accessibilityRole="button" key="3" />);

expect(() => view.getByRole('link')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "link"
<View
accessibilityRole="button"
/>"
`);

expect(() => view.getAllByRole('link')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "link"
<View
accessibilityRole="button"
/>"
`);

await expect(view.findByRole('link')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "link"
<View
accessibilityRole="button"
/>"
`);

await expect(view.findAllByRole('link')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with role: "link"
<View
accessibilityRole="button"
/>"
`);
});
58 changes: 53 additions & 5 deletions src/queries/__tests__/testId.test.tsx
Original file line number Diff line number Diff line change
@@ -144,9 +144,57 @@ test('byTestId queries support hidden option', () => {

expect(queryByTestId('hidden')).toBeFalsy();
expect(queryByTestId('hidden', { includeHiddenElements: false })).toBeFalsy();
expect(() =>
getByTestId('hidden', { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with testID: hidden"`
);
expect(() => getByTestId('hidden', { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with testID: hidden
<Text
style={
{
"display": "none",
}
}
testID="hidden"
>
Hidden from accessibility
</Text>"
`);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<View testID="TEST_ID" key="3" />);

expect(() => view.getByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with testID: FOO
<View
testID="TEST_ID"
/>"
`);

expect(() => view.getAllByTestId('FOO')).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with testID: FOO
<View
testID="TEST_ID"
/>"
`);

await expect(view.findByTestId('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with testID: FOO
<View
testID="TEST_ID"
/>"
`);

await expect(view.findAllByTestId('FOO')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with testID: FOO
<View
testID="TEST_ID"
/>"
`);
});
67 changes: 57 additions & 10 deletions src/queries/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as React from 'react';
import {
View,
Text,
TouchableOpacity,
Image,
Button,
Image,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { render, getDefaultNormalizer, within } from '../..';
import { getDefaultNormalizer, render, within } from '../..';

test('byText matches simple text', () => {
const { getByText } = render(<Text testID="text">Hello World</Text>);
@@ -496,11 +496,58 @@ test('byText support hidden option', () => {

expect(queryByText(/hidden/i)).toBeFalsy();
expect(queryByText(/hidden/i, { includeHiddenElements: false })).toBeFalsy();
expect(() =>
getByText(/hidden/i, { includeHiddenElements: false })
).toThrowErrorMatchingInlineSnapshot(
`"Unable to find an element with text: /hidden/i"`
);
expect(() => getByText(/hidden/i, { includeHiddenElements: false }))
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /hidden/i
<Text
style={
{
"display": "none",
}
}
>
Hidden from accessibility
</Text>"
`);
});

test('error message renders the element tree, preserving only helpful props', async () => {
const view = render(<View accessibilityViewIsModal key="this is filtered" />);

expect(() => view.getByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

expect(() => view.getAllByText(/foo/)).toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

await expect(view.findByText(/foo/)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);

await expect(view.findAllByText(/foo/)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to find an element with text: /foo/
<View
accessibilityViewIsModal={true}
/>"
`);
});

test('byText should return host component', () => {
115 changes: 97 additions & 18 deletions src/queries/makeQueries.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@ import type { ReactTestInstance } from 'react-test-renderer';
import { ErrorWithStack } from '../helpers/errors';
import waitFor from '../waitFor';
import type { WaitForOptions } from '../waitFor';
import format from '../helpers/format';
import { screen } from '../screen';
import { defaultMapProps } from '../helpers/format-default';

export type GetByQuery<Predicate, Options = void> = (
predicate: Predicate,
@@ -72,8 +75,8 @@ function extractDeprecatedWaitForOptions(options?: WaitForOptions) {
if (option) {
// eslint-disable-next-line no-console
console.warn(
`Use of option "${key}" in a findBy* query options (2nd parameter) is deprecated. Please pass this option in the waitForOptions (3rd parameter).
Example:
`Use of option "${key}" in a findBy* query options (2nd parameter) is deprecated. Please pass this option in the waitForOptions (3rd parameter).
Example:
findByText(text, {}, { ${key}: ${option.toString()} })`
);
@@ -83,30 +86,68 @@ Example:
return waitForOptions;
}

function formatErrorMessage(message: string, printElementTree: boolean) {
if (!printElementTree) {
return message;
}

const json = screen.toJSON();
if (!json) {
return message;
}

return `${message}\n\n${format(json, {
mapProps: defaultMapProps,
})}`;
}

function appendElementTreeToError(error: Error) {
const oldMessage = error.message;
error.message = formatErrorMessage(oldMessage, true);

// Required to make Jest print the element tree on error
error.stack = error.stack?.replace(oldMessage, error.message);

return error;
}

export function makeQueries<Predicate, Options>(
queryAllByQuery: UnboundQuery<QueryAllByQuery<Predicate, Options>>,
getMissingError: (predicate: Predicate, options?: Options) => string,
getMultipleError: (predicate: Predicate, options?: Options) => string
): UnboundQueries<Predicate, Options> {
function getAllByQuery(instance: ReactTestInstance) {
function getAllByQuery(
instance: ReactTestInstance,
{ printElementTree = true } = {}
) {
return function getAllFn(predicate: Predicate, options?: Options) {
const results = queryAllByQuery(instance)(predicate, options);

if (results.length === 0) {
throw new ErrorWithStack(getMissingError(predicate, options), getAllFn);
const errorMessage = formatErrorMessage(
getMissingError(predicate, options),
printElementTree
);
throw new ErrorWithStack(errorMessage, getAllFn);
}

return results;
};
}

function queryByQuery(instance: ReactTestInstance) {
function queryByQuery(
instance: ReactTestInstance,
{ printElementTree = true } = {}
) {
return function singleQueryFn(predicate: Predicate, options?: Options) {
const results = queryAllByQuery(instance)(predicate, options);

if (results.length > 1) {
throw new ErrorWithStack(
getMultipleError(predicate, options),
formatErrorMessage(
getMultipleError(predicate, options),
printElementTree
),
singleQueryFn
);
}
@@ -119,7 +160,10 @@ export function makeQueries<Predicate, Options>(
};
}

function getByQuery(instance: ReactTestInstance) {
function getByQuery(
instance: ReactTestInstance,
{ printElementTree = true } = {}
) {
return function getFn(predicate: Predicate, options?: Options) {
const results = queryAllByQuery(instance)(predicate, options);

@@ -128,7 +172,11 @@ export function makeQueries<Predicate, Options>(
}

if (results.length === 0) {
throw new ErrorWithStack(getMissingError(predicate, options), getFn);
const errorMessage = formatErrorMessage(
getMissingError(predicate, options),
printElementTree
);
throw new ErrorWithStack(errorMessage, getFn);
}

return results[0];
@@ -139,29 +187,60 @@ export function makeQueries<Predicate, Options>(
return function findAllFn(
predicate: Predicate,
queryOptions?: Options & WaitForOptions,
waitForOptions: WaitForOptions = {}
{
onTimeout = (error) => appendElementTreeToError(error),
...waitForOptions
}: WaitForOptions = {}
) {
const stackTraceError = new ErrorWithStack(
'STACK_TRACE_ERROR',
findAllFn
);
const deprecatedWaitForOptions =
extractDeprecatedWaitForOptions(queryOptions);
return waitFor(() => getAllByQuery(instance)(predicate, queryOptions), {
...deprecatedWaitForOptions,
...waitForOptions,
});

return waitFor(
() =>
getAllByQuery(instance, { printElementTree: false })(
predicate,
queryOptions
),
{
...deprecatedWaitForOptions,
...waitForOptions,
stackTraceError,
onTimeout,
}
);
};
}

function findByQuery(instance: ReactTestInstance) {
return function findFn(
predicate: Predicate,
queryOptions?: Options & WaitForOptions,
waitForOptions: WaitForOptions = {}
{
onTimeout = (error) => appendElementTreeToError(error),
...waitForOptions
}: WaitForOptions = {}
) {
const stackTraceError = new ErrorWithStack('STACK_TRACE_ERROR', findFn);
const deprecatedWaitForOptions =
extractDeprecatedWaitForOptions(queryOptions);
return waitFor(() => getByQuery(instance)(predicate, queryOptions), {
...deprecatedWaitForOptions,
...waitForOptions,
});

return waitFor(
() =>
getByQuery(instance, { printElementTree: false })(
predicate,
queryOptions
),
{
...deprecatedWaitForOptions,
...waitForOptions,
stackTraceError,
onTimeout,
}
);
};
}

16 changes: 12 additions & 4 deletions src/waitFor.ts
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ export type WaitForOptions = {
timeout?: number;
interval?: number;
stackTraceError?: ErrorWithStack;
onTimeout?: (error: unknown) => Error;
onTimeout?: (error: Error) => Error;
};

function waitForInternal<T>(
@@ -164,9 +164,14 @@ function waitForInternal<T>(
}

function handleTimeout() {
let error;
let error: Error;
if (lastError) {
error = lastError;
if (lastError instanceof Error) {
error = lastError;
} else {
error = new Error(String(lastError));
}

if (stackTraceError) {
copyStackTrace(error, stackTraceError);
}
@@ -177,7 +182,10 @@ function waitForInternal<T>(
}
}
if (typeof onTimeout === 'function') {
onTimeout(error);
const result = onTimeout(error);
if (result) {
error = result;
}
}
onDone({ type: 'error', error });
}
2 changes: 1 addition & 1 deletion typings/index.flow.js
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ declare type A11yValue = {
type WaitForOptions = {
timeout?: number,
interval?: number,
onTimeout?: (error: mixed) => Error,
onTimeout?: (error: Error) => Error,
};

type WaitForFunction = <T = any>(
12 changes: 6 additions & 6 deletions website/docs/MigrationV12.md
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ Elements that are hidden from accessiblity, e.g. elements on non-active screen w
Previous behaviour of matching hidden elements can be enabled on query level using [includeHiddenElements](api-queries#includehiddenelements-option) query options or globally using `defaultIncludeHiddenElements`(api#defaultincludehiddenelements-option) configuration option.

## 2. `*ByRole` queries now return only accessibility elements
`*ByRole` queries now return only accessibility elements, either explicitly marked with `accessible` prop or implicit ones where this status is derrived from component type itself (e.g `Text`, `TextInput`, `Switch`, but not `View`).
`*ByRole` queries now return only accessibility elements, either explicitly marked with `accessible` prop or implicit ones where this status is derived from component type itself (e.g `Text`, `TextInput`, `Switch`, but not `View`).

You may need to adjust relevant components under test to make sure they pass `isAccessibilityElement` check.

@@ -28,23 +28,23 @@ Following elements will match:

```ts
// Explicit "accessible" prop for View
<View accessible role="button" />
<View accessible accessibilityRole="button" />

// No need to "accessible" prop for Text, as it is implicitly accessible element.
<Text role="button">Button</Text>
<Text accessibilityRole="button">Button</Text>
```

While following elements will not match:

```ts
// Missing "accessible" prop for View
<View role="button" />
<View accessibilityRole="button" />

// Explicit "accessible={false}" prop for View
<View accessible={false} role="button" />
<View accessible={false} accessibilityRole="button" />

// Explicit "accessible={false}" for Text, which is implicitly accessible element
<Text accessible={false} role="button">Button</Text>
<Text accessible={false} accessibilityRole="button">Button</Text>
```

## 3. `*ByText`, `*ByDisplayValue`, `*ByPlaceholderText` queries now return host elements
2 changes: 1 addition & 1 deletion website/docusaurus.config.js
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ const siteConfig = {
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: `${repoUrl}/blob/main`,
editUrl: `${repoUrl}/blob/main/website`,
sidebarCollapsible: false,
},
blog: {
939 changes: 567 additions & 372 deletions yarn.lock

Large diffs are not rendered by default.