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"