diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index de5ebfd7f..8fb28fc0f 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -42,6 +42,7 @@ test('getByRole, queryByRole, findByRole', async () => { expect(() => getByRole(NO_MATCHES_TEXT)).toThrow( getNoInstancesFoundMessage(NO_MATCHES_TEXT) ); + expect(queryByRole(NO_MATCHES_TEXT)).toBeNull(); expect(() => getByRole('link')).toThrow( @@ -77,3 +78,60 @@ test('getAllByRole, queryAllByRole, findAllByRole', async () => { 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' + ); + }); +}); diff --git a/src/queries/role.ts b/src/queries/role.ts index 552998b22..02d3aa223 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,6 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; -import { TextMatch } from '../matches'; import { matchStringProp } from '../helpers/matchers/matchStringProp'; +import { TextMatch } from '../matches'; +import { getQueriesForElement } from '../within'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -11,14 +12,31 @@ import type { QueryByQuery, } from './makeQueries'; +type ByRoleOptions = { + name?: TextMatch; +}; + +const matchAccessibleNameIfNeeded = ( + node: ReactTestInstance, + name?: TextMatch +) => { + if (name == null) return true; + + const { queryAllByText, queryAllByLabelText } = getQueriesForElement(node); + return ( + queryAllByText(name).length > 0 || queryAllByLabelText(name).length > 0 + ); +}; + const queryAllByRole = ( instance: ReactTestInstance -): ((role: TextMatch) => Array) => - function queryAllByRoleFn(role) { +): ((role: TextMatch, options?: ByRoleOptions) => Array) => + function queryAllByRoleFn(role, options) { return instance.findAll( (node) => typeof node.type === 'string' && - matchStringProp(node.props.accessibilityRole, role) + matchStringProp(node.props.accessibilityRole, role) && + matchAccessibleNameIfNeeded(node, options?.name) ); }; @@ -34,12 +52,12 @@ const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( ); export type ByRoleQueries = { - getByRole: GetByQuery; - getAllByRole: GetAllByQuery; - queryByRole: QueryByQuery; - queryAllByRole: QueryAllByQuery; - findByRole: FindByQuery; - findAllByRole: FindAllByQuery; + getByRole: GetByQuery; + getAllByRole: GetAllByQuery; + queryByRole: QueryByQuery; + queryAllByRole: QueryAllByQuery; + findByRole: FindByQuery; + findAllByRole: FindAllByQuery; }; export const bindByRoleQueries = ( diff --git a/typings/index.flow.js b/typings/index.flow.js index b8f2ec0f1..a0f9acbc8 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -202,6 +202,11 @@ interface UnsafeByPropsQueries { | Array | []; } + +interface ByRoleOptions { + name?: string; +} + interface A11yAPI { // Label getByLabelText: (matcher: TextMatch) => GetReturn; @@ -244,16 +249,27 @@ interface A11yAPI { ) => FindAllReturn; // Role - getByRole: (matcher: A11yRole | RegExp) => GetReturn; - getAllByRole: (matcher: A11yRole | RegExp) => GetAllReturn; - queryByRole: (matcher: A11yRole | RegExp) => QueryReturn; - queryAllByRole: (matcher: A11yRole | RegExp) => QueryAllReturn; + getByRole: (matcher: A11yRole | RegExp, role?: ByRoleOptions) => GetReturn; + getAllByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOptions + ) => GetAllReturn; + queryByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOptions + ) => QueryReturn; + queryAllByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOptions + ) => QueryAllReturn; findByRole: ( matcher: A11yRole | RegExp, + role?: ByRoleOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByRole: ( matcher: A11yRole | RegExp, + role?: ByRoleOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 65196a1ea..fa446397c 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -189,6 +189,10 @@ render(); const element = screen.getByRole('button'); ``` +#### Options + +`name`: Finds an element with given `accessibilityRole` and an accessible name (equivalent to `byText` or `byLabelText` query). + ### `ByA11yState`, `ByAccessibilityState` > getByA11yState, getAllByA11yState, queryByA11yState, queryAllByA11yState, findByA11yState, findAllByA11yState @@ -305,7 +309,8 @@ To override normalization to remove some Unicode characters whilst keeping some ```typescript screen.getByText(node, 'text', { - normalizer: (str) => getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''), + normalizer: (str) => + getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''), }); ```