Skip to content

Commit 1782809

Browse files
committed
Allow searching for text in a host component
1 parent 3bce908 commit 1782809

File tree

5 files changed

+104
-22
lines changed

5 files changed

+104
-22
lines changed

src/__tests__/jest-native.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ test('jest-native matchers work correctly', () => {
4040
expect(getByText('Disabled Button')).toBeDisabled();
4141
expect(getByText('Enabled Button')).not.toBeDisabled();
4242

43-
expect(getByA11yHint('Empty Text')).toBeEmpty();
44-
expect(getByA11yHint('Empty View')).toBeEmpty();
45-
expect(getByA11yHint('Not Empty Text')).not.toBeEmpty();
46-
expect(getByA11yHint('Not Empty View')).not.toBeEmpty();
43+
expect(getByA11yHint('Empty Text')).toBeEmptyElement();
44+
expect(getByA11yHint('Empty View')).toBeEmptyElement();
45+
expect(getByA11yHint('Not Empty Text')).not.toBeEmptyElement();
46+
expect(getByA11yHint('Not Empty View')).not.toBeEmptyElement();
4747

4848
expect(getByA11yHint('Container View')).toContainElement(
4949
// $FlowFixMe - TODO: fix @testing-library/jest-native flow typings

src/__tests__/within.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,11 @@ test('within() exposes a11y queries', async () => {
9494
test('getQueriesForElement is alias to within', () => {
9595
expect(getQueriesForElement).toBe(within);
9696
});
97+
98+
test('within allows searching for text within a composite component', () => {
99+
const view = render(<Text testID="subject">Hello</Text>);
100+
// view.getByTestId('subject') returns a composite component, contrary to most queries returning host component
101+
// we want to be sure that this doesn't interfere with the way text is searched
102+
const hostTextQueries = within(view.getByTestId('subject'));
103+
expect(hostTextQueries.getByText('Hello')).toBeTruthy();
104+
});

src/queries/__tests__/role.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,43 @@ describe('supports name option', () => {
134134
'target-button'
135135
);
136136
});
137+
138+
test('returns an element when the direct child is text', () => {
139+
const { getByRole } = render(
140+
<Text accessibilityRole="header" testID="target-header">
141+
About
142+
</Text>
143+
);
144+
145+
// assert on the testId to be sure that the returned element is the one with the accessibilityRole
146+
expect(getByRole('header', { name: 'About' }).props.testID).toBe(
147+
'target-header'
148+
);
149+
});
150+
151+
test('returns an element with nested Text as children', () => {
152+
const { getByRole } = render(
153+
<Text accessibilityRole="header" testID="parent">
154+
<Text testID="child">About</Text>
155+
</Text>
156+
);
157+
158+
// assert on the testId to be sure that the returned element is the one with the accessibilityRole
159+
expect(getByRole('header', { name: 'About' }).props.testID).toBe('parent');
160+
});
161+
162+
test('returns a header with an accessibilityLabel', () => {
163+
const { getByRole } = render(
164+
<Text
165+
accessibilityRole="header"
166+
testID="target-header"
167+
accessibilityLabel="About"
168+
></Text>
169+
);
170+
171+
// assert on the testId to be sure that the returned element is the one with the accessibilityRole
172+
expect(getByRole('header', { name: 'About' }).props.testID).toBe(
173+
'target-header'
174+
);
175+
});
137176
});

src/queries/__tests__/text.test.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Button,
88
TextInput,
99
} from 'react-native';
10-
import { render, getDefaultNormalizer } from '../..';
10+
import { render, getDefaultNormalizer, within } from '../..';
1111

1212
type MyButtonProps = {
1313
children: React.ReactNode;
@@ -454,3 +454,9 @@ test('getByText and queryByText work with tabs', () => {
454454
expect(getByText(textWithTabs)).toBeTruthy();
455455
expect(queryByText(textWithTabs)).toBeTruthy();
456456
});
457+
458+
test('getByText searches for text within itself', () => {
459+
const { getByText } = render(<Text testID="subject">Hello</Text>);
460+
const textNode = within(getByText('Hello'));
461+
expect(textNode.getByText('Hello')).toBeTruthy();
462+
});

src/queries/text.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer';
22
import * as React from 'react';
33
import { createLibraryNotSupportedError } from '../helpers/errors';
44
import { filterNodeByType } from '../helpers/filterNodeByType';
5+
import { isHostElement } from '../helpers/component-tree';
56
import { matches, TextMatch } from '../matches';
67
import type { NormalizerFn } from '../matches';
78
import { makeQueries } from './makeQueries';
@@ -58,37 +59,65 @@ const getChildrenAsText = (
5859
const getNodeByText = (
5960
node: ReactTestInstance,
6061
text: TextMatch,
62+
TextComponent: React.ComponentType,
6163
options: TextMatchOptions = {}
6264
) => {
63-
try {
64-
const { Text } = require('react-native');
65-
const isTextComponent = filterNodeByType(node, Text);
66-
if (isTextComponent) {
67-
const textChildren = getChildrenAsText(node.props.children, Text);
68-
if (textChildren) {
69-
const textToTest = textChildren.join('');
70-
const { exact, normalizer } = options;
71-
return matches(text, textToTest, normalizer, exact);
72-
}
65+
const isTextComponent = filterNodeByType(node, TextComponent);
66+
if (isTextComponent) {
67+
const textChildren = getChildrenAsText(node.props.children, TextComponent);
68+
if (textChildren) {
69+
const textToTest = textChildren.join('');
70+
const { exact, normalizer } = options;
71+
return matches(text, textToTest, normalizer, exact);
7372
}
74-
return false;
75-
} catch (error) {
76-
throw createLibraryNotSupportedError(error);
7773
}
74+
return false;
7875
};
7976

77+
function getCompositeParent(
78+
element: ReactTestInstance,
79+
compositeType: React.ComponentType
80+
) {
81+
if (!isHostElement(element)) return null;
82+
83+
let current = element.parent;
84+
while (!isHostElement(current)) {
85+
// We're at the top of the tree
86+
if (!current) {
87+
return null;
88+
}
89+
90+
if (filterNodeByType(current, compositeType)) {
91+
return current;
92+
}
93+
current = current.parent ?? null;
94+
}
95+
96+
return null;
97+
}
98+
8099
const queryAllByText = (
81100
instance: ReactTestInstance
82101
): ((
83102
text: TextMatch,
84103
options?: TextMatchOptions
85104
) => Array<ReactTestInstance>) =>
86105
function queryAllByTextFn(text, options) {
87-
const results = instance.findAll((node) =>
88-
getNodeByText(node, text, options)
89-
);
106+
try {
107+
const { Text } = require('react-native');
108+
const rootInstance = isHostElement(instance)
109+
? getCompositeParent(instance, Text) ?? instance
110+
: instance;
90111

91-
return results;
112+
if (!rootInstance) return [];
113+
const results = rootInstance.findAll((node) =>
114+
getNodeByText(node, text, Text, options)
115+
);
116+
117+
return results;
118+
} catch (error) {
119+
throw createLibraryNotSupportedError(error);
120+
}
92121
};
93122

94123
const getMultipleError = (text: TextMatch) =>

0 commit comments

Comments
 (0)