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) &&