From 257701300a554cdbbcf00f267de58042da73e60b Mon Sep 17 00:00:00 2001 From: EBAM006 Date: Sun, 20 Nov 2022 18:05:48 +0100 Subject: [PATCH 01/11] refactor: group tests in accessibility --- src/helpers/__tests__/accessiblity.test.tsx | 302 ++++++++++---------- 1 file changed, 154 insertions(+), 148 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index 2405be1dd..5ec6cd3ef 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -2,174 +2,180 @@ import React from 'react'; import { View, Text, TextInput } from 'react-native'; import { render, isHiddenFromAccessibility, isInaccessible } from '../..'; -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); -}); - -test('returns true for null elements', () => { - expect(isHiddenFromAccessibility(null)).toBe(true); -}); - -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); -}); +describe('isHiddenFromAccessibility', () => { + 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); + }); + + test('returns true for null elements', () => { + expect(isHiddenFromAccessibility(null)).toBe(true); + }); + + 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( - - + test('detects deeply nested elements with accessibilityElementsHidden 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 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); -}); + ); + 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 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); -}); - -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('detects siblings of element with accessibilityViewIsModal prop', () => { - 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('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 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); + }); }); From 05be4aff9445a87bb190d664ebfb55532372408c Mon Sep 17 00:00:00 2001 From: EBAM006 Date: Sun, 20 Nov 2022 18:32:42 +0100 Subject: [PATCH 02/11] feat: add isAccessibilityElement helper --- src/helpers/__tests__/accessiblity.test.tsx | 53 ++++++++++++++++++++- src/helpers/accessiblity.ts | 21 +++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index 5ec6cd3ef..a8feeb172 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { View, Text, TextInput } from 'react-native'; +import { View, Text, TextInput, Pressable } from 'react-native'; import { render, isHiddenFromAccessibility, isInaccessible } from '../..'; +import { isAccessibilityElement } from '../accessiblity'; describe('isHiddenFromAccessibility', () => { test('returns false for accessible elements', () => { @@ -179,3 +180,53 @@ describe('isHiddenFromAccessibility', () => { expect(isInaccessible).toBe(isHiddenFromAccessibility); }); }); + +describe('isAccessibilityElement', () => { + describe('when accessible prop is not defined', () => { + test('returns true for Text component', () => { + const element = render(Hey).getByTestId( + 'text' + ); + expect(isAccessibilityElement(element)).toEqual(true); + }); + + test('returns true for TextInput component', () => { + const element = render().getByTestId('input'); + expect(isAccessibilityElement(element)).toEqual(true); + }); + + test('returns true for Pressable component', () => { + const element = render().getByTestId( + 'pressable' + ); + expect(isAccessibilityElement(element)).toEqual(true); + }); + + test('returns false for View component', () => { + const element = render().getByTestId('element'); + expect(isAccessibilityElement(element)).toEqual(false); + }); + }); + + describe('when accessible prop is defined', () => { + test('returns false when accessible prop is set to false', () => { + const element = render( + + Hey + + ).getByTestId('input'); + expect(isAccessibilityElement(element)).toEqual(false); + }); + + test('returns true when accessible prop is set to true', () => { + const element = render( + + ).getByTestId('view'); + expect(isAccessibilityElement(element)).toEqual(true); + }); + }); + + 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..16747157d 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -2,9 +2,11 @@ import { AccessibilityState, AccessibilityValue, StyleSheet, + 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 +83,20 @@ 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) + ); +} From 7a4b7fcf39f626e2a5614b3ced988e2eea5a42b0 Mon Sep 17 00:00:00 2001 From: EBAM006 Date: Sun, 20 Nov 2022 18:33:21 +0100 Subject: [PATCH 03/11] chore: fix typo --- src/queries/role.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index 641855b92..c260c3ea3 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -63,7 +63,7 @@ const queryAllByRole = ( 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' && matchStringProp(node.props.accessibilityRole, role) && matchAccessibleStateIfNeeded(node, options) && From f0cda1ca863b62ce0d6e172636883bbe3cdded1a Mon Sep 17 00:00:00 2001 From: EBAM006 Date: Sun, 20 Nov 2022 18:35:24 +0100 Subject: [PATCH 04/11] feat: handle accessible in byrole --- src/helpers/__tests__/accessiblity.test.tsx | 7 ++++++- src/helpers/accessiblity.ts | 4 +++- src/queries/__tests__/role-value.test.tsx | 2 ++ src/queries/__tests__/role.test.tsx | 9 +++++++++ src/queries/role.ts | 2 ++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index a8feeb172..b5d50b0c1 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, TextInput, Pressable } from 'react-native'; +import { View, Text, TextInput, Pressable, Switch } from 'react-native'; import { render, isHiddenFromAccessibility, isInaccessible } from '../..'; import { isAccessibilityElement } from '../accessiblity'; @@ -202,6 +202,11 @@ describe('isAccessibilityElement', () => { expect(isAccessibilityElement(element)).toEqual(true); }); + test('returns true for Switch component', () => { + const element = render().getByTestId('switch'); + expect(isAccessibilityElement(element)).toEqual(true); + }); + test('returns false for View component', () => { const element = render().getByTestId('element'); expect(isAccessibilityElement(element)).toEqual(false); diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 16747157d..86a99ae70 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -2,6 +2,7 @@ import { AccessibilityState, AccessibilityValue, StyleSheet, + Switch, Text, TextInput, } from 'react-native'; @@ -97,6 +98,7 @@ export function isAccessibilityElement( return ( isHostElementForType(element, Text) || - isHostElementForType(element, TextInput) + isHostElementForType(element, TextInput) || + isHostElementForType(element, Switch) ); } 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.test.tsx b/src/queries/__tests__/role.test.tsx index e93dde7d8..943c19082 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('takes accessible prop into account', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); +}); diff --git a/src/queries/role.ts b/src/queries/role.ts index c260c3ea3..be1a1a2f0 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'; @@ -65,6 +66,7 @@ const queryAllByRole = ( (node) => // run the cheapest checks first, and early exit to avoid unneeded computations typeof node.type === 'string' && + isAccessibilityElement(node) && matchStringProp(node.props.accessibilityRole, role) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && From 9f731cdf542c01cdfbc64ca0dc48cfad47391457 Mon Sep 17 00:00:00 2001 From: MattAgn Date: Tue, 17 Jan 2023 11:43:57 +0100 Subject: [PATCH 05/11] feat: only check accessibility when useBreakingChanges true --- src/queries/__tests__/role.breaking.test.tsx | 749 +++++++++++++++++++ src/queries/__tests__/role.test.tsx | 6 +- src/queries/role.ts | 6 +- 3 files changed, 757 insertions(+), 4 deletions(-) create mode 100644 src/queries/__tests__/role.breaking.test.tsx diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx new file mode 100644 index 000000000..8a05b8b58 --- /dev/null +++ b/src/queries/__tests__/role.breaking.test.tsx @@ -0,0 +1,749 @@ +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""` + ); +}); + +test('takes accessible prop into account', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); +}); diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index 943c19082..1b377ede0 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -734,11 +734,11 @@ test('byRole queries support hidden option', () => { ); }); -test('takes accessible prop into account', () => { - const { queryByRole } = render( +test('does not take accessible prop into account', () => { + const { getByRole } = render( Action ); - expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); + expect(getByRole('button', { name: 'Action' })).toBeTruthy(); }); diff --git a/src/queries/role.ts b/src/queries/role.ts index be1a1a2f0..e15b51bf4 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -14,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, @@ -61,12 +62,15 @@ const queryAllByRole = ( instance: ReactTestInstance ): ((role: TextMatch, options?: ByRoleOptions) => Array) => function queryAllByRoleFn(role, options) { + const shouldMatchOnlyAccessibilityElements = (node: ReactTestInstance) => + !getConfig().useBreakingChanges || isAccessibilityElement(node); + return findAll( instance, (node) => // run the cheapest checks first, and early exit to avoid unneeded computations typeof node.type === 'string' && - isAccessibilityElement(node) && + shouldMatchOnlyAccessibilityElements(node) && matchStringProp(node.props.accessibilityRole, role) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && From 56347cebe641353a980ea93408ce3874b4129379 Mon Sep 17 00:00:00 2001 From: MattAgn Date: Tue, 17 Jan 2023 11:53:56 +0100 Subject: [PATCH 06/11] test: group a11y tests together --- src/helpers/__tests__/accessiblity.test.tsx | 136 +++++++++++++------- 1 file changed, 88 insertions(+), 48 deletions(-) diff --git a/src/helpers/__tests__/accessiblity.test.tsx b/src/helpers/__tests__/accessiblity.test.tsx index b5d50b0c1..eeee51610 100644 --- a/src/helpers/__tests__/accessiblity.test.tsx +++ b/src/helpers/__tests__/accessiblity.test.tsx @@ -1,5 +1,12 @@ import React from 'react'; -import { View, Text, TextInput, Pressable, Switch } from 'react-native'; +import { + View, + Text, + TextInput, + Pressable, + Switch, + TouchableOpacity, +} from 'react-native'; import { render, isHiddenFromAccessibility, isInaccessible } from '../..'; import { isAccessibilityElement } from '../accessiblity'; @@ -182,53 +189,86 @@ describe('isHiddenFromAccessibility', () => { }); describe('isAccessibilityElement', () => { - describe('when accessible prop is not defined', () => { - test('returns true for Text component', () => { - const element = render(Hey).getByTestId( - 'text' - ); - expect(isAccessibilityElement(element)).toEqual(true); - }); - - test('returns true for TextInput component', () => { - const element = render().getByTestId('input'); - expect(isAccessibilityElement(element)).toEqual(true); - }); - - test('returns true for Pressable component', () => { - const element = render().getByTestId( - 'pressable' - ); - expect(isAccessibilityElement(element)).toEqual(true); - }); - - test('returns true for Switch component', () => { - const element = render().getByTestId('switch'); - expect(isAccessibilityElement(element)).toEqual(true); - }); - - test('returns false for View component', () => { - const element = render().getByTestId('element'); - expect(isAccessibilityElement(element)).toEqual(false); - }); - }); - - describe('when accessible prop is defined', () => { - test('returns false when accessible prop is set to false', () => { - const element = render( - - Hey - - ).getByTestId('input'); - expect(isAccessibilityElement(element)).toEqual(false); - }); - - test('returns true when accessible prop is set to true', () => { - const element = render( - - ).getByTestId('view'); - expect(isAccessibilityElement(element)).toEqual(true); - }); + 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', () => { From 18de89f4828112423e065ce7bc154706473ba3af Mon Sep 17 00:00:00 2001 From: MattAgn Date: Tue, 17 Jan 2023 12:04:59 +0100 Subject: [PATCH 07/11] test: add implicit accessible test --- src/queries/__tests__/role.breaking.test.tsx | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx index 8a05b8b58..19ec7773d 100644 --- a/src/queries/__tests__/role.breaking.test.tsx +++ b/src/queries/__tests__/role.breaking.test.tsx @@ -739,11 +739,22 @@ test('byRole queries support hidden option', () => { ); }); -test('takes accessible prop into account', () => { - const { queryByRole } = render( - - Action - - ); - expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); +describe('matches only accessible elements', () => { + test('takes explicit accessible prop into account', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); + }); + + test('takes implicit accessible value into account', () => { + const { queryByRole } = render( + + Action + + ); + expect(queryByRole('menu', { name: 'Action' })).toBeFalsy(); + }); }); From 86115992d37bedd3f2c2b4dc366dbc5009ea3b3d Mon Sep 17 00:00:00 2001 From: MattAgn Date: Tue, 17 Jan 2023 16:21:39 +0100 Subject: [PATCH 08/11] test: review byrole test titles --- src/queries/__tests__/role.breaking.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx index 19ec7773d..c426e864e 100644 --- a/src/queries/__tests__/role.breaking.test.tsx +++ b/src/queries/__tests__/role.breaking.test.tsx @@ -740,7 +740,7 @@ test('byRole queries support hidden option', () => { }); describe('matches only accessible elements', () => { - test('takes explicit accessible prop into account', () => { + test('ignores elements with accessible={false}', () => { const { queryByRole } = render( Action @@ -749,7 +749,7 @@ describe('matches only accessible elements', () => { expect(queryByRole('button', { name: 'Action' })).toBeFalsy(); }); - test('takes implicit accessible value into account', () => { + test('ignores elements with accessible={undefined} and that are implicitely not accessible', () => { const { queryByRole } = render( Action From 3326de7cd6a943883ef66c48cf746cbdb062d5c5 Mon Sep 17 00:00:00 2001 From: MattAgn Date: Tue, 17 Jan 2023 16:24:41 +0100 Subject: [PATCH 09/11] test: add test case for byRole --- src/queries/__tests__/role.breaking.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/queries/__tests__/role.breaking.test.tsx b/src/queries/__tests__/role.breaking.test.tsx index c426e864e..dbca753ed 100644 --- a/src/queries/__tests__/role.breaking.test.tsx +++ b/src/queries/__tests__/role.breaking.test.tsx @@ -740,6 +740,15 @@ test('byRole queries support hidden option', () => { }); 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( From 670333ab3a80228eece33ac157877f33e2f84530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 17 Jan 2023 22:29:58 +0000 Subject: [PATCH 10/11] refactor: code review changes --- src/queries/role.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index e15b51bf4..3356284a8 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -62,15 +62,15 @@ const queryAllByRole = ( instance: ReactTestInstance ): ((role: TextMatch, options?: ByRoleOptions) => Array) => function queryAllByRoleFn(role, options) { - const shouldMatchOnlyAccessibilityElements = (node: ReactTestInstance) => - !getConfig().useBreakingChanges || isAccessibilityElement(node); + const shouldMatchOnlyAccessibilityElements = getConfig().useBreakingChanges; return findAll( instance, (node) => // run the cheapest checks first, and early exit to avoid unneeded computations typeof node.type === 'string' && - shouldMatchOnlyAccessibilityElements(node) && + (!shouldMatchOnlyAccessibilityElements || + isAccessibilityElement(node)) && matchStringProp(node.props.accessibilityRole, role) && matchAccessibleStateIfNeeded(node, options) && matchAccessibilityValueIfNeeded(node, options?.value) && From 937fa2dc337414bf723bcbd7a7a584069bab61ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 17 Jan 2023 22:37:38 +0000 Subject: [PATCH 11/11] fix: code cov warnings --- src/__tests__/host-component-names.test.tsx | 23 +++++++++++++++++++++ src/helpers/host-component-names.tsx | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) 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/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 = {