From 300541b1e1b68d184f841220c1f60feb523a1c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augustin=20Le=20F=C3=A8vre?= Date: Sun, 18 Sep 2022 17:44:23 +0200 Subject: [PATCH 1/3] Add support for ByRole with name --- src/queries/__tests__/role.test.tsx | 59 +++++++++++++++++++++++++++++ src/queries/role.ts | 43 +++++++++++++++------ typings/index.flow.js | 21 ++++++++-- website/docs/Queries.md | 7 +++- 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index de5ebfd7f..ee15d66d9 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,61 @@ test('getAllByRole, queryAllByRole, findAllByRole', async () => { getNoInstancesFoundMessage(NO_MATCHES_TEXT) ); }); + +describe('*ByRole with a name', () => { + test('Find 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('Find 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('Find 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('Can find by name using a regex', () => { + 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..652157d09 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,6 +1,8 @@ import type { ReactTestInstance } from 'react-test-renderer'; -import { TextMatch } from '../matches'; +import type { AccessibilityRole } from 'react-native'; import { matchStringProp } from '../helpers/matchers/matchStringProp'; +import { TextMatch } from '../matches'; +import { getQueriesForElement } from '../within'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -11,20 +13,37 @@ import type { QueryByQuery, } from './makeQueries'; +type Role = AccessibilityRole | TextMatch; + +type ByRoleOptions = { + name?: TextMatch; +}; + +const filterByAccessibleName = ( + node: ReactTestInstance, + name: TextMatch | undefined +) => { + if (!name) return true; + + const { queryByText, queryByLabelText } = getQueriesForElement(node); + return Boolean(queryByText(name) || queryByLabelText(name)); +}; + const queryAllByRole = ( instance: ReactTestInstance -): ((role: TextMatch) => Array) => - function queryAllByRoleFn(role) { +): ((role: Role, 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) && + filterByAccessibleName(node, options?.name) ); }; -const getMultipleError = (role: TextMatch) => +const getMultipleError = (role: Role) => `Found multiple elements with accessibilityRole: ${String(role)} `; -const getMissingError = (role: TextMatch) => +const getMissingError = (role: Role) => `Unable to find an element with accessibilityRole: ${String(role)}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( @@ -34,12 +53,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..a1ac17dd4 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -202,6 +202,11 @@ interface UnsafeByPropsQueries { | Array | []; } + +interface ByRoleOption { + name: string; +} + interface A11yAPI { // Label getByLabelText: (matcher: TextMatch) => GetReturn; @@ -244,16 +249,24 @@ 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?: ByRoleOption) => GetReturn; + getAllByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOption + ) => GetAllReturn; + queryByRole: (matcher: A11yRole | RegExp, role?: ByRoleOption) => QueryReturn; + queryAllByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOption + ) => QueryAllReturn; findByRole: ( matcher: A11yRole | RegExp, + role?: ByRoleOption, waitForOptions?: WaitForOptions ) => FindReturn; findAllByRole: ( matcher: A11yRole | RegExp, + role?: ByRoleOption, waitForOptions?: WaitForOptions ) => FindAllReturn; diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 65196a1ea..c1c7d9a90 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 `queryByLabelText`). + ### `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, ''), }); ``` From 188a28529215656a8c260cbaee8424681d1aba3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augustin=20Le=20F=C3=A8vre?= Date: Tue, 20 Sep 2022 14:51:39 +0200 Subject: [PATCH 2/3] Fix test naming for consistency, improve types and ensure multiple children matching is supported --- src/queries/__tests__/role.test.tsx | 23 +++++++++--------- src/queries/role.ts | 36 +++++++++++++---------------- typings/index.flow.js | 19 ++++++++------- website/docs/Queries.md | 2 +- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/queries/__tests__/role.test.tsx b/src/queries/__tests__/role.test.tsx index ee15d66d9..8fb28fc0f 100644 --- a/src/queries/__tests__/role.test.tsx +++ b/src/queries/__tests__/role.test.tsx @@ -79,8 +79,8 @@ test('getAllByRole, queryAllByRole, findAllByRole', async () => { ); }); -describe('*ByRole with a name', () => { - test('Find an element that has the corresponding role and a children with the name', () => { +describe('supports name option', () => { + test('returns an element that has the corresponding role and a children with the name', () => { const { getByRole } = render( Save @@ -93,10 +93,11 @@ describe('*ByRole with a name', () => { ); }); - test('Find an element that has the corresponding role and a children with a matching accessibilityLabel', () => { + test('returns an element that has the corresponding role when several children include the name', () => { const { getByRole } = render( - + Save + Save ); @@ -106,13 +107,11 @@ describe('*ByRole with a name', () => { ); }); - test('Find an element that has the corresponding role and a matching accessibilityLabel', () => { + 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 @@ -121,7 +120,7 @@ describe('*ByRole with a name', () => { ); }); - test('Can find by name using a regex', () => { + 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( + expect(getByRole('button', { name: 'Save' }).props.testID).toBe( 'target-button' ); }); diff --git a/src/queries/role.ts b/src/queries/role.ts index 652157d09..cab17ab2b 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,5 +1,4 @@ import type { ReactTestInstance } from 'react-test-renderer'; -import type { AccessibilityRole } from 'react-native'; import { matchStringProp } from '../helpers/matchers/matchStringProp'; import { TextMatch } from '../matches'; import { getQueriesForElement } from '../within'; @@ -13,37 +12,34 @@ import type { QueryByQuery, } from './makeQueries'; -type Role = AccessibilityRole | TextMatch; - type ByRoleOptions = { name?: TextMatch; }; -const filterByAccessibleName = ( - node: ReactTestInstance, - name: TextMatch | undefined -) => { - if (!name) return true; +const matchAccessibleName = (node: ReactTestInstance, name?: TextMatch) => { + if (name == null) return true; - const { queryByText, queryByLabelText } = getQueriesForElement(node); - return Boolean(queryByText(name) || queryByLabelText(name)); + const { queryAllByText, queryAllByLabelText } = getQueriesForElement(node); + return ( + queryAllByText(name).length > 0 || queryAllByLabelText(name).length > 0 + ); }; const queryAllByRole = ( instance: ReactTestInstance -): ((role: Role, options?: ByRoleOptions) => Array) => +): ((role: TextMatch, options?: ByRoleOptions) => Array) => function queryAllByRoleFn(role, options) { return instance.findAll( (node) => typeof node.type === 'string' && matchStringProp(node.props.accessibilityRole, role) && - filterByAccessibleName(node, options?.name) + matchAccessibleName(node, options?.name) ); }; -const getMultipleError = (role: Role) => +const getMultipleError = (role: TextMatch) => `Found multiple elements with accessibilityRole: ${String(role)} `; -const getMissingError = (role: Role) => +const getMissingError = (role: TextMatch) => `Unable to find an element with accessibilityRole: ${String(role)}`; const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries( @@ -53,12 +49,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 a1ac17dd4..a0f9acbc8 100644 --- a/typings/index.flow.js +++ b/typings/index.flow.js @@ -203,8 +203,8 @@ interface UnsafeByPropsQueries { | []; } -interface ByRoleOption { - name: string; +interface ByRoleOptions { + name?: string; } interface A11yAPI { @@ -249,24 +249,27 @@ interface A11yAPI { ) => FindAllReturn; // Role - getByRole: (matcher: A11yRole | RegExp, role?: ByRoleOption) => GetReturn; + getByRole: (matcher: A11yRole | RegExp, role?: ByRoleOptions) => GetReturn; getAllByRole: ( matcher: A11yRole | RegExp, - role?: ByRoleOption + role?: ByRoleOptions ) => GetAllReturn; - queryByRole: (matcher: A11yRole | RegExp, role?: ByRoleOption) => QueryReturn; + queryByRole: ( + matcher: A11yRole | RegExp, + role?: ByRoleOptions + ) => QueryReturn; queryAllByRole: ( matcher: A11yRole | RegExp, - role?: ByRoleOption + role?: ByRoleOptions ) => QueryAllReturn; findByRole: ( matcher: A11yRole | RegExp, - role?: ByRoleOption, + role?: ByRoleOptions, waitForOptions?: WaitForOptions ) => FindReturn; findAllByRole: ( matcher: A11yRole | RegExp, - role?: ByRoleOption, + role?: ByRoleOptions, waitForOptions?: WaitForOptions ) => FindAllReturn; diff --git a/website/docs/Queries.md b/website/docs/Queries.md index c1c7d9a90..fa446397c 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -191,7 +191,7 @@ const element = screen.getByRole('button'); #### Options -`name`: Finds an element with given `accessibilityRole` and an accessible name (equivalent to `ByText` or `queryByLabelText`). +`name`: Finds an element with given `accessibilityRole` and an accessible name (equivalent to `byText` or `byLabelText` query). ### `ByA11yState`, `ByAccessibilityState` From d51d1dece98b9c71f37b5868b6f69e9514b91aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augustin=20Le=20F=C3=A8vre?= Date: Thu, 22 Sep 2022 14:10:37 +0200 Subject: [PATCH 3/3] Update internal helper name for better readability --- src/queries/role.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/queries/role.ts b/src/queries/role.ts index cab17ab2b..02d3aa223 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -16,7 +16,10 @@ type ByRoleOptions = { name?: TextMatch; }; -const matchAccessibleName = (node: ReactTestInstance, name?: TextMatch) => { +const matchAccessibleNameIfNeeded = ( + node: ReactTestInstance, + name?: TextMatch +) => { if (name == null) return true; const { queryAllByText, queryAllByLabelText } = getQueriesForElement(node); @@ -33,7 +36,7 @@ const queryAllByRole = ( (node) => typeof node.type === 'string' && matchStringProp(node.props.accessibilityRole, role) && - matchAccessibleName(node, options?.name) + matchAccessibleNameIfNeeded(node, options?.name) ); };