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
This commit is contained in:
Dana Hartweg
2018-12-22 17:09:28 -05:00
committed by Michał Pierzchała
parent 5145a708db
commit ee888f1dcd
7 changed files with 185 additions and 1 deletions
@@ -10,6 +10,18 @@ exports[`debug 1`] = `
>
not fresh
</Text>
<TextInput
allowFontScaling={true}
placeholder=\\"Add custom freshness\\"
testID=\\"bananaCustomFreshness\\"
underlineColorAndroid=\\"transparent\\"
/>
<TextInput
allowFontScaling={true}
placeholder=\\"Who inspected freshness?\\"
testID=\\"bananaChef\\"
underlineColorAndroid=\\"transparent\\"
/>
<View
accessible={true}
isTVSelectable={true}
@@ -42,6 +54,18 @@ exports[`debug changing component: bananaFresh button message should now be "fre
>
fresh
</Text>
<TextInput
allowFontScaling={true}
placeholder=\\"Add custom freshness\\"
testID=\\"bananaCustomFreshness\\"
underlineColorAndroid=\\"transparent\\"
/>
<TextInput
allowFontScaling={true}
placeholder=\\"Who inspected freshness?\\"
testID=\\"bananaChef\\"
underlineColorAndroid=\\"transparent\\"
/>
<View
accessible={true}
isTVSelectable={true}
@@ -74,6 +98,18 @@ exports[`debug: shallow 1`] = `
>
not fresh
</Text>
<TextInput
allowFontScaling={true}
placeholder=\\"Add custom freshness\\"
testID=\\"bananaCustomFreshness\\"
underlineColorAndroid=\\"transparent\\"
/>
<TextInput
allowFontScaling={true}
placeholder=\\"Who inspected freshness?\\"
testID=\\"bananaChef\\"
underlineColorAndroid=\\"transparent\\"
/>
<Button
onPress={[Function anonymous]}
type=\\"primary\\"
@@ -95,6 +131,18 @@ exports[`debug: shallow with message 1`] = `
>
not fresh
</Text>
<TextInput
allowFontScaling={true}
placeholder=\\"Add custom freshness\\"
testID=\\"bananaCustomFreshness\\"
underlineColorAndroid=\\"transparent\\"
/>
<TextInput
allowFontScaling={true}
placeholder=\\"Who inspected freshness?\\"
testID=\\"bananaChef\\"
underlineColorAndroid=\\"transparent\\"
/>
<Button
onPress={[Function anonymous]}
type=\\"primary\\"
@@ -116,6 +164,18 @@ exports[`debug: with message 1`] = `
>
not fresh
</Text>
<TextInput
allowFontScaling={true}
placeholder=\\"Add custom freshness\\"
testID=\\"bananaCustomFreshness\\"
underlineColorAndroid=\\"transparent\\"
/>
<TextInput
allowFontScaling={true}
placeholder=\\"Who inspected freshness?\\"
testID=\\"bananaChef\\"
underlineColorAndroid=\\"transparent\\"
/>
<View
accessible={true}
isTVSelectable={true}
+40 -1
View File
@@ -1,10 +1,13 @@
// @flow
/* eslint-disable react/no-multi-comp */
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { View, Text, TextInput, TouchableOpacity } from 'react-native';
import stripAnsi from 'strip-ansi';
import { render, fireEvent } from '..';
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
class Button extends React.Component<*> {
render() {
return (
@@ -45,6 +48,11 @@ class Banana extends React.Component<*, *> {
<Text testID="bananaFresh">
{this.state.fresh ? 'fresh' : 'not fresh'}
</Text>
<TextInput
testID="bananaCustomFreshness"
placeholder={PLACEHOLDER_FRESHNESS}
/>
<TextInput testID="bananaChef" placeholder={PLACEHOLDER_CHEF} />
<Button onPress={this.changeFresh} type="primary">
Change freshness!
</Button>
@@ -138,6 +146,37 @@ test('getAllByText, queryAllByText', () => {
expect(queryAllByText('InExistent')).toHaveLength(0);
});
test('getByPlaceholder, queryByPlaceholder', () => {
const { getByPlaceholder, queryByPlaceholder } = render(<Banana />);
const input = getByPlaceholder(/custom/i);
expect(input.props.placeholder).toBe(PLACEHOLDER_FRESHNESS);
const sameInput = getByPlaceholder(PLACEHOLDER_FRESHNESS);
expect(sameInput.props.placeholder).toBe(PLACEHOLDER_FRESHNESS);
expect(() => getByPlaceholder('no placeholder')).toThrow(
'No instances found'
);
expect(queryByPlaceholder(/add/i)).toBe(input);
expect(queryByPlaceholder('no placeholder')).toBeNull();
expect(() => queryByPlaceholder(/fresh/)).toThrow('Expected 1 but found 2');
});
test('getAllByPlaceholder, queryAllByPlaceholder', () => {
const { getAllByPlaceholder, queryAllByPlaceholder } = render(<Banana />);
const inputs = getAllByPlaceholder(/fresh/i);
expect(inputs).toHaveLength(2);
expect(() => getAllByPlaceholder('no placeholder')).toThrow(
'No instances found'
);
expect(queryAllByPlaceholder(/fresh/i)).toEqual(inputs);
expect(queryAllByPlaceholder('no placeholder')).toHaveLength(0);
});
test('getByProps, queryByProps', () => {
const { getByProps, queryByProps } = render(<Banana />);
const primaryType = getByProps({ type: 'primary' });
+42
View File
@@ -28,6 +28,21 @@ const getNodeByText = (node, text) => {
}
};
const getTextInputNodeByPlaceholder = (node, placeholder) => {
try {
// eslint-disable-next-line
const { TextInput } = require('react-native');
return (
filterNodeByType(node, TextInput) &&
(typeof placeholder === 'string'
? placeholder === node.props.placeholder
: placeholder.test(node.props.placeholder))
);
} catch (error) {
throw createLibraryNotSupportedError(error);
}
};
const prepareErrorMessage = error =>
// Strip info about custom predicate
error.message.replace(/ matching custom predicate[^]*/gm, '');
@@ -62,6 +77,17 @@ export const getByText = (instance: ReactTestInstance) =>
}
};
export const getByPlaceholder = (instance: ReactTestInstance) =>
function getByPlaceholderFn(placeholder: string | RegExp) {
try {
return instance.find(node =>
getTextInputNodeByPlaceholder(node, placeholder)
);
} catch (error) {
throw new ErrorWithStack(prepareErrorMessage(error), getByPlaceholderFn);
}
};
export const getByProps = (instance: ReactTestInstance) =>
function getByPropsFn(props: { [propName: string]: any }) {
try {
@@ -114,6 +140,20 @@ export const getAllByText = (instance: ReactTestInstance) =>
return results;
};
export const getAllByPlaceholder = (instance: ReactTestInstance) =>
function getAllByPlaceholderFn(placeholder: string | RegExp) {
const results = instance.findAll(node =>
getTextInputNodeByPlaceholder(node, placeholder)
);
if (results.length === 0) {
throw new ErrorWithStack(
`No instances found with placeholder: ${String(placeholder)}`,
getAllByPlaceholderFn
);
}
return results;
};
export const getAllByProps = (instance: ReactTestInstance) =>
function getAllByPropsFn(props: { [propName: string]: any }) {
const results = instance.findAllByProps(props);
@@ -131,9 +171,11 @@ export const getByAPI = (instance: ReactTestInstance) => ({
getByName: getByName(instance),
getByType: getByType(instance),
getByText: getByText(instance),
getByPlaceholder: getByPlaceholder(instance),
getByProps: getByProps(instance),
getAllByName: getAllByName(instance),
getAllByType: getAllByType(instance),
getAllByText: getAllByText(instance),
getAllByPlaceholder: getAllByPlaceholder(instance),
getAllByProps: getAllByProps(instance),
});
+23
View File
@@ -5,10 +5,12 @@ import {
getByName,
getByType,
getByText,
getByPlaceholder,
getByProps,
getAllByName,
getAllByType,
getAllByText,
getAllByPlaceholder,
getAllByProps,
} from './getByAPI';
import { ErrorWithStack, logDeprecationWarning } from './errors';
@@ -48,6 +50,15 @@ export const queryByText = (instance: ReactTestInstance) =>
}
};
export const queryByPlaceholder = (instance: ReactTestInstance) =>
function queryByPlaceholderFn(placeholder: string | RegExp) {
try {
return getByPlaceholder(instance)(placeholder);
} catch (error) {
return createQueryByError(error, queryByPlaceholder);
}
};
export const queryByProps = (instance: ReactTestInstance) =>
function queryByPropsFn(props: { [propName: string]: any }) {
try {
@@ -97,6 +108,16 @@ export const queryAllByText = (instance: ReactTestInstance) => (
}
};
export const queryAllByPlaceholder = (instance: ReactTestInstance) => (
placeholder: string | RegExp
) => {
try {
return getAllByPlaceholder(instance)(placeholder);
} catch (error) {
return [];
}
};
export const queryAllByProps = (instance: ReactTestInstance) => (props: {
[propName: string]: any,
}) => {
@@ -112,9 +133,11 @@ export const queryByAPI = (instance: ReactTestInstance) => ({
queryByName: queryByName(instance),
queryByType: queryByType(instance),
queryByText: queryByText(instance),
queryByPlaceholder: queryByPlaceholder(instance),
queryByProps: queryByProps(instance),
queryAllByName: queryAllByName(instance),
queryAllByType: queryAllByType(instance),
queryAllByText: queryAllByText(instance),
queryAllByPlaceholder: queryAllByPlaceholder(instance),
queryAllByProps: queryAllByProps(instance),
});