Skip to content

Commit 4e06bea

Browse files
author
pierrezimmermann
committed
chore: make queries by text based on the tree
1 parent 1360b73 commit 4e06bea

10 files changed

+170
-107
lines changed

src/__tests__/byText.test.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,21 @@ test('getByText and queryByText work properly with multiple nested fragments', (
473473
expect(getByText('Hello')).toBeTruthy();
474474
expect(queryByText('Hello')).not.toBeNull();
475475
});
476+
477+
const TranslationText = ({
478+
translationKey,
479+
}: {
480+
translationKey: string;
481+
}): React.ReactElement => {
482+
return <>{translationKey}</>;
483+
};
484+
485+
test('it should be abdle to find text nested in components that render as string', () => {
486+
const { getByText } = render(
487+
<Text>
488+
<TranslationText translationKey="hello" />
489+
</Text>
490+
);
491+
492+
expect(getByText('hello')).toBeTruthy();
493+
});

src/helpers/byDisplayValue.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
22
import { matches, TextMatch } from '../matches';
33
import { makeQueries } from './makeQueries';
44
import type { Queries } from './makeQueries';
@@ -28,13 +28,13 @@ const getTextInputNodeByDisplayValue = (
2828
};
2929

3030
const queryAllByDisplayValue = (
31-
instance: ReactTestInstance
31+
renderer: ReactTestRenderer
3232
): ((
3333
displayValue: TextMatch,
3434
queryOptions?: TextMatchOptions
3535
) => Array<ReactTestInstance>) =>
3636
function queryAllByDisplayValueFn(displayValue, queryOptions) {
37-
return instance.findAll((node) =>
37+
return renderer.root.findAll((node) =>
3838
getTextInputNodeByDisplayValue(node, displayValue, queryOptions)
3939
);
4040
};

src/helpers/byPlaceholderText.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
22
import { matches, TextMatch } from '../matches';
33
import { makeQueries } from './makeQueries';
44
import type { Queries } from './makeQueries';
@@ -24,13 +24,13 @@ const getTextInputNodeByPlaceholderText = (
2424
};
2525

2626
const queryAllByPlaceholderText = (
27-
instance: ReactTestInstance
27+
renderer: ReactTestRenderer
2828
): ((
2929
placeholder: TextMatch,
3030
queryOptions?: TextMatchOptions
3131
) => Array<ReactTestInstance>) =>
3232
function queryAllByPlaceholderFn(placeholder, queryOptions) {
33-
return instance.findAll((node) =>
33+
return renderer.root.findAll((node) =>
3434
getTextInputNodeByPlaceholderText(node, placeholder, queryOptions)
3535
);
3636
};

src/helpers/byTestId.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
22
import { matches, TextMatch } from '../matches';
33
import { makeQueries } from './makeQueries';
44
import type { Queries } from './makeQueries';
@@ -14,13 +14,13 @@ const getNodeByTestId = (
1414
};
1515

1616
const queryAllByTestId = (
17-
instance: ReactTestInstance
17+
renderer: ReactTestRenderer
1818
): ((
1919
testId: TextMatch,
2020
queryOptions?: TextMatchOptions
2121
) => Array<ReactTestInstance>) =>
2222
function queryAllByTestIdFn(testId, queryOptions) {
23-
const results = instance
23+
const results = renderer.root
2424
.findAll((node) => getNodeByTestId(node, testId, queryOptions))
2525
.filter((element) => typeof element.type === 'string');
2626

src/helpers/byText.ts

+89-44
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type {
2+
ReactTestInstance,
3+
ReactTestRenderer,
4+
ReactTestRendererTree,
5+
} from 'react-test-renderer';
26
import * as React from 'react';
37
import { matches, TextMatch } from '../matches';
48
import type { NormalizerFn } from '../matches';
59
import { makeQueries } from './makeQueries';
610
import type { Queries } from './makeQueries';
7-
import { filterNodeByType } from './filterNodeByType';
811
import { createLibraryNotSupportedError } from './errors';
912

1013
export type TextMatchOptions = {
@@ -13,75 +16,117 @@ export type TextMatchOptions = {
1316
};
1417

1518
const getChildrenAsText = (
16-
children: React.ReactChild[],
19+
children: ReactTestRendererTree | ReactTestRendererTree[] | null,
1720
TextComponent: React.ComponentType
18-
) => {
19-
const textContent: string[] = [];
20-
React.Children.forEach(children, (child) => {
21-
if (typeof child === 'string') {
22-
textContent.push(child);
23-
return;
24-
}
21+
): string[] => {
22+
if (!children) {
23+
return [];
24+
}
2525

26-
if (typeof child === 'number') {
27-
textContent.push(child.toString());
28-
return;
26+
if (typeof children === 'string') {
27+
return [children];
28+
}
29+
30+
const textContent: string[] = [];
31+
if (!Array.isArray(children)) {
32+
// Bail on traversing text children down the tree if current node (child)
33+
// has no text. In such situations, react-test-renderer will traverse down
34+
// this tree in a separate call and run this query again. As a result, the
35+
// query will match the deepest text node that matches requested text.
36+
if (children.type === 'Text') {
37+
return [];
2938
}
3039

31-
if (child?.props?.children) {
32-
// Bail on traversing text children down the tree if current node (child)
33-
// has no text. In such situations, react-test-renderer will traverse down
34-
// this tree in a separate call and run this query again. As a result, the
35-
// query will match the deepest text node that matches requested text.
36-
if (filterNodeByType(child, TextComponent)) {
37-
return;
38-
}
40+
return getChildrenAsText(children.rendered, TextComponent);
41+
}
3942

40-
if (filterNodeByType(child, React.Fragment)) {
41-
textContent.push(
42-
...getChildrenAsText(child.props.children, TextComponent)
43-
);
44-
}
45-
}
46-
});
43+
children.forEach((child) =>
44+
textContent.push(...getChildrenAsText(child, TextComponent))
45+
);
4746

4847
return textContent;
4948
};
5049

51-
const getNodeByText = (
52-
node: ReactTestInstance,
50+
const getInstancesByText = (
51+
tree: ReactTestRendererTree,
5352
text: TextMatch,
5453
options: TextMatchOptions = {}
55-
) => {
54+
): Omit<ReactTestInstance, 'parent'>[] => {
55+
const instances: Omit<ReactTestInstance, 'parent'>[] = [];
56+
5657
try {
58+
if (!tree.rendered) {
59+
return [];
60+
}
61+
62+
if (!tree.instance) {
63+
if (!Array.isArray(tree.rendered)) {
64+
return [...getInstancesByText(tree.rendered, text, options)];
65+
}
66+
67+
tree.rendered.forEach((rendered) => {
68+
instances.push(...getInstancesByText(rendered, text, options));
69+
});
70+
return instances;
71+
}
72+
73+
const textChildren: string[] = [];
5774
const { Text } = require('react-native');
58-
const isTextComponent = filterNodeByType(node, Text);
59-
if (isTextComponent) {
60-
const textChildren = getChildrenAsText(node.props.children, Text);
61-
if (textChildren) {
62-
const textToTest = textChildren.join('');
63-
const { exact, normalizer } = options;
64-
return matches(text, textToTest, normalizer, exact);
75+
if (!Array.isArray(tree.rendered)) {
76+
if (tree.rendered.type === 'Text') {
77+
textChildren.push(...getChildrenAsText(tree.rendered.rendered, Text));
6578
}
79+
instances.push(...getInstancesByText(tree.rendered, text, options));
80+
} else {
81+
tree.rendered.forEach((child) => {
82+
if (child.type === 'Text') {
83+
textChildren.push(...getChildrenAsText(child, Text));
84+
} else {
85+
instances.push(...getInstancesByText(child, text, options));
86+
}
87+
});
6688
}
67-
return false;
89+
90+
if (textChildren.length) {
91+
const textToTest = textChildren.join('');
92+
const { exact, normalizer } = options;
93+
if (matches(text, textToTest, normalizer, exact)) {
94+
instances.push(tree.instance);
95+
}
96+
}
97+
98+
return instances;
6899
} catch (error) {
69100
throw createLibraryNotSupportedError(error);
70101
}
71102
};
72103

73104
const queryAllByText = (
74-
instance: ReactTestInstance
105+
renderer: ReactTestRenderer
75106
): ((
76107
text: TextMatch,
77108
queryOptions?: TextMatchOptions
78109
) => Array<ReactTestInstance>) =>
79110
function queryAllByTextFn(text, queryOptions) {
80-
const results = instance.findAll((node) =>
81-
getNodeByText(node, text, queryOptions)
82-
);
111+
const tree = renderer.toTree();
112+
const treeInstance = renderer.root;
113+
114+
if (!tree) {
115+
return [];
116+
}
117+
118+
const instancesFound = getInstancesByText(tree, text, queryOptions);
83119

84-
return results;
120+
// Instances in the tree are not of type ReactTestInstance because they do not have parent
121+
// This is problematic when firing events so we find in the root nodes the one that matches
122+
return instancesFound.map((instanceFound) => {
123+
return treeInstance.find((treeInstance) => {
124+
return (
125+
treeInstance.instance &&
126+
treeInstance.instance._reactInternals.stateNode === instanceFound
127+
);
128+
});
129+
});
85130
};
86131

87132
const getMultipleError = (text: TextMatch) =>

src/helpers/findByAPI.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
22
import type { WaitForOptions } from '../waitFor';
33
import type { TextMatch } from '../matches';
44
import type { TextMatchOptions } from './byText';
@@ -56,15 +56,15 @@ export type FindByAPI = {
5656
) => Promise<ReactTestInstance>;
5757
};
5858

59-
export const findByAPI = (instance: ReactTestInstance): FindByAPI => ({
60-
findByTestId: findByTestId(instance),
61-
findByText: findByText(instance),
62-
findByPlaceholderText: findByPlaceholderText(instance),
63-
findByDisplayValue: findByDisplayValue(instance),
64-
findAllByTestId: findAllByTestId(instance),
65-
findAllByText: findAllByText(instance),
66-
findAllByPlaceholderText: findAllByPlaceholderText(instance),
67-
findAllByDisplayValue: findAllByDisplayValue(instance),
59+
export const findByAPI = (renderer: ReactTestRenderer): FindByAPI => ({
60+
findByTestId: findByTestId(renderer),
61+
findByText: findByText(renderer),
62+
findByPlaceholderText: findByPlaceholderText(renderer),
63+
findByDisplayValue: findByDisplayValue(renderer),
64+
findAllByTestId: findAllByTestId(renderer),
65+
findAllByText: findAllByText(renderer),
66+
findAllByPlaceholderText: findAllByPlaceholderText(renderer),
67+
findAllByDisplayValue: findAllByDisplayValue(renderer),
6868

6969
// Renamed
7070
findByPlaceholder: () =>

src/helpers/getByAPI.ts

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactTestInstance } from 'react-test-renderer';
1+
import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer';
22
import * as React from 'react';
33
import prettyFormat from 'pretty-format';
44
import type { TextMatch } from '../matches';
@@ -104,19 +104,19 @@ export const UNSAFE_getAllByProps = (
104104
return results;
105105
};
106106

107-
export const getByAPI = (instance: ReactTestInstance): GetByAPI => ({
108-
getByText: getByText(instance),
109-
getByPlaceholderText: getByPlaceholderText(instance),
110-
getByDisplayValue: getByDisplayValue(instance),
111-
getByTestId: getByTestId(instance),
112-
getAllByText: getAllByText(instance),
113-
getAllByPlaceholderText: getAllByPlaceholderText(instance),
114-
getAllByDisplayValue: getAllByDisplayValue(instance),
115-
getAllByTestId: getAllByTestId(instance),
107+
export const getByAPI = (renderer: ReactTestRenderer): GetByAPI => ({
108+
getByText: getByText(renderer),
109+
getByPlaceholderText: getByPlaceholderText(renderer),
110+
getByDisplayValue: getByDisplayValue(renderer),
111+
getByTestId: getByTestId(renderer),
112+
getAllByText: getAllByText(renderer),
113+
getAllByPlaceholderText: getAllByPlaceholderText(renderer),
114+
getAllByDisplayValue: getAllByDisplayValue(renderer),
115+
getAllByTestId: getAllByTestId(renderer),
116116

117117
// Unsafe
118-
UNSAFE_getByType: UNSAFE_getByType(instance),
119-
UNSAFE_getAllByType: UNSAFE_getAllByType(instance),
120-
UNSAFE_getByProps: UNSAFE_getByProps(instance),
121-
UNSAFE_getAllByProps: UNSAFE_getAllByProps(instance),
118+
UNSAFE_getByType: UNSAFE_getByType(renderer.root),
119+
UNSAFE_getAllByType: UNSAFE_getAllByType(renderer.root),
120+
UNSAFE_getByProps: UNSAFE_getByProps(renderer.root),
121+
UNSAFE_getAllByProps: UNSAFE_getAllByProps(renderer.root),
122122
});

0 commit comments

Comments
 (0)