diff --git a/package.json b/package.json index 81e1aba96..71b5791f3 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,9 @@ }, "jest": { "preset": "./jest-preset", - "setupFilesAfterEnv": ["./jest-setup.ts"], + "setupFilesAfterEnv": [ + "./jest-setup.ts" + ], "testPathIgnorePatterns": [ "timerUtils", "examples/" diff --git a/src/helpers/matchers/matchLabelText.ts b/src/helpers/matchers/matchLabelText.ts new file mode 100644 index 000000000..8058f2904 --- /dev/null +++ b/src/helpers/matchers/matchLabelText.ts @@ -0,0 +1,51 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { matches, TextMatch, TextMatchOptions } from '../../matches'; +import { findAll } from '../findAll'; +import { matchTextContent } from './matchTextContent'; + +export function matchLabelText( + root: ReactTestInstance, + element: ReactTestInstance, + text: TextMatch, + options: TextMatchOptions = {} +) { + return ( + matchAccessibilityLabel(element, text, options) || + matchAccessibilityLabelledBy( + root, + element.props.accessibilityLabelledBy, + text, + options + ) + ); +} + +function matchAccessibilityLabel( + element: ReactTestInstance, + text: TextMatch, + options: TextMatchOptions +) { + const { exact, normalizer } = options; + return matches(text, element.props.accessibilityLabel, normalizer, exact); +} + +function matchAccessibilityLabelledBy( + root: ReactTestInstance, + nativeId: string | undefined, + text: TextMatch, + options: TextMatchOptions +) { + if (!nativeId) { + return false; + } + + return ( + findAll( + root, + (node) => + typeof node.type === 'string' && + node.props.nativeID === nativeId && + matchTextContent(node, text, options) + ).length > 0 + ); +} diff --git a/src/helpers/matchers/matchTextContent.ts b/src/helpers/matchers/matchTextContent.ts index cf7770e27..4c025e6ac 100644 --- a/src/helpers/matchers/matchTextContent.ts +++ b/src/helpers/matchers/matchTextContent.ts @@ -1,23 +1,19 @@ -import { Text } from 'react-native'; import type { ReactTestInstance } from 'react-test-renderer'; -import { getConfig } from '../../config'; import { matches, TextMatch, TextMatchOptions } from '../../matches'; -import { filterNodeByType } from '../filterNodeByType'; import { getTextContent } from '../getTextContent'; -import { getHostComponentNames } from '../host-component-names'; +/** + * Matches the given node's text content against string or regex matcher. + * + * @param node - Node which text content will be matched + * @param text - The string or regex to match. + * @returns - Whether the node's text content matches the given string or regex. + */ export function matchTextContent( node: ReactTestInstance, text: TextMatch, options: TextMatchOptions = {} ) { - const textType = getConfig().useBreakingChanges - ? getHostComponentNames().text - : Text; - if (!filterNodeByType(node, textType)) { - return false; - } - const textContent = getTextContent(node); const { exact, normalizer } = options; return matches(text, textContent, normalizer, exact); diff --git a/src/queries/__tests__/labelText.test.tsx b/src/queries/__tests__/labelText.test.tsx index 9e1b5c0cd..07aa3fccc 100644 --- a/src/queries/__tests__/labelText.test.tsx +++ b/src/queries/__tests__/labelText.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, Text, TouchableOpacity } from 'react-native'; +import { View, Text, TextInput, TouchableOpacity } from 'react-native'; import { render } from '../..'; const BUTTON_LABEL = 'cool button'; @@ -165,3 +165,31 @@ test('byLabelText queries support hidden option', () => { `"Unable to find an element with accessibilityLabel: hidden"` ); }); + +test('getByLabelText supports accessibilityLabelledBy', async () => { + const { getByLabelText, getByTestId } = render( + <> + Label for input + {/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */} + + + ); + + expect(getByLabelText('Label for input')).toBe(getByTestId('textInput')); + expect(getByLabelText(/input/)).toBe(getByTestId('textInput')); +}); + +test('getByLabelText supports nested accessibilityLabelledBy', async () => { + const { getByLabelText, getByTestId } = render( + <> + + Label for input + + {/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */} + + + ); + + expect(getByLabelText('Label for input')).toBe(getByTestId('textInput')); + expect(getByLabelText(/input/)).toBe(getByTestId('textInput')); +}); diff --git a/src/queries/__tests__/text.breaking.test.tsx b/src/queries/__tests__/text.breaking.test.tsx index 322588ad7..eb243edd1 100644 --- a/src/queries/__tests__/text.breaking.test.tsx +++ b/src/queries/__tests__/text.breaking.test.tsx @@ -510,6 +510,5 @@ test('byText support hidden option', () => { test('byText should return host component', () => { const { getByText } = render(hello); - expect(getByText('hello').type).toBe('Text'); }); diff --git a/src/queries/__tests__/text.test.tsx b/src/queries/__tests__/text.test.tsx index a7d280d32..27f9eb82a 100644 --- a/src/queries/__tests__/text.test.tsx +++ b/src/queries/__tests__/text.test.tsx @@ -505,6 +505,5 @@ test('byText support hidden option', () => { test('byText should return composite Text', () => { const { getByText } = render(hello); - expect(getByText('hello').type).toBe(Text); }); diff --git a/src/queries/labelText.ts b/src/queries/labelText.ts index acfed4520..7b5fba156 100644 --- a/src/queries/labelText.ts +++ b/src/queries/labelText.ts @@ -1,6 +1,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { findAll } from '../helpers/findAll'; -import { matches, TextMatch, TextMatchOptions } from '../matches'; +import { TextMatch, TextMatchOptions } from '../matches'; +import { matchLabelText } from '../helpers/matchers/matchLabelText'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -14,30 +15,17 @@ import { CommonQueryOptions } from './options'; type ByLabelTextOptions = CommonQueryOptions & TextMatchOptions; -const getNodeByLabelText = ( - node: ReactTestInstance, - text: TextMatch, - options: TextMatchOptions = {} -) => { - const { exact, normalizer } = options; - return matches(text, node.props.accessibilityLabel, normalizer, exact); -}; - -const queryAllByLabelText = ( - instance: ReactTestInstance -): (( - text: TextMatch, - queryOptions?: ByLabelTextOptions -) => Array) => - function queryAllByLabelTextFn(text, queryOptions) { +function queryAllByLabelText(instance: ReactTestInstance) { + return (text: TextMatch, queryOptions?: ByLabelTextOptions) => { return findAll( instance, (node) => typeof node.type === 'string' && - getNodeByLabelText(node, text, queryOptions), + matchLabelText(instance, node, text, queryOptions), queryOptions ); }; +} const getMultipleError = (labelText: TextMatch) => `Found multiple elements with accessibilityLabel: ${String(labelText)} `; diff --git a/src/queries/testId.ts b/src/queries/testId.ts index 3e7977d9f..c33122b91 100644 --- a/src/queries/testId.ts +++ b/src/queries/testId.ts @@ -30,13 +30,13 @@ const queryAllByTestId = ( queryOptions?: ByTestIdOptions ) => Array) => function queryAllByTestIdFn(testId, queryOptions) { - const results = findAll( + return findAll( instance, - (node) => getNodeByTestId(node, testId, queryOptions), + (node) => + typeof node.type === 'string' && + getNodeByTestId(node, testId, queryOptions), queryOptions ); - - return results.filter((element) => typeof element.type === 'string'); }; const getMultipleError = (testId: TextMatch) => diff --git a/src/queries/text.ts b/src/queries/text.ts index ef34333a9..744d88a04 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -1,13 +1,15 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { Text } from 'react-native'; -import { findAll } from '../helpers/findAll'; -import { matchTextContent } from '../helpers/matchers/matchTextContent'; -import { TextMatch, TextMatchOptions } from '../matches'; +import { getConfig } from '../config'; import { getCompositeParentOfType, isHostElementForType, } from '../helpers/component-tree'; -import { getConfig } from '../config'; +import { filterNodeByType } from '../helpers/filterNodeByType'; +import { findAll } from '../helpers/findAll'; +import { getHostComponentNames } from '../helpers/host-component-names'; +import { matchTextContent } from '../helpers/matchers/matchTextContent'; +import { TextMatch, TextMatchOptions } from '../matches'; import { makeQueries } from './makeQueries'; import type { FindAllByQuery, @@ -39,7 +41,8 @@ const queryAllByText = ( const results = findAll( baseInstance, - (node) => matchTextContent(node, text, options), + (node) => + filterNodeByType(node, Text) && matchTextContent(node, text, options), { ...options, matchDeepestOnly: true } ); @@ -47,10 +50,16 @@ const queryAllByText = ( } // vNext version: returns host Text - return findAll(instance, (node) => matchTextContent(node, text, options), { - ...options, - matchDeepestOnly: true, - }); + return findAll( + instance, + (node) => + filterNodeByType(node, getHostComponentNames().text) && + matchTextContent(node, text, options), + { + ...options, + matchDeepestOnly: true, + } + ); }; const getMultipleError = (text: TextMatch) => diff --git a/website/docs/Queries.md b/website/docs/Queries.md index 979038f81..ddacabeac 100644 --- a/website/docs/Queries.md +++ b/website/docs/Queries.md @@ -212,7 +212,9 @@ getByLabelText( ): ReactTestInstance; ``` -Returns a `ReactTestInstance` with matching `accessibilityLabel` prop. +Returns a `ReactTestInstance` with matching label: +- either by matching [`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop +- or by matching text content of view referenced by [`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop ```jsx import { render, screen } from '@testing-library/react-native';