Skip to content

Commit 235942b

Browse files
authored
fix(breaking): correct faulty implementation of finding by name (#62)
* fix(breaking): correct faulty implementation of finding by name * chore: update docs * refactor: code review
1 parent 5b249c3 commit 235942b

File tree

11 files changed

+211
-181
lines changed

11 files changed

+211
-181
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ This library is a replacement for [Enzyme](http://airbnb.io/enzyme/).
3030
```jsx
3131
import { render, fireEvent } from 'react-native-testing-library';
3232
import { QuestionsBoard } from '../QuestionsBoard';
33+
import { Question } from '../Question';
3334

3435
function setAnswer(question, answer) {
3536
fireEvent.changeText(question, answer);
3637
}
3738

3839
test('should verify two questions', () => {
3940
const { getAllByName, getByText } = render(<QuestionsBoard {...props} />);
40-
const allQuestions = getAllByName('Question');
41+
const allQuestions = getAllByName(Question);
4142

4243
setAnswer(allQuestions[0], 'a1');
4344
setAnswer(allQuestions[1], 'a2');
@@ -84,7 +85,7 @@ Deeply render given React element and returns helpers to query the output.
8485
```jsx
8586
import { render } from 'react-native-testing-library';
8687

87-
const { getByTestId, getByName /*...*/ } = render(<Component />);
88+
const { getByTestId, getByText /*...*/ } = render(<Component />);
8889
```
8990

9091
Returns a `RenderResult` object with following properties:
@@ -104,13 +105,13 @@ type ReactTestInstance = {
104105
};
105106
```
106107

107-
### `getByName: (name: string | React.ComponentType<*>)`
108+
### `getByName: (name: React.ComponentType<*>)`
108109

109-
A method returning a `ReactTestInstance` with matching name – may be a string or React Element. Throws when no matches.
110+
A method returning a `ReactTestInstance` with matching a React component type. Throws when no matches.
110111

111-
### `getAllByName: (name: string | React.ComponentType<*>)`
112+
### `getAllByName: (name: React.ComponentType<*>)`
112113

113-
A method returning an array of `ReactTestInstance`s with matching name – may be a string or React Element.
114+
A method returning an array of `ReactTestInstance`s with matching a React component type.
114115

115116
### `getByText: (text: string | RegExp)`
116117

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"jest": "^23.6.0",
2525
"metro-react-native-babel-preset": "^0.49.0",
2626
"react": "16.6.1",
27-
"react-native": "^0.57.3",
27+
"react-native": "^0.57.5",
2828
"react-test-renderer": "16.6.1",
2929
"release-it": "^8.0.0",
3030
"strip-ansi": "^5.0.0",

src/__tests__/__snapshots__/shallow.test.js.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ exports[`shallow rendering React Test Instance 1`] = `
1313
`;
1414

1515
exports[`shallow rendering React elements 1`] = `
16-
<Component>
17-
<Component
16+
<View>
17+
<Text
1818
testID="text-button"
1919
>
2020
Press me
21-
</Component>
22-
</Component>
21+
</Text>
22+
</View>
2323
`;

src/__tests__/render.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ test('getByTestId, queryByTestId', () => {
5757
const component = getByTestId('bananaFresh');
5858

5959
expect(component.props.children).toBe('not fresh');
60-
expect(() => getByTestId('InExistent')).toThrow();
60+
expect(() => getByTestId('InExistent')).toThrow('No instances found');
6161

6262
expect(getByTestId('bananaFresh')).toBe(component);
6363
expect(queryByTestId('InExistent')).toBeNull();
@@ -76,7 +76,8 @@ test('getByName, queryByName', () => {
7676
sameButton.props.onPress();
7777

7878
expect(bananaFresh.props.children).toBe('not fresh');
79-
expect(() => getByName('InExistent')).toThrow();
79+
expect(() => getByName('InExistent')).toThrow('No instances found');
80+
expect(() => getByName(Text)).toThrow('Expected 1 but found 3');
8081

8182
expect(queryByName('Button')).toBe(button);
8283
expect(queryByName('InExistent')).toBeNull();
@@ -89,7 +90,7 @@ test('getAllByName, queryAllByName', () => {
8990
expect(text.props.children).toBe('Is the banana fresh?');
9091
expect(status.props.children).toBe('not fresh');
9192
expect(button.props.children).toBe('Change freshness!');
92-
expect(() => getAllByName('InExistent')).toThrow();
93+
expect(() => getAllByName('InExistent')).toThrow('No instances found');
9394

9495
expect(queryAllByName('Text')[1]).toBe(status);
9596
expect(queryAllByName('InExistent')).toHaveLength(0);
@@ -104,7 +105,7 @@ test('getByText, queryByText', () => {
104105
const sameButton = getByText('not fresh');
105106

106107
expect(sameButton.props.children).toBe('not fresh');
107-
expect(() => getByText('InExistent')).toThrow();
108+
expect(() => getByText('InExistent')).toThrow('No instances found');
108109

109110
expect(queryByText(/change/i)).toBe(button);
110111
expect(queryByText('InExistent')).toBeNull();
@@ -115,7 +116,7 @@ test('getAllByText, queryAllByText', () => {
115116
const buttons = getAllByText(/fresh/i);
116117

117118
expect(buttons).toHaveLength(3);
118-
expect(() => getAllByText('InExistent')).toThrow();
119+
expect(() => getAllByText('InExistent')).toThrow('No instances found');
119120

120121
expect(queryAllByText(/fresh/i)).toEqual(buttons);
121122
expect(queryAllByText('InExistent')).toHaveLength(0);
@@ -126,7 +127,9 @@ test('getByProps, queryByProps', () => {
126127
const primaryType = getByProps({ type: 'primary' });
127128

128129
expect(primaryType.props.children).toBe('Change freshness!');
129-
expect(() => getByProps({ type: 'inexistent' })).toThrow();
130+
expect(() => getByProps({ type: 'inexistent' })).toThrow(
131+
'No instances found'
132+
);
130133

131134
expect(queryByProps({ type: 'primary' })).toBe(primaryType);
132135
expect(queryByProps({ type: 'inexistent' })).toBeNull();
@@ -137,7 +140,9 @@ test('getAllByProp, queryAllByProps', () => {
137140
const primaryTypes = getAllByProps({ type: 'primary' });
138141

139142
expect(primaryTypes).toHaveLength(1);
140-
expect(() => getAllByProps({ type: 'inexistent' })).toThrow();
143+
expect(() => getAllByProps({ type: 'inexistent' })).toThrow(
144+
'No instances found'
145+
);
141146

142147
expect(queryAllByProps({ type: 'primary' })).toEqual(primaryTypes);
143148
expect(queryAllByProps({ type: 'inexistent' })).toHaveLength(0);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// @flow
2+
import React from 'react';
3+
import {
4+
Image,
5+
Text,
6+
TextInput,
7+
Modal,
8+
View,
9+
ScrollView,
10+
ActivityIndicator,
11+
TouchableOpacity,
12+
} from 'react-native';
13+
14+
import { render } from '..';
15+
16+
class RNComponents extends React.Component<*> {
17+
render() {
18+
return (
19+
<View>
20+
<Modal visible>
21+
<ScrollView>
22+
<Image />
23+
<TextInput value="value" />
24+
<TouchableOpacity onPress={() => {}}>
25+
<Text>t1</Text>
26+
<Text>t2</Text>
27+
<Text>t3</Text>
28+
</TouchableOpacity>
29+
<ActivityIndicator show />
30+
</ScrollView>
31+
</Modal>
32+
</View>
33+
);
34+
}
35+
}
36+
37+
test('getByName smoke test to see how unstable it gets', () => {
38+
const { getByName } = render(<RNComponents />);
39+
expect(() => getByName('Image')).toThrow(); // – doesn't have displayName set properly
40+
getByName('TextInput');
41+
expect(() => getByName('Modal')).toThrow(); // – doesn't have displayName set properly
42+
getByName('View');
43+
getByName('ScrollView');
44+
expect(() => getByName('ActivityIndicator')).toThrow(); // – doesn't have displayName set properly
45+
getByName('TouchableOpacity');
46+
});
47+
48+
test('getAllByName smoke test to see how unstable it gets', () => {
49+
const { getAllByName } = render(<RNComponents />);
50+
51+
const [t1, t2, t3] = getAllByName('Text');
52+
expect(t1.props.children).toBe('t1');
53+
expect(t2.props.children).toBe('t2');
54+
expect(t3.props.children).toBe('t3');
55+
});

src/debug.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
/* eslint-disable no-console */
33
import * as React from 'react';
4-
import prettyFormat, { plugins } from 'pretty-format'; // eslint-disable-line import/no-extraneous-dependencies
4+
import prettyFormat, { plugins } from 'pretty-format';
55
import shallow from './shallow';
66
import render from './render';
77

src/fireEvent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @flow
2-
import ErrorWithStack from './helpers/errorWithStack';
2+
import { ErrorWithStack } from './helpers/errors';
33

44
const findEventHandler = (element: ReactTestInstance, eventName: string) => {
55
const eventHandler = toEventHandlerName(eventName);

src/helpers/errorWithStack.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/helpers/errors.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @flow
2+
export class ErrorWithStack extends Error {
3+
constructor(message: ?string, callsite: Function) {
4+
super(message);
5+
if (Error.captureStackTrace) {
6+
Error.captureStackTrace(this, callsite);
7+
}
8+
}
9+
}
10+
11+
export const createLibraryNotSupportedError = (error: Error) =>
12+
new Error(
13+
`Currently the only supported library to search by text is "react-native".\n\n${
14+
error.message
15+
}`
16+
);

src/helpers/getByAPI.js

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
// @flow
22
import * as React from 'react';
3-
import ErrorWithStack from './errorWithStack';
3+
import prettyFormat from 'pretty-format';
4+
import { ErrorWithStack, createLibraryNotSupportedError } from './errors';
45

5-
const getNodeByName = (node, name) =>
6-
node.type.name === name ||
7-
node.type.displayName === name ||
8-
node.type === name;
6+
const filterNodeByType = (node, type) => node.type === type;
97

10-
const getNodeByText = (node, text) =>
11-
(getNodeByName(node, 'Text') || getNodeByName(node, 'TextInput')) &&
12-
(typeof text === 'string'
13-
? text === node.props.children
14-
: text.test(node.props.children));
8+
const filterNodeByName = (node, name) =>
9+
typeof node.type !== 'string' &&
10+
(node.type.displayName === name || node.type.name === name);
1511

12+
const getNodeByText = (node, text) => {
13+
try {
14+
// eslint-disable-next-line
15+
const { Text, TextInput } = require('react-native');
16+
return (
17+
(filterNodeByType(node, Text) || filterNodeByType(node, TextInput)) &&
18+
(typeof text === 'string'
19+
? text === node.props.children
20+
: text.test(node.props.children))
21+
);
22+
} catch (error) {
23+
throw createLibraryNotSupportedError(error);
24+
}
25+
};
26+
27+
const prepareErrorMessage = error =>
28+
// Strip info about custom predicate
29+
error.message.replace(/ matching custom predicate[^]*/gm, '');
30+
31+
// TODO: deprecate getByName(string | type) in favor of getByType(type)
1632
export const getByName = (instance: ReactTestInstance) =>
1733
function getByNameFn(name: string | React.ComponentType<*>) {
1834
try {
19-
return instance.find(node => getNodeByName(node, name));
35+
return typeof name === 'string'
36+
? instance.find(node => filterNodeByName(node, name))
37+
: instance.findByType(name);
2038
} catch (error) {
21-
throw new ErrorWithStack(`Component not found.`, getByNameFn);
39+
throw new ErrorWithStack(prepareErrorMessage(error), getByNameFn);
2240
}
2341
};
2442

@@ -27,7 +45,7 @@ export const getByText = (instance: ReactTestInstance) =>
2745
try {
2846
return instance.find(node => getNodeByText(node, text));
2947
} catch (error) {
30-
throw new ErrorWithStack(`Component not found.`, getByTextFn);
48+
throw new ErrorWithStack(prepareErrorMessage(error), getByTextFn);
3149
}
3250
};
3351

@@ -36,7 +54,7 @@ export const getByProps = (instance: ReactTestInstance) =>
3654
try {
3755
return instance.findByProps(props);
3856
} catch (error) {
39-
throw new ErrorWithStack(`Component not found.`, getByPropsFn);
57+
throw new ErrorWithStack(prepareErrorMessage(error), getByPropsFn);
4058
}
4159
};
4260

@@ -45,15 +63,19 @@ export const getByTestId = (instance: ReactTestInstance) =>
4563
try {
4664
return instance.findByProps({ testID });
4765
} catch (error) {
48-
throw new ErrorWithStack(`Component not found.`, getByTestIdFn);
66+
throw new ErrorWithStack(prepareErrorMessage(error), getByTestIdFn);
4967
}
5068
};
5169

70+
// TODO: deprecate getAllByName(string | type) in favor of getAllByType(type)
5271
export const getAllByName = (instance: ReactTestInstance) =>
5372
function getAllByNameFn(name: string | React.ComponentType<*>) {
54-
const results = instance.findAll(node => getNodeByName(node, name));
73+
const results =
74+
typeof name === 'string'
75+
? instance.findAll(node => filterNodeByName(node, name))
76+
: instance.findAllByType(name);
5577
if (results.length === 0) {
56-
throw new ErrorWithStack(`Components not found.`, getAllByNameFn);
78+
throw new ErrorWithStack('No instances found', getAllByNameFn);
5779
}
5880
return results;
5981
};
@@ -62,7 +84,10 @@ export const getAllByText = (instance: ReactTestInstance) =>
6284
function getAllByTextFn(text: string | RegExp) {
6385
const results = instance.findAll(node => getNodeByText(node, text));
6486
if (results.length === 0) {
65-
throw new ErrorWithStack(`Components not found.`, getAllByTextFn);
87+
throw new ErrorWithStack(
88+
`No instances found with text: ${String(text)}`,
89+
getAllByTextFn
90+
);
6691
}
6792
return results;
6893
};
@@ -71,7 +96,10 @@ export const getAllByProps = (instance: ReactTestInstance) =>
7196
function getAllByPropsFn(props: { [propName: string]: any }) {
7297
const results = instance.findAllByProps(props);
7398
if (results.length === 0) {
74-
throw new ErrorWithStack(`Components not found.`, getAllByPropsFn);
99+
throw new ErrorWithStack(
100+
`No instances found with props:\n${prettyFormat(props)}`,
101+
getAllByPropsFn
102+
);
75103
}
76104
return results;
77105
};

0 commit comments

Comments
 (0)