diff --git a/src/__tests__/getByApi.test.js b/src/__tests__/getByApi.test.js index a2b2637d1..2756cff2d 100644 --- a/src/__tests__/getByApi.test.js +++ b/src/__tests__/getByApi.test.js @@ -50,3 +50,10 @@ test('supports a regex matcher', () => { expect(getByTestId(/view/)).toBeTruthy(); expect(getAllByTestId(/text/)).toHaveLength(2); }); + +test('throws when no text matching is found', () => { + const { getByText } = render(Hello); + expect(() => getByText('SomethingElse')).toThrowError( + 'No instances found with text: SomethingElse' + ); +}); diff --git a/src/__tests__/queryByApi.test.js b/src/__tests__/queryByApi.test.js index f7f39b991..b95f7488d 100644 --- a/src/__tests__/queryByApi.test.js +++ b/src/__tests__/queryByApi.test.js @@ -114,3 +114,32 @@ test('queryByText nested deep in ', () => { ).queryByText('Hello World!') ).toBeTruthy(); }); + +test('queryByText subset of longer text', () => { + expect( + render(This is a long text).queryByText('long text') + ).toBeTruthy(); +}); + +test('queryAllByText does not match several times the same text', () => { + const allMatched = render( + + Start + This is a long text + + ).queryAllByText('long text'); + expect(allMatched.length).toBe(1); + expect(allMatched[0].props.nativeID).toBe('2'); +}); + +test('queryAllByText matches all the matching nodes', () => { + const allMatched = render( + + Start + This is a long text + This is another long text + + ).queryAllByText('long text'); + expect(allMatched.length).toBe(2); + expect(allMatched.map((node) => node.props.nativeID)).toEqual(['2', '3']); +}); diff --git a/src/helpers/getByAPI.js b/src/helpers/getByAPI.js index 844820125..d8bbac387 100644 --- a/src/helpers/getByAPI.js +++ b/src/helpers/getByAPI.js @@ -20,12 +20,29 @@ const getNodeByText = (node, text) => { const textChildren = getChildrenAsText(node.props.children, Text); if (textChildren) { const textToTest = textChildren.join(''); - return typeof text === 'string' - ? text === textToTest - : text.test(textToTest); + if (typeof text === 'string') { + if (text === textToTest) { + return { exact: true }; + } else { + if (textToTest.includes(text)) { + return { + exact: false, + text, + }; + } + + return null; + } + } else { + return text.test(textToTest) + ? { + exact: true, + } + : null; + } } } - return false; + return null; } catch (error) { throw createLibraryNotSupportedError(error); } @@ -95,13 +112,46 @@ const getNodeByTestId = (node, testID) => { : testID.test(node.props.testID); }; +export const nonErroringGetByText = (instance: ReactTestInstance) => + function nonErroringGetByTextFn(text: string | RegExp) { + const matchingInstances = instance.findAll((node) => + getNodeByText(node, text) + ); + if (matchingInstances.length === 0) { + return null; + } + + const matches = matchingInstances.map((instance: ReactTestInstance) => ({ + instance, + ...getNodeByText(instance, text), + })); + + // Sort the matches from the best to the worst (exact matches should come first, then closest + // matching text) + matches.sort((firstMatch, secondMatch) => { + if (firstMatch.exact) { + return -1; + } + if (secondMatch.exact) { + return 1; + } + return secondMatch.text.length - firstMatch.text.length; + }); + + return matches[0].instance; + }; + export const getByText = (instance: ReactTestInstance) => function getByTextFn(text: string | RegExp) { - try { - return instance.find((node) => getNodeByText(node, text)); - } catch (error) { - throw new ErrorWithStack(prepareErrorMessage(error), getByTextFn); + const match = nonErroringGetByText(instance)(text); + if (match) { + return match; } + + throw new ErrorWithStack( + `No instances found with text: ${String(text)}`, + getByTextFn + ); }; export const getByPlaceholderText = (instance: ReactTestInstance) => @@ -148,9 +198,29 @@ export const getByTestId = (instance: ReactTestInstance) => } }; -export const getAllByText = (instance: ReactTestInstance) => +export const getAllByText = (rootInstance: ReactTestInstance) => function getAllByTextFn(text: string | RegExp) { - const results = instance.findAll((node) => getNodeByText(node, text)); + const results = rootInstance.findAll((instance) => { + // We want to match only if there's no better match down the tree. + const match = getNodeByText(instance, text); + if (match && match.exact === false) { + const matchingInstances = instance.findAll((node) => + getNodeByText(node, text) + ); + if (matchingInstances.length === 0) { + return false; + } + + if (matchingInstances.length === 1) { + return true; + } + + // There's more than 1 match, that means that down the tree there will be a better matching node + return false; + } + + return Boolean(match); + }); if (results.length === 0) { throw new ErrorWithStack( `No instances found with text: ${String(text)}`, diff --git a/src/helpers/queryByAPI.js b/src/helpers/queryByAPI.js index 3d5ce8488..2e3280f3f 100644 --- a/src/helpers/queryByAPI.js +++ b/src/helpers/queryByAPI.js @@ -2,7 +2,7 @@ import * as React from 'react'; import { getByTestId, - getByText, + nonErroringGetByText, getByPlaceholderText, getByDisplayValue, getAllByTestId, @@ -23,7 +23,7 @@ import { export const queryByText = (instance: ReactTestInstance) => function queryByTextFn(text: string | RegExp) { try { - return getByText(instance)(text); + return nonErroringGetByText(instance)(text); } catch (error) { return createQueryByError(error, queryByTextFn); }