Skip to content

feat: check accessibilityLabelledBy in *ByLabelText queries #1191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@
},
"jest": {
"preset": "./jest-preset",
"setupFilesAfterEnv": ["./jest-setup.ts"],
"setupFilesAfterEnv": [
"./jest-setup.ts"
],
"testPathIgnorePatterns": [
"timerUtils",
"examples/"
Expand Down
51 changes: 51 additions & 0 deletions src/helpers/matchers/matchLabelText.ts
Original file line number Diff line number Diff line change
@@ -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
);
}
18 changes: 7 additions & 11 deletions src/helpers/matchers/matchTextContent.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
30 changes: 29 additions & 1 deletion src/queries/__tests__/labelText.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
<>
<Text nativeID="label">Label for input</Text>
{/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */}
<TextInput testID="textInput" accessibilityLabelledBy="label" />
</>
);

expect(getByLabelText('Label for input')).toBe(getByTestId('textInput'));
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
});

test('getByLabelText supports nested accessibilityLabelledBy', async () => {
const { getByLabelText, getByTestId } = render(
<>
<View nativeID="label">
<Text>Label for input</Text>
</View>
{/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */}
<TextInput testID="textInput" accessibilityLabelledBy="label" />
</>
);

expect(getByLabelText('Label for input')).toBe(getByTestId('textInput'));
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
});
1 change: 0 additions & 1 deletion src/queries/__tests__/text.breaking.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,5 @@ test('byText support hidden option', () => {

test('byText should return host component', () => {
const { getByText } = render(<Text>hello</Text>);

expect(getByText('hello').type).toBe('Text');
});
1 change: 0 additions & 1 deletion src/queries/__tests__/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,5 @@ test('byText support hidden option', () => {

test('byText should return composite Text', () => {
const { getByText } = render(<Text>hello</Text>);

expect(getByText('hello').type).toBe(Text);
});
24 changes: 6 additions & 18 deletions src/queries/labelText.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<ReactTestInstance>) =>
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)} `;
Expand Down
8 changes: 4 additions & 4 deletions src/queries/testId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ const queryAllByTestId = (
queryOptions?: ByTestIdOptions
) => Array<ReactTestInstance>) =>
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) =>
Expand Down
27 changes: 18 additions & 9 deletions src/queries/text.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,18 +41,25 @@ const queryAllByText = (

const results = findAll(
baseInstance,
(node) => matchTextContent(node, text, options),
(node) =>
filterNodeByType(node, Text) && matchTextContent(node, text, options),
{ ...options, matchDeepestOnly: true }
);

return results;
}

// 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) =>
Expand Down
4 changes: 3 additions & 1 deletion website/docs/Queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down