Skip to content

Commit 0af7211

Browse files
feat: check accessibilityLabelledBy in *ByLabelText queries (#1191)
* feat: check accessibilityLabelledBy in *ByLabelText queries * refactor: move getNodeByText to matchTextContent helper * test: improve getByLabelText tests * refactor: extract matchLabelText * refactor: tweaks * refactor: code review changes * refactor: code review changes * chore: rebase on main * chore: fix lint * chore: fix test code in files * chore: fix lint * chore: fix ts error due to missing RN typing Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
1 parent 3b55d99 commit 0af7211

File tree

10 files changed

+121
-47
lines changed

10 files changed

+121
-47
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@
8686
},
8787
"jest": {
8888
"preset": "./jest-preset",
89-
"setupFilesAfterEnv": ["./jest-setup.ts"],
89+
"setupFilesAfterEnv": [
90+
"./jest-setup.ts"
91+
],
9092
"testPathIgnorePatterns": [
9193
"timerUtils",
9294
"examples/"
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import { matches, TextMatch, TextMatchOptions } from '../../matches';
3+
import { findAll } from '../findAll';
4+
import { matchTextContent } from './matchTextContent';
5+
6+
export function matchLabelText(
7+
root: ReactTestInstance,
8+
element: ReactTestInstance,
9+
text: TextMatch,
10+
options: TextMatchOptions = {}
11+
) {
12+
return (
13+
matchAccessibilityLabel(element, text, options) ||
14+
matchAccessibilityLabelledBy(
15+
root,
16+
element.props.accessibilityLabelledBy,
17+
text,
18+
options
19+
)
20+
);
21+
}
22+
23+
function matchAccessibilityLabel(
24+
element: ReactTestInstance,
25+
text: TextMatch,
26+
options: TextMatchOptions
27+
) {
28+
const { exact, normalizer } = options;
29+
return matches(text, element.props.accessibilityLabel, normalizer, exact);
30+
}
31+
32+
function matchAccessibilityLabelledBy(
33+
root: ReactTestInstance,
34+
nativeId: string | undefined,
35+
text: TextMatch,
36+
options: TextMatchOptions
37+
) {
38+
if (!nativeId) {
39+
return false;
40+
}
41+
42+
return (
43+
findAll(
44+
root,
45+
(node) =>
46+
typeof node.type === 'string' &&
47+
node.props.nativeID === nativeId &&
48+
matchTextContent(node, text, options)
49+
).length > 0
50+
);
51+
}

src/helpers/matchers/matchTextContent.ts

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
import { Text } from 'react-native';
21
import type { ReactTestInstance } from 'react-test-renderer';
3-
import { getConfig } from '../../config';
42
import { matches, TextMatch, TextMatchOptions } from '../../matches';
5-
import { filterNodeByType } from '../filterNodeByType';
63
import { getTextContent } from '../getTextContent';
7-
import { getHostComponentNames } from '../host-component-names';
84

5+
/**
6+
* Matches the given node's text content against string or regex matcher.
7+
*
8+
* @param node - Node which text content will be matched
9+
* @param text - The string or regex to match.
10+
* @returns - Whether the node's text content matches the given string or regex.
11+
*/
912
export function matchTextContent(
1013
node: ReactTestInstance,
1114
text: TextMatch,
1215
options: TextMatchOptions = {}
1316
) {
14-
const textType = getConfig().useBreakingChanges
15-
? getHostComponentNames().text
16-
: Text;
17-
if (!filterNodeByType(node, textType)) {
18-
return false;
19-
}
20-
2117
const textContent = getTextContent(node);
2218
const { exact, normalizer } = options;
2319
return matches(text, textContent, normalizer, exact);

src/queries/__tests__/labelText.test.tsx

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { View, Text, TouchableOpacity } from 'react-native';
2+
import { View, Text, TextInput, TouchableOpacity } from 'react-native';
33
import { render } from '../..';
44

55
const BUTTON_LABEL = 'cool button';
@@ -165,3 +165,31 @@ test('byLabelText queries support hidden option', () => {
165165
`"Unable to find an element with accessibilityLabel: hidden"`
166166
);
167167
});
168+
169+
test('getByLabelText supports accessibilityLabelledBy', async () => {
170+
const { getByLabelText, getByTestId } = render(
171+
<>
172+
<Text nativeID="label">Label for input</Text>
173+
{/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */}
174+
<TextInput testID="textInput" accessibilityLabelledBy="label" />
175+
</>
176+
);
177+
178+
expect(getByLabelText('Label for input')).toBe(getByTestId('textInput'));
179+
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
180+
});
181+
182+
test('getByLabelText supports nested accessibilityLabelledBy', async () => {
183+
const { getByLabelText, getByTestId } = render(
184+
<>
185+
<View nativeID="label">
186+
<Text>Label for input</Text>
187+
</View>
188+
{/* @ts-expect-error: waiting for RN 0.71.2 to fix incorrectly omitted `accessibilityLabelledBy` typedef. */}
189+
<TextInput testID="textInput" accessibilityLabelledBy="label" />
190+
</>
191+
);
192+
193+
expect(getByLabelText('Label for input')).toBe(getByTestId('textInput'));
194+
expect(getByLabelText(/input/)).toBe(getByTestId('textInput'));
195+
});

src/queries/__tests__/text.breaking.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,5 @@ test('byText support hidden option', () => {
510510

511511
test('byText should return host component', () => {
512512
const { getByText } = render(<Text>hello</Text>);
513-
514513
expect(getByText('hello').type).toBe('Text');
515514
});

src/queries/__tests__/text.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,5 @@ test('byText support hidden option', () => {
505505

506506
test('byText should return composite Text', () => {
507507
const { getByText } = render(<Text>hello</Text>);
508-
509508
expect(getByText('hello').type).toBe(Text);
510509
});

src/queries/labelText.ts

+6-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { findAll } from '../helpers/findAll';
3-
import { matches, TextMatch, TextMatchOptions } from '../matches';
3+
import { TextMatch, TextMatchOptions } from '../matches';
4+
import { matchLabelText } from '../helpers/matchers/matchLabelText';
45
import { makeQueries } from './makeQueries';
56
import type {
67
FindAllByQuery,
@@ -14,30 +15,17 @@ import { CommonQueryOptions } from './options';
1415

1516
type ByLabelTextOptions = CommonQueryOptions & TextMatchOptions;
1617

17-
const getNodeByLabelText = (
18-
node: ReactTestInstance,
19-
text: TextMatch,
20-
options: TextMatchOptions = {}
21-
) => {
22-
const { exact, normalizer } = options;
23-
return matches(text, node.props.accessibilityLabel, normalizer, exact);
24-
};
25-
26-
const queryAllByLabelText = (
27-
instance: ReactTestInstance
28-
): ((
29-
text: TextMatch,
30-
queryOptions?: ByLabelTextOptions
31-
) => Array<ReactTestInstance>) =>
32-
function queryAllByLabelTextFn(text, queryOptions) {
18+
function queryAllByLabelText(instance: ReactTestInstance) {
19+
return (text: TextMatch, queryOptions?: ByLabelTextOptions) => {
3320
return findAll(
3421
instance,
3522
(node) =>
3623
typeof node.type === 'string' &&
37-
getNodeByLabelText(node, text, queryOptions),
24+
matchLabelText(instance, node, text, queryOptions),
3825
queryOptions
3926
);
4027
};
28+
}
4129

4230
const getMultipleError = (labelText: TextMatch) =>
4331
`Found multiple elements with accessibilityLabel: ${String(labelText)} `;

src/queries/testId.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ const queryAllByTestId = (
3030
queryOptions?: ByTestIdOptions
3131
) => Array<ReactTestInstance>) =>
3232
function queryAllByTestIdFn(testId, queryOptions) {
33-
const results = findAll(
33+
return findAll(
3434
instance,
35-
(node) => getNodeByTestId(node, testId, queryOptions),
35+
(node) =>
36+
typeof node.type === 'string' &&
37+
getNodeByTestId(node, testId, queryOptions),
3638
queryOptions
3739
);
38-
39-
return results.filter((element) => typeof element.type === 'string');
4040
};
4141

4242
const getMultipleError = (testId: TextMatch) =>

src/queries/text.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { Text } from 'react-native';
3-
import { findAll } from '../helpers/findAll';
4-
import { matchTextContent } from '../helpers/matchers/matchTextContent';
5-
import { TextMatch, TextMatchOptions } from '../matches';
3+
import { getConfig } from '../config';
64
import {
75
getCompositeParentOfType,
86
isHostElementForType,
97
} from '../helpers/component-tree';
10-
import { getConfig } from '../config';
8+
import { filterNodeByType } from '../helpers/filterNodeByType';
9+
import { findAll } from '../helpers/findAll';
10+
import { getHostComponentNames } from '../helpers/host-component-names';
11+
import { matchTextContent } from '../helpers/matchers/matchTextContent';
12+
import { TextMatch, TextMatchOptions } from '../matches';
1113
import { makeQueries } from './makeQueries';
1214
import type {
1315
FindAllByQuery,
@@ -39,18 +41,25 @@ const queryAllByText = (
3941

4042
const results = findAll(
4143
baseInstance,
42-
(node) => matchTextContent(node, text, options),
44+
(node) =>
45+
filterNodeByType(node, Text) && matchTextContent(node, text, options),
4346
{ ...options, matchDeepestOnly: true }
4447
);
4548

4649
return results;
4750
}
4851

4952
// vNext version: returns host Text
50-
return findAll(instance, (node) => matchTextContent(node, text, options), {
51-
...options,
52-
matchDeepestOnly: true,
53-
});
53+
return findAll(
54+
instance,
55+
(node) =>
56+
filterNodeByType(node, getHostComponentNames().text) &&
57+
matchTextContent(node, text, options),
58+
{
59+
...options,
60+
matchDeepestOnly: true,
61+
}
62+
);
5463
};
5564

5665
const getMultipleError = (text: TextMatch) =>

website/docs/Queries.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,9 @@ getByLabelText(
212212
): ReactTestInstance;
213213
```
214214

215-
Returns a `ReactTestInstance` with matching `accessibilityLabel` prop.
215+
Returns a `ReactTestInstance` with matching label:
216+
- either by matching [`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop
217+
- or by matching text content of view referenced by [`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop
216218

217219
```jsx
218220
import { render, screen } from '@testing-library/react-native';

0 commit comments

Comments
 (0)