Skip to content

Commit ee888f1

Browse files
danahartwegthymikee
authored andcommitted
feat: add getByPlaceholder query functionality (#101)
### Summary There is a helper that exists in `react-testing-library` to query inputs by their `placeholder`. This PR adds that support. You **could** achieve this by querying for a `placeholder` prop, or querying for react native `TextInput`s manually and then checking their props, but it felt cleaner to have this contained in the testing library itself. It will also allow you to bypass any other instances that may have a `placeholder` prop that aren't `TextInput`s. If we don't want to expand the surface of the library further, I completely understand that and am fine with this being closed. Just seems like it may be useful and save some time for several scenarios =) ### Test plan + Render a `TextInput` with a `placeholder` prop + Query the test instance via `getByPlaceholder` for the input + Verify the test instance is found
1 parent 5145a70 commit ee888f1

File tree

7 files changed

+185
-1
lines changed

7 files changed

+185
-1
lines changed

docs/API.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ A method returning a `ReactTestInstance` with matching text – may be a string
5252

5353
A method returning an array of `ReactTestInstance`s with matching text – may be a string or regular expression.
5454

55+
### `getByPlaceholder: (placeholder: string | RegExp)`
56+
57+
A method returning a `ReactTestInstance` for a `TextInput` with a matching placeholder – may be a string or regular expression. Throws when no matches.
58+
59+
### `getAllByPlaceholder: (placeholder: string | RegExp)`
60+
61+
A method returning an array of `ReactTestInstance`s for `TextInput`'s with a matching placeholder – may be a string or regular expression.
62+
5563
### `getByProps: (props: { [propName: string]: any })`
5664

5765
A method returning a `ReactTestInstance` with matching props object. Throws when no matches.

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ exports[`debug 1`] = `
1010
>
1111
not fresh
1212
</Text>
13+
<TextInput
14+
allowFontScaling={true}
15+
placeholder=\\"Add custom freshness\\"
16+
testID=\\"bananaCustomFreshness\\"
17+
underlineColorAndroid=\\"transparent\\"
18+
/>
19+
<TextInput
20+
allowFontScaling={true}
21+
placeholder=\\"Who inspected freshness?\\"
22+
testID=\\"bananaChef\\"
23+
underlineColorAndroid=\\"transparent\\"
24+
/>
1325
<View
1426
accessible={true}
1527
isTVSelectable={true}
@@ -42,6 +54,18 @@ exports[`debug changing component: bananaFresh button message should now be "fre
4254
>
4355
fresh
4456
</Text>
57+
<TextInput
58+
allowFontScaling={true}
59+
placeholder=\\"Add custom freshness\\"
60+
testID=\\"bananaCustomFreshness\\"
61+
underlineColorAndroid=\\"transparent\\"
62+
/>
63+
<TextInput
64+
allowFontScaling={true}
65+
placeholder=\\"Who inspected freshness?\\"
66+
testID=\\"bananaChef\\"
67+
underlineColorAndroid=\\"transparent\\"
68+
/>
4569
<View
4670
accessible={true}
4771
isTVSelectable={true}
@@ -74,6 +98,18 @@ exports[`debug: shallow 1`] = `
7498
>
7599
not fresh
76100
</Text>
101+
<TextInput
102+
allowFontScaling={true}
103+
placeholder=\\"Add custom freshness\\"
104+
testID=\\"bananaCustomFreshness\\"
105+
underlineColorAndroid=\\"transparent\\"
106+
/>
107+
<TextInput
108+
allowFontScaling={true}
109+
placeholder=\\"Who inspected freshness?\\"
110+
testID=\\"bananaChef\\"
111+
underlineColorAndroid=\\"transparent\\"
112+
/>
77113
<Button
78114
onPress={[Function anonymous]}
79115
type=\\"primary\\"
@@ -95,6 +131,18 @@ exports[`debug: shallow with message 1`] = `
95131
>
96132
not fresh
97133
</Text>
134+
<TextInput
135+
allowFontScaling={true}
136+
placeholder=\\"Add custom freshness\\"
137+
testID=\\"bananaCustomFreshness\\"
138+
underlineColorAndroid=\\"transparent\\"
139+
/>
140+
<TextInput
141+
allowFontScaling={true}
142+
placeholder=\\"Who inspected freshness?\\"
143+
testID=\\"bananaChef\\"
144+
underlineColorAndroid=\\"transparent\\"
145+
/>
98146
<Button
99147
onPress={[Function anonymous]}
100148
type=\\"primary\\"
@@ -116,6 +164,18 @@ exports[`debug: with message 1`] = `
116164
>
117165
not fresh
118166
</Text>
167+
<TextInput
168+
allowFontScaling={true}
169+
placeholder=\\"Add custom freshness\\"
170+
testID=\\"bananaCustomFreshness\\"
171+
underlineColorAndroid=\\"transparent\\"
172+
/>
173+
<TextInput
174+
allowFontScaling={true}
175+
placeholder=\\"Who inspected freshness?\\"
176+
testID=\\"bananaChef\\"
177+
underlineColorAndroid=\\"transparent\\"
178+
/>
119179
<View
120180
accessible={true}
121181
isTVSelectable={true}

src/__tests__/render.test.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// @flow
22
/* eslint-disable react/no-multi-comp */
33
import React from 'react';
4-
import { View, Text, TouchableOpacity } from 'react-native';
4+
import { View, Text, TextInput, TouchableOpacity } from 'react-native';
55
import stripAnsi from 'strip-ansi';
66
import { render, fireEvent } from '..';
77

8+
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
9+
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
10+
811
class Button extends React.Component<*> {
912
render() {
1013
return (
@@ -45,6 +48,11 @@ class Banana extends React.Component<*, *> {
4548
<Text testID="bananaFresh">
4649
{this.state.fresh ? 'fresh' : 'not fresh'}
4750
</Text>
51+
<TextInput
52+
testID="bananaCustomFreshness"
53+
placeholder={PLACEHOLDER_FRESHNESS}
54+
/>
55+
<TextInput testID="bananaChef" placeholder={PLACEHOLDER_CHEF} />
4856
<Button onPress={this.changeFresh} type="primary">
4957
Change freshness!
5058
</Button>
@@ -138,6 +146,37 @@ test('getAllByText, queryAllByText', () => {
138146
expect(queryAllByText('InExistent')).toHaveLength(0);
139147
});
140148

149+
test('getByPlaceholder, queryByPlaceholder', () => {
150+
const { getByPlaceholder, queryByPlaceholder } = render(<Banana />);
151+
const input = getByPlaceholder(/custom/i);
152+
153+
expect(input.props.placeholder).toBe(PLACEHOLDER_FRESHNESS);
154+
155+
const sameInput = getByPlaceholder(PLACEHOLDER_FRESHNESS);
156+
157+
expect(sameInput.props.placeholder).toBe(PLACEHOLDER_FRESHNESS);
158+
expect(() => getByPlaceholder('no placeholder')).toThrow(
159+
'No instances found'
160+
);
161+
162+
expect(queryByPlaceholder(/add/i)).toBe(input);
163+
expect(queryByPlaceholder('no placeholder')).toBeNull();
164+
expect(() => queryByPlaceholder(/fresh/)).toThrow('Expected 1 but found 2');
165+
});
166+
167+
test('getAllByPlaceholder, queryAllByPlaceholder', () => {
168+
const { getAllByPlaceholder, queryAllByPlaceholder } = render(<Banana />);
169+
const inputs = getAllByPlaceholder(/fresh/i);
170+
171+
expect(inputs).toHaveLength(2);
172+
expect(() => getAllByPlaceholder('no placeholder')).toThrow(
173+
'No instances found'
174+
);
175+
176+
expect(queryAllByPlaceholder(/fresh/i)).toEqual(inputs);
177+
expect(queryAllByPlaceholder('no placeholder')).toHaveLength(0);
178+
});
179+
141180
test('getByProps, queryByProps', () => {
142181
const { getByProps, queryByProps } = render(<Banana />);
143182
const primaryType = getByProps({ type: 'primary' });

src/helpers/getByAPI.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ const getNodeByText = (node, text) => {
2828
}
2929
};
3030

31+
const getTextInputNodeByPlaceholder = (node, placeholder) => {
32+
try {
33+
// eslint-disable-next-line
34+
const { TextInput } = require('react-native');
35+
return (
36+
filterNodeByType(node, TextInput) &&
37+
(typeof placeholder === 'string'
38+
? placeholder === node.props.placeholder
39+
: placeholder.test(node.props.placeholder))
40+
);
41+
} catch (error) {
42+
throw createLibraryNotSupportedError(error);
43+
}
44+
};
45+
3146
const prepareErrorMessage = error =>
3247
// Strip info about custom predicate
3348
error.message.replace(/ matching custom predicate[^]*/gm, '');
@@ -62,6 +77,17 @@ export const getByText = (instance: ReactTestInstance) =>
6277
}
6378
};
6479

80+
export const getByPlaceholder = (instance: ReactTestInstance) =>
81+
function getByPlaceholderFn(placeholder: string | RegExp) {
82+
try {
83+
return instance.find(node =>
84+
getTextInputNodeByPlaceholder(node, placeholder)
85+
);
86+
} catch (error) {
87+
throw new ErrorWithStack(prepareErrorMessage(error), getByPlaceholderFn);
88+
}
89+
};
90+
6591
export const getByProps = (instance: ReactTestInstance) =>
6692
function getByPropsFn(props: { [propName: string]: any }) {
6793
try {
@@ -114,6 +140,20 @@ export const getAllByText = (instance: ReactTestInstance) =>
114140
return results;
115141
};
116142

143+
export const getAllByPlaceholder = (instance: ReactTestInstance) =>
144+
function getAllByPlaceholderFn(placeholder: string | RegExp) {
145+
const results = instance.findAll(node =>
146+
getTextInputNodeByPlaceholder(node, placeholder)
147+
);
148+
if (results.length === 0) {
149+
throw new ErrorWithStack(
150+
`No instances found with placeholder: ${String(placeholder)}`,
151+
getAllByPlaceholderFn
152+
);
153+
}
154+
return results;
155+
};
156+
117157
export const getAllByProps = (instance: ReactTestInstance) =>
118158
function getAllByPropsFn(props: { [propName: string]: any }) {
119159
const results = instance.findAllByProps(props);
@@ -131,9 +171,11 @@ export const getByAPI = (instance: ReactTestInstance) => ({
131171
getByName: getByName(instance),
132172
getByType: getByType(instance),
133173
getByText: getByText(instance),
174+
getByPlaceholder: getByPlaceholder(instance),
134175
getByProps: getByProps(instance),
135176
getAllByName: getAllByName(instance),
136177
getAllByType: getAllByType(instance),
137178
getAllByText: getAllByText(instance),
179+
getAllByPlaceholder: getAllByPlaceholder(instance),
138180
getAllByProps: getAllByProps(instance),
139181
});

src/helpers/queryByAPI.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {
55
getByName,
66
getByType,
77
getByText,
8+
getByPlaceholder,
89
getByProps,
910
getAllByName,
1011
getAllByType,
1112
getAllByText,
13+
getAllByPlaceholder,
1214
getAllByProps,
1315
} from './getByAPI';
1416
import { ErrorWithStack, logDeprecationWarning } from './errors';
@@ -48,6 +50,15 @@ export const queryByText = (instance: ReactTestInstance) =>
4850
}
4951
};
5052

53+
export const queryByPlaceholder = (instance: ReactTestInstance) =>
54+
function queryByPlaceholderFn(placeholder: string | RegExp) {
55+
try {
56+
return getByPlaceholder(instance)(placeholder);
57+
} catch (error) {
58+
return createQueryByError(error, queryByPlaceholder);
59+
}
60+
};
61+
5162
export const queryByProps = (instance: ReactTestInstance) =>
5263
function queryByPropsFn(props: { [propName: string]: any }) {
5364
try {
@@ -97,6 +108,16 @@ export const queryAllByText = (instance: ReactTestInstance) => (
97108
}
98109
};
99110

111+
export const queryAllByPlaceholder = (instance: ReactTestInstance) => (
112+
placeholder: string | RegExp
113+
) => {
114+
try {
115+
return getAllByPlaceholder(instance)(placeholder);
116+
} catch (error) {
117+
return [];
118+
}
119+
};
120+
100121
export const queryAllByProps = (instance: ReactTestInstance) => (props: {
101122
[propName: string]: any,
102123
}) => {
@@ -112,9 +133,11 @@ export const queryByAPI = (instance: ReactTestInstance) => ({
112133
queryByName: queryByName(instance),
113134
queryByType: queryByType(instance),
114135
queryByText: queryByText(instance),
136+
queryByPlaceholder: queryByPlaceholder(instance),
115137
queryByProps: queryByProps(instance),
116138
queryAllByName: queryAllByName(instance),
117139
queryAllByType: queryAllByType(instance),
118140
queryAllByText: queryAllByText(instance),
141+
queryAllByPlaceholder: queryAllByPlaceholder(instance),
119142
queryAllByProps: queryAllByProps(instance),
120143
});

typings/__tests__/index.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ interface HasRequiredProp {
1515

1616
const View = props => props.children;
1717
const Text = props => props.children;
18+
const TextInput = props => props.children;
1819
const ElementWithRequiredProps = (props: HasRequiredProp) => (
1920
<Text>{props.requiredProp}</Text>
2021
);
2122

2223
const TestComponent = () => (
2324
<View>
2425
<Text>Test component</Text>
26+
<TextInput placeholder="my placeholder" />
2527
</View>
2628
);
2729

@@ -36,6 +38,8 @@ const getByTypeWithRequiredProps: ReactTestInstance = tree.getByType(
3638
);
3739
const getByTextString: ReactTestInstance = tree.getByText('<View />');
3840
const getByTextRegExp: ReactTestInstance = tree.getByText(/View/g);
41+
const getByPlaceholderString: ReactTestInstance = tree.getByPlaceholder('my placeholder');
42+
const getByPlaceholderRegExp: ReactTestInstance = tree.getByPlaceholder(/placeholder/g);
3943
const getByProps: ReactTestInstance = tree.getByProps({ value: 2 });
4044
const getByTestId: ReactTestInstance = tree.getByTestId('test-id');
4145
const getAllByNameString: Array<ReactTestInstance> = tree.getAllByName('View');
@@ -65,6 +69,10 @@ const queryByTextString: ReactTestInstance | null = tree.queryByText(
6569
'<View />'
6670
);
6771
const queryByTextRegExp: ReactTestInstance | null = tree.queryByText(/View/g);
72+
const queryByPlaceholderString: ReactTestInstance | null = tree.queryByText(
73+
'my placeholder'
74+
);
75+
const queryByPlaceholderRegExp: ReactTestInstance | null = tree.queryByText(/placeholder/g);
6876
const queryByProps: ReactTestInstance | null = tree.queryByProps({ value: 2 });
6977
const queryByTestId: ReactTestInstance | null = tree.queryByTestId('test-id');
7078
const queryAllByNameString: Array<ReactTestInstance> = tree.getAllByName(

typings/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,29 @@ export interface GetByAPI {
55
getByName: (name: React.ReactType | string) => ReactTestInstance;
66
getByType: <P>(type: React.ComponentType<P>) => ReactTestInstance;
77
getByText: (text: string | RegExp) => ReactTestInstance;
8+
getByPlaceholder: (placeholder: string | RegExp) => ReactTestInstance;
89
getByProps: (props: Record<string, any>) => ReactTestInstance;
910
getByTestId: (testID: string) => ReactTestInstance;
1011
getAllByName: (name: React.ReactType | string) => Array<ReactTestInstance>;
1112
getAllByType: <P>(type: React.ComponentType<P>) => Array<ReactTestInstance>;
1213
getAllByText: (text: string | RegExp) => Array<ReactTestInstance>;
14+
getAllByPlaceholder: (placeholder: string | RegExp) => Array<ReactTestInstance>;
1315
getAllByProps: (props: Record<string, any>) => Array<ReactTestInstance>;
1416
}
1517

1618
export interface QueryByAPI {
1719
queryByName: (name: React.ReactType | string) => ReactTestInstance | null;
1820
queryByType: <P>(type: React.ComponentType<P>) => ReactTestInstance | null;
1921
queryByText: (name: string | RegExp) => ReactTestInstance | null;
22+
queryByPlaceholder: (placeholder: string | RegExp) => ReactTestInstance | null;
2023
queryByProps: (props: Record<string, any>) => ReactTestInstance | null;
2124
queryByTestId: (testID: string) => ReactTestInstance | null;
2225
queryAllByName: (name: React.ReactType | string) => Array<ReactTestInstance> | [];
2326
queryAllByType: <P>(
2427
type: React.ComponentType<P>
2528
) => Array<ReactTestInstance> | [];
2629
queryAllByText: (text: string | RegExp) => Array<ReactTestInstance> | [];
30+
queryAllByPlaceholder: (placeholder: string | RegExp) => Array<ReactTestInstance> | [];
2731
queryAllByProps: (
2832
props: Record<string, any>
2933
) => Array<ReactTestInstance> | [];

0 commit comments

Comments
 (0)