diff --git a/package.json b/package.json
index 33ce692f7..8eb2b87c7 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"@babel/core": "^7.1.2",
"@callstack/eslint-config": "^6.0.0",
"@release-it/conventional-changelog": "^1.1.0",
- "@types/react": "^16.7.11",
+ "@types/react": "^16.8.6",
"@types/react-test-renderer": "^16.0.3",
"@typescript-eslint/eslint-plugin": "^1.9.0",
"babel-jest": "^24.7.1",
@@ -31,14 +31,15 @@
"flow-copy-source": "^2.0.6",
"jest": "^24.7.1",
"metro-react-native-babel-preset": "^0.52.0",
- "react": "^16.8.3",
+ "react": "^16.8.6",
"react-native": "^0.59.8",
- "react-test-renderer": "^16.8.3",
+ "react-test-renderer": "^16.8.6",
"release-it": "^12.1.0",
"strip-ansi": "^5.0.0",
"typescript": "^3.1.1"
},
"dependencies": {
+ "is-plain-object": "^3.0.0",
"pretty-format": "^24.0.0"
},
"peerDependencies": {
diff --git a/src/__tests__/__snapshots__/shallow.test.js.snap b/src/__tests__/__snapshots__/shallow.test.js.snap
index 37c0b834a..ac70b9743 100644
--- a/src/__tests__/__snapshots__/shallow.test.js.snap
+++ b/src/__tests__/__snapshots__/shallow.test.js.snap
@@ -1,15 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`shallow rendering React Test Instance 1`] = `
-
Press me
-
+
`;
exports[`shallow rendering React elements 1`] = `
diff --git a/src/__tests__/getByAPI.test.js b/src/__tests__/getByAPI.test.js
new file mode 100644
index 000000000..f38a817e0
--- /dev/null
+++ b/src/__tests__/getByAPI.test.js
@@ -0,0 +1,77 @@
+// @flow
+import React from 'react';
+import { TouchableOpacity, Text } from 'react-native';
+import { render } from '..';
+
+const BUTTON_ID = 'button_id';
+const TEXT_ID = 'text_id';
+const BUTTON_STYLE = { textTransform: 'uppercase' };
+const TEXT_LABEL = 'cool text';
+const NO_MATCHES_TEXT = 'not-existent-element';
+
+const NO_INSTANCES_FOUND = 'No instances found';
+const FOUND_TWO_INSTANCES = 'Expected 1 but found 2 instances';
+
+const Typography = ({ children, ...rest }) => {
+ return {children};
+};
+
+class Button extends React.Component<*> {
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+function Section() {
+ return (
+ <>
+ Title
+
+ >
+ );
+}
+
+test('getBy, queryBy', () => {
+ const { getBy, queryBy } = render();
+
+ expect(getBy({ testID: BUTTON_ID }).props.testID).toEqual(BUTTON_ID);
+ const button = getBy({ testID: /button/g });
+ expect(button && button.props.testID).toEqual(BUTTON_ID);
+ expect(
+ getBy({ testID: BUTTON_ID, type: TouchableOpacity }).props.testID
+ ).toEqual(BUTTON_ID);
+ expect(() => getBy({ testID: BUTTON_ID, type: Text })).toThrow(
+ NO_INSTANCES_FOUND
+ );
+ expect(
+ getBy({
+ testID: BUTTON_ID,
+ props: { style: { textTransform: 'uppercase' } },
+ }).props.testID
+ ).toEqual(BUTTON_ID);
+ expect(() => getBy({ testID: NO_MATCHES_TEXT })).toThrow(NO_INSTANCES_FOUND);
+ expect(queryBy({ testID: NO_MATCHES_TEXT })).toBeNull();
+
+ expect(() => getBy({ testID: TEXT_ID })).toThrow(FOUND_TWO_INSTANCES);
+ expect(() => queryBy({ testID: TEXT_ID })).toThrow(FOUND_TWO_INSTANCES);
+});
+
+test('getAllBy, queryAllBy', () => {
+ const { getAllBy, queryAllBy } = render();
+
+ const texts = getAllBy({ testID: TEXT_ID, type: Text });
+ expect(texts).toHaveLength(2);
+ const buttons = getAllBy({ testID: BUTTON_ID, type: TouchableOpacity });
+ expect(buttons).toHaveLength(1);
+ expect(queryAllBy({ testID: /id/g })).toEqual(
+ expect.arrayContaining([...texts, ...buttons])
+ );
+ expect(() => getAllBy({ testID: NO_MATCHES_TEXT })).toThrow(
+ NO_INSTANCES_FOUND
+ );
+ expect(queryAllBy({ testID: NO_MATCHES_TEXT })).toEqual([]);
+});
diff --git a/src/helpers/getByAPI.js b/src/helpers/getByAPI.js
index c8fe5c429..d4932a6a7 100644
--- a/src/helpers/getByAPI.js
+++ b/src/helpers/getByAPI.js
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import prettyFormat from 'pretty-format';
+import isPlainObject from 'is-plain-object';
import {
ErrorWithStack,
createLibraryNotSupportedError,
@@ -54,6 +55,48 @@ const getTextInputNodeByPlaceholder = (node, placeholder) => {
}
};
+const makePaths = criteria =>
+ [].concat(
+ ...Object.keys(criteria).map(key =>
+ isPlainObject(criteria[key])
+ ? makePaths(criteria[key]).map(({ path, value }) => ({
+ path: [key, ...path],
+ value,
+ }))
+ : [{ path: [key], value: criteria[key] }]
+ )
+ );
+
+const makeTest = criteria => {
+ if (criteria.testID) {
+ criteria.props = { testID: criteria.testID, ...criteria.props };
+ delete criteria.testID;
+ }
+ const paths = makePaths(criteria);
+
+ return node =>
+ paths.every(({ path: [...path], value }) => {
+ let curr = node;
+ while (path.length) {
+ curr = curr[path.shift()];
+ if (!curr) return false;
+ }
+ return value instanceof RegExp && typeof curr === 'string'
+ ? new RegExp(value).test(curr)
+ : curr === value;
+ });
+};
+
+export const getBy = (instance: ReactTestInstance) =>
+ function getByFn(criteria: Function | { [string]: any }) {
+ try {
+ const test = typeof criteria === 'object' ? makeTest(criteria) : criteria;
+ return instance.find(test);
+ } catch (error) {
+ throw new ErrorWithStack(prepareErrorMessage(error), getByFn);
+ }
+ };
+
export const getByName = (instance: ReactTestInstance) =>
function getByNameFn(name: string | React.ComponentType<*>) {
logDeprecationWarning('getByName', 'getByType');
@@ -113,6 +156,19 @@ export const getByTestId = (instance: ReactTestInstance) =>
}
};
+export const getAllBy = (instance: ReactTestInstance) =>
+ function getAllByFn(criteria: Function | { [string]: any }) {
+ const test = typeof criteria === 'object' ? makeTest(criteria) : criteria;
+ const results = instance.findAll(test);
+ if (results.length === 0) {
+ throw new ErrorWithStack(
+ `No instances found with for criteria:\n${prettyFormat(criteria)}`,
+ getAllByFn
+ );
+ }
+ return results;
+ };
+
export const getAllByName = (instance: ReactTestInstance) =>
function getAllByNameFn(name: string | React.ComponentType<*>) {
logDeprecationWarning('getAllByName', 'getAllByType');
@@ -173,13 +229,28 @@ export const getAllByProps = (instance: ReactTestInstance) =>
return results;
};
+export const getAllByTestId = (instance: ReactTestInstance) =>
+ function getAllByTestIdFn(testID: string) {
+ const results = instance.findAllByProps({ testID });
+ if (results.length === 0) {
+ throw new ErrorWithStack(
+ `No instances found with testID: ${String(testID)}`,
+ getAllByTestIdFn
+ );
+ }
+ return results;
+ };
+
export const getByAPI = (instance: ReactTestInstance) => ({
+ getBy: getBy(instance),
getByTestId: getByTestId(instance),
getByName: getByName(instance),
getByType: getByType(instance),
getByText: getByText(instance),
getByPlaceholder: getByPlaceholder(instance),
getByProps: getByProps(instance),
+ getAllBy: getAllBy(instance),
+ getAllByTestId: getAllByTestId(instance),
getAllByName: getAllByName(instance),
getAllByType: getAllByType(instance),
getAllByText: getAllByText(instance),
diff --git a/src/helpers/queryByAPI.js b/src/helpers/queryByAPI.js
index 8ca7dbff5..184b9629e 100644
--- a/src/helpers/queryByAPI.js
+++ b/src/helpers/queryByAPI.js
@@ -1,12 +1,15 @@
// @flow
import * as React from 'react';
import {
+ getBy,
getByTestId,
getByName,
getByType,
getByText,
getByPlaceholder,
getByProps,
+ getAllBy,
+ getAllByTestId,
getAllByName,
getAllByType,
getAllByText,
@@ -15,6 +18,15 @@ import {
} from './getByAPI';
import { logDeprecationWarning, createQueryByError } from './errors';
+export const queryBy = (instance: ReactTestInstance) =>
+ function queryByFn(criteria: Function | { [string]: any }) {
+ try {
+ return getBy(instance)(criteria);
+ } catch (error) {
+ return createQueryByError(error, queryByFn);
+ }
+ };
+
export const queryByName = (instance: ReactTestInstance) =>
function queryByNameFn(name: string | React.ComponentType<*>) {
logDeprecationWarning('queryByName', 'getByName');
@@ -70,6 +82,16 @@ export const queryByTestId = (instance: ReactTestInstance) =>
}
};
+export const queryAllBy = (instance: ReactTestInstance) => (
+ criteria: Function | { [string]: any }
+) => {
+ try {
+ return getAllBy(instance)(criteria);
+ } catch (error) {
+ return [];
+ }
+};
+
export const queryAllByName = (instance: ReactTestInstance) => (
name: string | React.ComponentType<*>
) => {
@@ -121,13 +143,26 @@ export const queryAllByProps = (instance: ReactTestInstance) => (props: {
}
};
+export const queryAllByTestId = (instance: ReactTestInstance) => (
+ testID: string
+) => {
+ try {
+ return getAllByTestId(instance)(testID);
+ } catch (error) {
+ return [];
+ }
+};
+
export const queryByAPI = (instance: ReactTestInstance) => ({
+ queryBy: queryBy(instance),
queryByTestId: queryByTestId(instance),
queryByName: queryByName(instance),
queryByType: queryByType(instance),
queryByText: queryByText(instance),
queryByPlaceholder: queryByPlaceholder(instance),
queryByProps: queryByProps(instance),
+ queryAllBy: queryAllBy(instance),
+ queryAllByTestId: queryAllByTestId(instance),
queryAllByName: queryAllByName(instance),
queryAllByType: queryAllByType(instance),
queryAllByText: queryAllByText(instance),
diff --git a/yarn.lock b/yarn.lock
index 87fa4c141..a1888897e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1005,7 +1005,7 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@^16.7.11":
+"@types/react@*", "@types/react@^16.8.6":
version "16.8.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.19.tgz#629154ef05e2e1985cdde94477deefd823ad9be3"
integrity sha512-QzEzjrd1zFzY9cDlbIiFvdr+YUmefuuRYrPxmkwG0UQv5XF35gFIi7a95m1bNVcFU0VimxSZ5QVGSiBmlggQXQ==
@@ -6632,16 +6632,11 @@ react-devtools-core@^3.6.0:
shell-quote "^1.6.1"
ws "^3.3.1"
-react-is@^16.8.1, react-is@^16.8.4:
+react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
-react-is@^16.8.3:
- version "16.8.3"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
- integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==
-
react-native@^0.59.8:
version "0.59.8"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.59.8.tgz#ade4141c777c60f5ec4889d9811d0f80a9d56547"
@@ -6705,15 +6700,15 @@ react-proxy@^1.1.7:
lodash "^4.6.1"
react-deep-force-update "^1.0.0"
-react-test-renderer@^16.8.3:
- version "16.8.3"
- resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.3.tgz#230006af264cc46aeef94392e04747c21839e05e"
- integrity sha512-rjJGYebduKNZH0k1bUivVrRLX04JfIQ0FKJLPK10TAb06XWhfi4gTobooF9K/DEFNW98iGac3OSxkfIJUN9Mdg==
+react-test-renderer@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
+ integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
- react-is "^16.8.3"
- scheduler "^0.13.3"
+ react-is "^16.8.6"
+ scheduler "^0.13.6"
react-transform-hmr@^1.0.4:
version "1.0.4"
@@ -6722,15 +6717,15 @@ react-transform-hmr@^1.0.4:
global "^4.3.0"
react-proxy "^1.1.7"
-react@^16.8.3:
- version "16.8.3"
- resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
- integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==
+react@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
+ integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
- scheduler "^0.13.3"
+ scheduler "^0.13.6"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -7266,10 +7261,10 @@ sax@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
-scheduler@^0.13.3:
- version "0.13.3"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
- integrity sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==
+scheduler@^0.13.6:
+ version "0.13.6"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+ integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"