From ee888f1dcdc34fd3f509dc9ea00f27ebe824cc36 Mon Sep 17 00:00:00 2001 From: Dana Hartweg Date: Sat, 22 Dec 2018 17:09:28 -0500 Subject: [PATCH] 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 --- docs/API.md | 8 +++ .../__snapshots__/render.test.js.snap | 60 +++++++++++++++++++ src/__tests__/render.test.js | 41 ++++++++++++- src/helpers/getByAPI.js | 42 +++++++++++++ src/helpers/queryByAPI.js | 23 +++++++ typings/__tests__/index.test.tsx | 8 +++ typings/index.d.ts | 4 ++ 7 files changed, 185 insertions(+), 1 deletion(-) diff --git a/docs/API.md b/docs/API.md index 4dd52974..15a89e0c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -52,6 +52,14 @@ A method returning a `ReactTestInstance` with matching text – may be a string A method returning an array of `ReactTestInstance`s with matching text – may be a string or regular expression. +### `getByPlaceholder: (placeholder: string | RegExp)` + +A method returning a `ReactTestInstance` for a `TextInput` with a matching placeholder – may be a string or regular expression. Throws when no matches. + +### `getAllByPlaceholder: (placeholder: string | RegExp)` + +A method returning an array of `ReactTestInstance`s for `TextInput`'s with a matching placeholder – may be a string or regular expression. + ### `getByProps: (props: { [propName: string]: any })` A method returning a `ReactTestInstance` with matching props object. Throws when no matches. diff --git a/src/__tests__/__snapshots__/render.test.js.snap b/src/__tests__/__snapshots__/render.test.js.snap index 63b4a8c3..dd0cc846 100644 --- a/src/__tests__/__snapshots__/render.test.js.snap +++ b/src/__tests__/__snapshots__/render.test.js.snap @@ -10,6 +10,18 @@ exports[`debug 1`] = ` > not fresh + + fresh + + not fresh + + @@ -138,6 +146,37 @@ test('getAllByText, queryAllByText', () => { expect(queryAllByText('InExistent')).toHaveLength(0); }); +test('getByPlaceholder, queryByPlaceholder', () => { + const { getByPlaceholder, queryByPlaceholder } = render(); + 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(); + 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(); const primaryType = getByProps({ type: 'primary' }); diff --git a/src/helpers/getByAPI.js b/src/helpers/getByAPI.js index 5d5ca0dc..a2fd7fd0 100644 --- a/src/helpers/getByAPI.js +++ b/src/helpers/getByAPI.js @@ -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), }); diff --git a/src/helpers/queryByAPI.js b/src/helpers/queryByAPI.js index 15458af8..a5ef8062 100644 --- a/src/helpers/queryByAPI.js +++ b/src/helpers/queryByAPI.js @@ -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), }); diff --git a/typings/__tests__/index.test.tsx b/typings/__tests__/index.test.tsx index 9249ee13..b9312587 100644 --- a/typings/__tests__/index.test.tsx +++ b/typings/__tests__/index.test.tsx @@ -15,6 +15,7 @@ interface HasRequiredProp { const View = props => props.children; const Text = props => props.children; +const TextInput = props => props.children; const ElementWithRequiredProps = (props: HasRequiredProp) => ( {props.requiredProp} ); @@ -22,6 +23,7 @@ const ElementWithRequiredProps = (props: HasRequiredProp) => ( const TestComponent = () => ( Test component + ); @@ -36,6 +38,8 @@ const getByTypeWithRequiredProps: ReactTestInstance = tree.getByType( ); const getByTextString: ReactTestInstance = tree.getByText(''); const getByTextRegExp: ReactTestInstance = tree.getByText(/View/g); +const getByPlaceholderString: ReactTestInstance = tree.getByPlaceholder('my placeholder'); +const getByPlaceholderRegExp: ReactTestInstance = tree.getByPlaceholder(/placeholder/g); const getByProps: ReactTestInstance = tree.getByProps({ value: 2 }); const getByTestId: ReactTestInstance = tree.getByTestId('test-id'); const getAllByNameString: Array = tree.getAllByName('View'); @@ -65,6 +69,10 @@ const queryByTextString: ReactTestInstance | null = tree.queryByText( '' ); const queryByTextRegExp: ReactTestInstance | null = tree.queryByText(/View/g); +const queryByPlaceholderString: ReactTestInstance | null = tree.queryByText( + 'my placeholder' +); +const queryByPlaceholderRegExp: ReactTestInstance | null = tree.queryByText(/placeholder/g); const queryByProps: ReactTestInstance | null = tree.queryByProps({ value: 2 }); const queryByTestId: ReactTestInstance | null = tree.queryByTestId('test-id'); const queryAllByNameString: Array = tree.getAllByName( diff --git a/typings/index.d.ts b/typings/index.d.ts index 8232c537..0b974e59 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -5,11 +5,13 @@ export interface GetByAPI { getByName: (name: React.ReactType | string) => ReactTestInstance; getByType:

(type: React.ComponentType

) => ReactTestInstance; getByText: (text: string | RegExp) => ReactTestInstance; + getByPlaceholder: (placeholder: string | RegExp) => ReactTestInstance; getByProps: (props: Record) => ReactTestInstance; getByTestId: (testID: string) => ReactTestInstance; getAllByName: (name: React.ReactType | string) => Array; getAllByType:

(type: React.ComponentType

) => Array; getAllByText: (text: string | RegExp) => Array; + getAllByPlaceholder: (placeholder: string | RegExp) => Array; getAllByProps: (props: Record) => Array; } @@ -17,6 +19,7 @@ export interface QueryByAPI { queryByName: (name: React.ReactType | string) => ReactTestInstance | null; queryByType:

(type: React.ComponentType

) => ReactTestInstance | null; queryByText: (name: string | RegExp) => ReactTestInstance | null; + queryByPlaceholder: (placeholder: string | RegExp) => ReactTestInstance | null; queryByProps: (props: Record) => ReactTestInstance | null; queryByTestId: (testID: string) => ReactTestInstance | null; queryAllByName: (name: React.ReactType | string) => Array | []; @@ -24,6 +27,7 @@ export interface QueryByAPI { type: React.ComponentType

) => Array | []; queryAllByText: (text: string | RegExp) => Array | []; + queryAllByPlaceholder: (placeholder: string | RegExp) => Array | []; queryAllByProps: ( props: Record ) => Array | [];