diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx
index 98b1cbc6d..0a1fd203b 100644
--- a/src/__tests__/host-component-names.test.tsx
+++ b/src/__tests__/host-component-names.test.tsx
@@ -2,8 +2,13 @@ import { View } from 'react-native';
import TestRenderer from 'react-test-renderer';
import { configureInternal, getConfig } from '../config';
import { getHostComponentNames } from '../helpers/host-component-names';
+import * as within from '../within';
const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock;
+const mockGetQueriesForElements = jest.spyOn(
+ within,
+ 'getQueriesForElement'
+) as jest.Mock;
describe('getHostComponentNames', () => {
test('updates internal config with host component names when they are not defined', () => {
@@ -45,4 +50,22 @@ describe('getHostComponentNames', () => {
"
`);
});
+
+ test('throw an error when autodetection fails due to getByTestId returning non-host component', () => {
+ mockGetQueriesForElements.mockReturnValue({
+ getByTestId: () => {
+ return { type: View };
+ },
+ });
+
+ expect(() => getHostComponentNames()).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.
+ "
+ `);
+ });
});
diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx
index 2405be1dd..eeee51610 100644
--- a/src/helpers/__tests__/accessiblity.test.tsx
+++ b/src/helpers/__tests__/accessiblity.test.tsx
@@ -1,175 +1,277 @@
import React from 'react';
-import { View, Text, TextInput } from 'react-native';
+import {
+ View,
+ Text,
+ TextInput,
+ Pressable,
+ Switch,
+ TouchableOpacity,
+} from 'react-native';
import { render, isHiddenFromAccessibility, isInaccessible } from '../..';
+import { isAccessibilityElement } from '../accessiblity';
-test('returns false for accessible elements', () => {
- expect(
- isHiddenFromAccessibility(
- render().getByTestId('subject')
- )
- ).toBe(false);
-
- expect(
- isHiddenFromAccessibility(
- render(Hello).getByTestId('subject')
- )
- ).toBe(false);
-
- expect(
- isHiddenFromAccessibility(
- render().getByTestId('subject')
- )
- ).toBe(false);
-});
+describe('isHiddenFromAccessibility', () => {
+ test('returns false for accessible elements', () => {
+ expect(
+ isHiddenFromAccessibility(
+ render().getByTestId('subject')
+ )
+ ).toBe(false);
-test('returns true for null elements', () => {
- expect(isHiddenFromAccessibility(null)).toBe(true);
-});
+ expect(
+ isHiddenFromAccessibility(
+ render(Hello).getByTestId('subject')
+ )
+ ).toBe(false);
-test('detects elements with accessibilityElementsHidden prop', () => {
- const view = render();
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ expect(
+ isHiddenFromAccessibility(
+ render().getByTestId('subject')
+ )
+ ).toBe(false);
+ });
-test('detects nested elements with accessibilityElementsHidden prop', () => {
- const view = render(
-
-
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('returns true for null elements', () => {
+ expect(isHiddenFromAccessibility(null)).toBe(true);
+ });
-test('detects deeply nested elements with accessibilityElementsHidden prop', () => {
- const view = render(
-
-
+ test('detects elements with accessibilityElementsHidden prop', () => {
+ const view = render();
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
+
+ test('detects nested elements with accessibilityElementsHidden prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
+
+ test('detects deeply nested elements with accessibilityElementsHidden prop', () => {
+ const view = render(
+
-
+
+
+
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects elements with importantForAccessibility="no-hide-descendants" prop', () => {
- const view = render(
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects elements with importantForAccessibility="no-hide-descendants" prop', () => {
+ const view = render(
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', () => {
- const view = render(
-
-
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects nested elements with importantForAccessibility="no-hide-descendants" prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects elements with display=none', () => {
- const view = render();
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects elements with display=none', () => {
+ const view = render();
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects nested elements with display=none', () => {
- const view = render(
-
-
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects nested elements with display=none', () => {
+ const view = render(
+
+
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects deeply nested elements with display=none', () => {
- const view = render(
-
-
+ test('detects deeply nested elements with display=none', () => {
+ const view = render(
+
-
+
+
+
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects elements with display=none with complex style', () => {
- const view = render(
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects elements with display=none with complex style', () => {
+ const view = render(
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('is not trigged by opacity = 0', () => {
- const view = render();
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
-});
+ test('is not trigged by opacity = 0', () => {
+ const view = render();
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
+ });
-test('detects siblings of element with accessibilityViewIsModal prop', () => {
- const view = render(
-
-
-
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ test('detects siblings of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('detects deeply nested siblings of element with accessibilityViewIsModal prop', () => {
- const view = render(
-
-
+ test('detects deeply nested siblings of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
-
+
+
+
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
-});
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(true);
+ });
-test('is not triggered for element with accessibilityViewIsModal prop', () => {
- const view = render(
-
-
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
-});
+ test('is not triggered for element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
+ });
-test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
- const view = render(
-
-
-
+ test('is not triggered for child of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
+
+
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
-});
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
+ });
-test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
- const view = render(
-
-
-
+ test('is not triggered for descendent of element with accessibilityViewIsModal prop', () => {
+ const view = render(
+
+
-
+
+
+
-
- );
- expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
+ );
+ expect(isHiddenFromAccessibility(view.getByTestId('subject'))).toBe(false);
+ });
+
+ test('has isInaccessible alias', () => {
+ expect(isInaccessible).toBe(isHiddenFromAccessibility);
+ });
});
-test('has isInaccessible alias', () => {
- expect(isInaccessible).toBe(isHiddenFromAccessibility);
+describe('isAccessibilityElement', () => {
+ test('matches View component properly', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeFalsy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('matches TextInput component properly', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('matches Text component properly', () => {
+ const { getByTestId } = render(
+
+ Default
+
+ True
+
+
+ False
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('matches Switch component properly', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('matches Pressable component properly', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('matches TouchableOpacity component properly', () => {
+ const { getByTestId } = render(
+
+
+
+
+
+ );
+ expect(isAccessibilityElement(getByTestId('default'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('true'))).toBeTruthy();
+ expect(isAccessibilityElement(getByTestId('false'))).toBeFalsy();
+ });
+
+ test('returns false when given null', () => {
+ expect(isAccessibilityElement(null)).toEqual(false);
+ });
});
diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts
index 8d74db91a..86a99ae70 100644
--- a/src/helpers/accessiblity.ts
+++ b/src/helpers/accessiblity.ts
@@ -2,9 +2,12 @@ import {
AccessibilityState,
AccessibilityValue,
StyleSheet,
+ Switch,
+ Text,
+ TextInput,
} from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
-import { getHostSiblings } from './component-tree';
+import { getHostSiblings, isHostElementForType } from './component-tree';
type IsInaccessibleOptions = {
cache?: WeakMap;
@@ -81,3 +84,21 @@ function isSubtreeInaccessible(element: ReactTestInstance): boolean {
return false;
}
+
+export function isAccessibilityElement(
+ element: ReactTestInstance | null
+): boolean {
+ if (element == null) {
+ return false;
+ }
+
+ if (element.props.accessible !== undefined) {
+ return element.props.accessible;
+ }
+
+ return (
+ isHostElementForType(element, Text) ||
+ isHostElementForType(element, TextInput) ||
+ isHostElementForType(element, Switch)
+ );
+}
diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx
index d4611702f..5872c2417 100644
--- a/src/helpers/host-component-names.tsx
+++ b/src/helpers/host-component-names.tsx
@@ -29,7 +29,7 @@ export function getHostComponentNames(): HostComponentNames {
typeof textHostName !== 'string' ||
typeof textInputHostName !== 'string'
) {
- throw new Error(defaultErrorMessage);
+ throw new Error('getByTestId returned non-host component');
}
const hostComponentNames = {
diff --git a/src/queries/__tests__/role-value.test.tsx b/src/queries/__tests__/role-value.test.tsx
index e9b03e9c8..80c2c3ea8 100644
--- a/src/queries/__tests__/role-value.test.tsx
+++ b/src/queries/__tests__/role-value.test.tsx
@@ -6,6 +6,7 @@ describe('accessibility value', () => {
test('matches using all value props', () => {
const { getByRole, queryByRole } = render(
@@ -41,6 +42,7 @@ describe('accessibility value', () => {
test('matches using single value', () => {
const { getByRole, queryByRole } = render(
diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx
new file mode 100644
index 000000000..dbca753ed
--- /dev/null
+++ b/src/queries/__tests__/role.breaking.test.tsx
@@ -0,0 +1,769 @@
+import * as React from 'react';
+import {
+ TouchableOpacity,
+ TouchableWithoutFeedback,
+ Text,
+ View,
+ Pressable,
+ Button as RNButton,
+} from 'react-native';
+import { render } from '../..';
+import { configureInternal } from '../../config';
+
+beforeEach(() => {
+ configureInternal({ useBreakingChanges: true });
+});
+
+const TEXT_LABEL = 'cool text';
+
+// Little hack to make all the methods happy with type
+const NO_MATCHES_TEXT: any = 'not-existent-element';
+
+const getMultipleInstancesFoundMessage = (value: string) => {
+ return `Found multiple elements with role: "${value}"`;
+};
+
+const getNoInstancesFoundMessage = (value: string) => {
+ return `Unable to find an element with role: "${value}"`;
+};
+
+const Typography = ({ children, ...rest }: any) => {
+ return {children};
+};
+
+const Button = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+const Section = () => (
+ <>
+ Title
+
+ >
+);
+
+test('getByRole, queryByRole, findByRole', async () => {
+ const { getByRole, queryByRole, findByRole } = render();
+
+ expect(getByRole('button').props.accessibilityRole).toEqual('button');
+ const button = queryByRole(/button/g);
+ expect(button?.props.accessibilityRole).toEqual('button');
+
+ expect(() => getByRole(NO_MATCHES_TEXT)).toThrow(
+ getNoInstancesFoundMessage(NO_MATCHES_TEXT)
+ );
+
+ expect(queryByRole(NO_MATCHES_TEXT)).toBeNull();
+
+ expect(() => getByRole('link')).toThrow(
+ getMultipleInstancesFoundMessage('link')
+ );
+ expect(() => queryByRole('link')).toThrow(
+ getMultipleInstancesFoundMessage('link')
+ );
+
+ const asyncButton = await findByRole('button');
+ expect(asyncButton.props.accessibilityRole).toEqual('button');
+ await expect(findByRole(NO_MATCHES_TEXT)).rejects.toThrow(
+ getNoInstancesFoundMessage(NO_MATCHES_TEXT)
+ );
+ await expect(findByRole('link')).rejects.toThrow(
+ getMultipleInstancesFoundMessage('link')
+ );
+});
+
+test('getAllByRole, queryAllByRole, findAllByRole', async () => {
+ const { getAllByRole, queryAllByRole, findAllByRole } = render();
+
+ expect(getAllByRole('link')).toHaveLength(2);
+ expect(queryAllByRole(/ink/g)).toHaveLength(2);
+
+ expect(() => getAllByRole(NO_MATCHES_TEXT)).toThrow(
+ getNoInstancesFoundMessage(NO_MATCHES_TEXT)
+ );
+ expect(queryAllByRole(NO_MATCHES_TEXT)).toEqual([]);
+
+ await expect(findAllByRole('link')).resolves.toHaveLength(2);
+ await expect(findAllByRole(NO_MATCHES_TEXT)).rejects.toThrow(
+ getNoInstancesFoundMessage(NO_MATCHES_TEXT)
+ );
+});
+
+describe('supports name option', () => {
+ test('returns an element that has the corresponding role and a children with the name', () => {
+ const { getByRole } = render(
+
+ Save
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
+ 'target-button'
+ );
+ });
+
+ test('returns an element that has the corresponding role when several children include the name', () => {
+ const { getByRole } = render(
+
+ Save
+ Save
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
+ 'target-button'
+ );
+ });
+
+ test('returns an element that has the corresponding role and a children with a matching accessibilityLabel', () => {
+ const { getByRole } = render(
+
+
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
+ 'target-button'
+ );
+ });
+
+ test('returns an element that has the corresponding role and a matching accessibilityLabel', () => {
+ const { getByRole } = render(
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('button', { name: 'Save' }).props.testID).toBe(
+ 'target-button'
+ );
+ });
+
+ test('returns an element when the direct child is text', () => {
+ const { getByRole, getByTestId } = render(
+
+ About
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('header', { name: 'About' })).toBe(
+ getByTestId('target-header')
+ );
+ expect(getByRole('header', { name: 'About' }).props.testID).toBe(
+ 'target-header'
+ );
+ });
+
+ test('returns an element with nested Text as children', () => {
+ const { getByRole, getByTestId } = render(
+
+ About
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('header', { name: 'About' })).toBe(getByTestId('parent'));
+ expect(getByRole('header', { name: 'About' }).props.testID).toBe('parent');
+ });
+
+ test('returns a header with an accessibilityLabel', () => {
+ const { getByRole, getByTestId } = render(
+
+ );
+
+ // assert on the testId to be sure that the returned element is the one with the accessibilityRole
+ expect(getByRole('header', { name: 'About' })).toBe(
+ getByTestId('target-header')
+ );
+ expect(getByRole('header', { name: 'About' }).props.testID).toBe(
+ 'target-header'
+ );
+ });
+});
+
+describe('supports accessibility states', () => {
+ describe('disabled', () => {
+ test('returns a disabled element when required', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { disabled: true })).toBeTruthy();
+ expect(queryByRole('button', { disabled: false })).toBe(null);
+ });
+
+ test('returns the correct element when only one matches all the requirements', () => {
+ const { getByRole } = render(
+ <>
+
+ Save
+
+
+ Save
+
+ >
+ );
+
+ expect(
+ getByRole('button', { name: 'Save', disabled: true }).props.testID
+ ).toBe('correct');
+ });
+
+ test('returns an implicitly enabled element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { disabled: false })).toBeTruthy();
+ expect(queryByRole('button', { disabled: true })).toBe(null);
+ });
+
+ test('returns an explicitly enabled element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { disabled: false })).toBeTruthy();
+ expect(queryByRole('button', { disabled: true })).toBe(null);
+ });
+
+ test('does not return disabled elements when querying for non disabled', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('button', { disabled: false })).toBe(null);
+ });
+
+ test('returns elements using the built-in disabled prop', () => {
+ const { getByRole } = render(
+ <>
+
+ Pressable
+
+
+
+
+ TouchableWithoutFeedback
+
+
+ {}} title="RNButton" />
+ >
+ );
+
+ expect(
+ getByRole('button', { name: 'Pressable', disabled: true })
+ ).toBeTruthy();
+
+ expect(
+ getByRole('button', {
+ name: 'TouchableWithoutFeedback',
+ disabled: true,
+ })
+ ).toBeTruthy();
+
+ expect(
+ getByRole('button', { name: 'RNButton', disabled: true })
+ ).toBeTruthy();
+ });
+ });
+
+ describe('selected', () => {
+ test('returns a selected element when required', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('tab', { selected: true })).toBeTruthy();
+ expect(queryByRole('tab', { selected: false })).toBe(null);
+ });
+
+ test('returns the correct element when only one matches all the requirements', () => {
+ const { getByRole } = render(
+ <>
+
+ Save
+
+
+ Save
+
+ >
+ );
+
+ expect(
+ getByRole('tab', { name: 'Save', selected: true }).props.testID
+ ).toBe('correct');
+ });
+
+ test('returns an implicitly non selected element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('tab', { selected: false })).toBeTruthy();
+ expect(queryByRole('tab', { selected: true })).toBe(null);
+ });
+
+ test('returns an explicitly non selected element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('tab', { selected: false })).toBeTruthy();
+ expect(queryByRole('tab', { selected: true })).toBe(null);
+ });
+
+ test('does not return selected elements when querying for non selected', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('tab', { selected: false })).toBe(null);
+ });
+ });
+
+ describe('checked', () => {
+ test('returns a checked element when required', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('checkbox', { checked: true })).toBeTruthy();
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ expect(queryByRole('checkbox', { checked: 'mixed' })).toBe(null);
+ });
+
+ it('returns `mixed` checkboxes', () => {
+ const { queryByRole, getByRole } = render(
+
+ );
+
+ expect(getByRole('checkbox', { checked: 'mixed' })).toBeTruthy();
+ expect(queryByRole('checkbox', { checked: true })).toBe(null);
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ });
+
+ it('does not return mixed checkboxes when querying for checked: true', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ });
+
+ test('returns the correct element when only one matches all the requirements', () => {
+ const { getByRole } = render(
+ <>
+
+ Save
+
+
+ Save
+
+ >
+ );
+
+ expect(
+ getByRole('checkbox', { name: 'Save', checked: true }).props.testID
+ ).toBe('correct');
+ });
+
+ test('does not return return as non checked an element with checked: undefined', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ });
+
+ test('returns an explicitly non checked element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('checkbox', { checked: false })).toBeTruthy();
+ expect(queryByRole('checkbox', { checked: true })).toBe(null);
+ });
+
+ test('does not return checked elements when querying for non checked', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ });
+
+ test('does not return mixed elements when querying for non checked', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('checkbox', { checked: false })).toBe(null);
+ });
+ });
+
+ describe('busy', () => {
+ test('returns a busy element when required', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { busy: true })).toBeTruthy();
+ expect(queryByRole('button', { busy: false })).toBe(null);
+ });
+
+ test('returns the correct element when only one matches all the requirements', () => {
+ const { getByRole } = render(
+ <>
+
+ Save
+
+
+ Save
+
+ >
+ );
+
+ expect(
+ getByRole('button', { name: 'Save', busy: true }).props.testID
+ ).toBe('correct');
+ });
+
+ test('returns an implicitly non busy element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { busy: false })).toBeTruthy();
+ expect(queryByRole('button', { busy: true })).toBe(null);
+ });
+
+ test('returns an explicitly non busy element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { busy: false })).toBeTruthy();
+ expect(queryByRole('button', { busy: true })).toBe(null);
+ });
+
+ test('does not return busy elements when querying for non busy', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('button', { selected: false })).toBe(null);
+ });
+ });
+
+ describe('expanded', () => {
+ test('returns a expanded element when required', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { expanded: true })).toBeTruthy();
+ expect(queryByRole('button', { expanded: false })).toBe(null);
+ });
+
+ test('returns the correct element when only one matches all the requirements', () => {
+ const { getByRole } = render(
+ <>
+
+ Save
+
+
+ Save
+
+ >
+ );
+
+ expect(
+ getByRole('button', { name: 'Save', expanded: true }).props.testID
+ ).toBe('correct');
+ });
+
+ test('does not return return as non expanded an element with expanded: undefined', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('button', { expanded: false })).toBe(null);
+ });
+
+ test('returns an explicitly non expanded element', () => {
+ const { getByRole, queryByRole } = render(
+
+ );
+
+ expect(getByRole('button', { expanded: false })).toBeTruthy();
+ expect(queryByRole('button', { expanded: true })).toBe(null);
+ });
+
+ test('does not return expanded elements when querying for non expanded', () => {
+ const { queryByRole } = render(
+
+ );
+
+ expect(queryByRole('button', { expanded: false })).toBe(null);
+ });
+ });
+
+ test('ignores non queried accessibilityState', () => {
+ const { getByRole, queryByRole } = render(
+
+ Save
+
+ );
+
+ expect(
+ getByRole('button', {
+ name: 'Save',
+ disabled: true,
+ })
+ ).toBeTruthy();
+ expect(
+ queryByRole('button', {
+ name: 'Save',
+ disabled: false,
+ })
+ ).toBe(null);
+ });
+
+ test('matches an element combining all the options', () => {
+ const { getByRole } = render(
+
+ Save
+
+ );
+
+ expect(
+ getByRole('button', {
+ name: 'Save',
+ disabled: true,
+ selected: true,
+ checked: true,
+ busy: true,
+ expanded: true,
+ })
+ ).toBeTruthy();
+ });
+});
+
+describe('error messages', () => {
+ test('gives a descriptive error message when querying with a role', () => {
+ const { getByRole } = render();
+
+ expect(() => getByRole('button')).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "button""`
+ );
+ });
+
+ test('gives a descriptive error message when querying with a role and a name', () => {
+ const { getByRole } = render();
+
+ expect(() =>
+ getByRole('button', { name: 'Save' })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "button", name: "Save""`
+ );
+ });
+
+ test('gives a descriptive error message when querying with a role, a name and accessibility state', () => {
+ const { getByRole } = render();
+
+ expect(() =>
+ getByRole('button', { name: 'Save', disabled: true })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "button", name: "Save", disabled state: true"`
+ );
+ });
+
+ test('gives a descriptive error message when querying with a role, a name and several accessibility state', () => {
+ const { getByRole } = render();
+
+ 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"`
+ );
+ });
+
+ test('gives a descriptive error message when querying with a role and an accessibility state', () => {
+ const { getByRole } = render();
+
+ expect(() =>
+ getByRole('button', { disabled: true })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "button", disabled state: true"`
+ );
+ });
+
+ test('gives a descriptive error message when querying with a role and an accessibility value', () => {
+ const { getByRole } = render();
+
+ expect(() =>
+ getByRole('adjustable', { value: { min: 1 } })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "adjustable", min value: 1"`
+ );
+
+ 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/"`
+ );
+ });
+});
+
+test('byRole queries support hidden option', () => {
+ const { getByRole, queryByRole } = render(
+
+ Hidden from accessibility
+
+ );
+
+ expect(getByRole('button')).toBeTruthy();
+ expect(getByRole('button', { includeHiddenElements: true })).toBeTruthy();
+
+ expect(queryByRole('button', { includeHiddenElements: false })).toBeFalsy();
+ expect(() =>
+ getByRole('button', { includeHiddenElements: false })
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Unable to find an element with role: "button""`
+ );
+});
+
+describe('matches only accessible elements', () => {
+ test('matches elements with accessible={true}', () => {
+ const { queryByRole } = render(
+
+ Action
+
+ );
+ expect(queryByRole('menu', { name: 'Action' })).toBeTruthy();
+ });
+
+ test('ignores elements with accessible={false}', () => {
+ const { queryByRole } = render(
+
+ Action
+
+ );
+ expect(queryByRole('button', { name: 'Action' })).toBeFalsy();
+ });
+
+ test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => {
+ const { queryByRole } = render(
+
+ Action
+
+ );
+ expect(queryByRole('menu', { name: 'Action' })).toBeFalsy();
+ });
+});
diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx
index e93dde7d8..1b377ede0 100644
--- a/src/queries/__tests__/role.test.tsx
+++ b/src/queries/__tests__/role.test.tsx
@@ -733,3 +733,12 @@ test('byRole queries support hidden option', () => {
`"Unable to find an element with role: "button""`
);
});
+
+test('does not take accessible prop into account', () => {
+ const { getByRole } = render(
+
+ Action
+
+ );
+ expect(getByRole('button', { name: 'Action' })).toBeTruthy();
+});
diff --git a/src/queries/role.ts b/src/queries/role.ts
index 641855b92..3356284a8 100644
--- a/src/queries/role.ts
+++ b/src/queries/role.ts
@@ -3,6 +3,7 @@ import type { ReactTestInstance } from 'react-test-renderer';
import {
accessibilityStateKeys,
accessiblityValueKeys,
+ isAccessibilityElement,
} from '../helpers/accessiblity';
import { findAll } from '../helpers/findAll';
import { matchAccessibilityState } from '../helpers/matchers/accessibilityState';
@@ -13,6 +14,7 @@ import {
import { matchStringProp } from '../helpers/matchers/matchStringProp';
import type { TextMatch } from '../matches';
import { getQueriesForElement } from '../within';
+import { getConfig } from '../config';
import { makeQueries } from './makeQueries';
import type {
FindAllByQuery,
@@ -60,11 +62,15 @@ const queryAllByRole = (
instance: ReactTestInstance
): ((role: TextMatch, options?: ByRoleOptions) => Array) =>
function queryAllByRoleFn(role, options) {
+ const shouldMatchOnlyAccessibilityElements = getConfig().useBreakingChanges;
+
return findAll(
instance,
(node) =>
- // run the cheapest checks first, and early exit too avoid unneeded computations
+ // run the cheapest checks first, and early exit to avoid unneeded computations
typeof node.type === 'string' &&
+ (!shouldMatchOnlyAccessibilityElements ||
+ isAccessibilityElement(node)) &&
matchStringProp(node.props.accessibilityRole, role) &&
matchAccessibleStateIfNeeded(node, options) &&
matchAccessibilityValueIfNeeded(node, options?.value) &&