Skip to content

Enable partial matching #546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/__tests__/getByApi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Text>Hello</Text>);
expect(() => getByText('SomethingElse')).toThrowError(
'No instances found with text: SomethingElse'
);
});
29 changes: 29 additions & 0 deletions src/__tests__/queryByApi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,32 @@ test('queryByText nested deep <CustomText> in <Text>', () => {
).queryByText('Hello World!')
).toBeTruthy();
});

test('queryByText subset of longer text', () => {
expect(
render(<Text>This is a long text</Text>).queryByText('long text')
).toBeTruthy();
});

test('queryAllByText does not match several times the same text', () => {
const allMatched = render(
<Text nativeID="1">
Start
<Text nativeID="2">This is a long text</Text>
</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(
<Text nativeID="1">
Start
<Text nativeID="2">This is a long text</Text>
<Text nativeID="3">This is another long text</Text>
</Text>
).queryAllByText('long text');
expect(allMatched.length).toBe(2);
expect(allMatched.map((node) => node.props.nativeID)).toEqual(['2', '3']);
});
90 changes: 80 additions & 10 deletions src/helpers/getByAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Copy link
Collaborator Author

@AugustinLF AugustinLF Sep 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using includes here posed a lot of problem since it means that the parent nodes will also match. Hence the significant update to the logic.

return {
exact: false,
text,
};
}

return null;
}
} else {
return text.test(textToTest)
? {
exact: true,
}
: null;
}
}
}
return false;
return null;
} catch (error) {
throw createLibraryNotSupportedError(error);
}
Expand Down Expand Up @@ -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) =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we switch from find to findAll, the implementation doesn't throw anymore if no match is found. So I updated that so we can still use this implementation in queryByText (which shouldn't throw).

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) =>
Expand Down Expand Up @@ -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)}`,
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/queryByAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as React from 'react';
import {
getByTestId,
getByText,
nonErroringGetByText,
getByPlaceholderText,
getByDisplayValue,
getAllByTestId,
Expand All @@ -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);
}
Expand Down