Skip to content

Commit 58cd0f4

Browse files
committed
feat: add generic getBy and getAllBy queries
1 parent 6a65b76 commit 58cd0f4

File tree

6 files changed

+181
-31
lines changed

6 files changed

+181
-31
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@babel/core": "^7.1.2",
2020
"@callstack/eslint-config": "^6.0.0",
2121
"@release-it/conventional-changelog": "^1.1.0",
22-
"@types/react": "^16.7.11",
22+
"@types/react": "^16.8.6",
2323
"@types/react-test-renderer": "^16.0.3",
2424
"@typescript-eslint/eslint-plugin": "^1.9.0",
2525
"babel-jest": "^24.7.1",
@@ -31,14 +31,15 @@
3131
"flow-copy-source": "^2.0.6",
3232
"jest": "^24.7.1",
3333
"metro-react-native-babel-preset": "^0.52.0",
34-
"react": "^16.8.3",
34+
"react": "^16.8.6",
3535
"react-native": "^0.59.8",
36-
"react-test-renderer": "^16.8.3",
36+
"react-test-renderer": "^16.8.6",
3737
"release-it": "^12.1.0",
3838
"strip-ansi": "^5.0.0",
3939
"typescript": "^3.1.1"
4040
},
4141
"dependencies": {
42+
"is-plain-object": "^3.0.0",
4243
"pretty-format": "^24.0.0"
4344
},
4445
"peerDependencies": {

src/__tests__/__snapshots__/shallow.test.js.snap

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`shallow rendering React Test Instance 1`] = `
4-
<TouchableText
5-
accessible={true}
6-
allowFontScaling={true}
7-
ellipsizeMode="tail"
8-
forwardedRef={null}
4+
<Text
95
testID="text-button"
106
>
117
Press me
12-
</TouchableText>
8+
</Text>
139
`;
1410

1511
exports[`shallow rendering React elements 1`] = `

src/__tests__/getByAPI.test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// @flow
2+
import React from 'react';
3+
import { TouchableOpacity, Text } from 'react-native';
4+
import { render } from '..';
5+
6+
const BUTTON_ID = 'button_id';
7+
const TEXT_ID = 'text_id';
8+
const BUTTON_STYLE = { textTransform: 'uppercase' };
9+
const TEXT_LABEL = 'cool text';
10+
const NO_MATCHES_TEXT = 'not-existent-element';
11+
12+
const NO_INSTANCES_FOUND = 'No instances found';
13+
const FOUND_TWO_INSTANCES = 'Expected 1 but found 2 instances';
14+
15+
const Typography = ({ children, ...rest }) => {
16+
return <Text {...rest}>{children}</Text>;
17+
};
18+
19+
class Button extends React.Component<*> {
20+
render() {
21+
return (
22+
<TouchableOpacity testID={BUTTON_ID} style={BUTTON_STYLE}>
23+
<Typography testID={TEXT_ID}>{this.props.children}</Typography>
24+
</TouchableOpacity>
25+
);
26+
}
27+
}
28+
29+
function Section() {
30+
return (
31+
<>
32+
<Typography testID={TEXT_ID}>Title</Typography>
33+
<Button>{TEXT_LABEL}</Button>
34+
</>
35+
);
36+
}
37+
38+
test('getBy, queryBy', () => {
39+
const { getBy, queryBy } = render(<Section />);
40+
41+
expect(getBy({ testID: BUTTON_ID }).props.testID).toEqual(BUTTON_ID);
42+
const button = getBy({ testID: /button/g });
43+
expect(button && button.props.testID).toEqual(BUTTON_ID);
44+
expect(
45+
getBy({ testID: BUTTON_ID, type: TouchableOpacity }).props.testID
46+
).toEqual(BUTTON_ID);
47+
expect(() => getBy({ testID: BUTTON_ID, type: Text })).toThrow(
48+
NO_INSTANCES_FOUND
49+
);
50+
expect(
51+
getBy({
52+
testID: BUTTON_ID,
53+
props: { style: { textTransform: 'uppercase' } },
54+
}).props.testID
55+
).toEqual(BUTTON_ID);
56+
expect(() => getBy({ testID: NO_MATCHES_TEXT })).toThrow(NO_INSTANCES_FOUND);
57+
expect(queryBy({ testID: NO_MATCHES_TEXT })).toBeNull();
58+
59+
expect(() => getBy({ testID: TEXT_ID })).toThrow(FOUND_TWO_INSTANCES);
60+
expect(() => queryBy({ testID: TEXT_ID })).toThrow(FOUND_TWO_INSTANCES);
61+
});
62+
63+
test('getAllBy, queryAllBy', () => {
64+
const { getAllBy, queryAllBy } = render(<Section />);
65+
66+
const texts = getAllBy({ testID: TEXT_ID, type: Text });
67+
expect(texts).toHaveLength(2);
68+
const buttons = getAllBy({ testID: BUTTON_ID, type: TouchableOpacity });
69+
expect(buttons).toHaveLength(1);
70+
expect(queryAllBy({ testID: /id/g })).toEqual(
71+
expect.arrayContaining([...texts, ...buttons])
72+
);
73+
expect(() => getAllBy({ testID: NO_MATCHES_TEXT })).toThrow(
74+
NO_INSTANCES_FOUND
75+
);
76+
expect(queryAllBy({ testID: NO_MATCHES_TEXT })).toEqual([]);
77+
});

src/helpers/getByAPI.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @flow
22
import * as React from 'react';
33
import prettyFormat from 'pretty-format';
4+
import isPlainObject from 'is-plain-object';
45
import {
56
ErrorWithStack,
67
createLibraryNotSupportedError,
@@ -54,6 +55,48 @@ const getTextInputNodeByPlaceholder = (node, placeholder) => {
5455
}
5556
};
5657

58+
const makePaths = criteria =>
59+
[].concat(
60+
...Object.keys(criteria).map(key =>
61+
isPlainObject(criteria[key])
62+
? makePaths(criteria[key]).map(({ path, value }) => ({
63+
path: [key, ...path],
64+
value,
65+
}))
66+
: [{ path: [key], value: criteria[key] }]
67+
)
68+
);
69+
70+
const makeTest = criteria => {
71+
if (criteria.testID) {
72+
criteria.props = { testID: criteria.testID, ...criteria.props };
73+
delete criteria.testID;
74+
}
75+
const paths = makePaths(criteria);
76+
77+
return node =>
78+
paths.every(({ path: [...path], value }) => {
79+
let curr = node;
80+
while (path.length) {
81+
curr = curr[path.shift()];
82+
if (!curr) return false;
83+
}
84+
return value instanceof RegExp && typeof curr === 'string'
85+
? new RegExp(value).test(curr)
86+
: curr === value;
87+
});
88+
};
89+
90+
export const getBy = (instance: ReactTestInstance) =>
91+
function getByFn(criteria: Function | { [string]: any }) {
92+
try {
93+
const test = typeof criteria === 'object' ? makeTest(criteria) : criteria;
94+
return instance.find(test);
95+
} catch (error) {
96+
throw new ErrorWithStack(prepareErrorMessage(error), getByFn);
97+
}
98+
};
99+
57100
export const getByName = (instance: ReactTestInstance) =>
58101
function getByNameFn(name: string | React.ComponentType<*>) {
59102
logDeprecationWarning('getByName', 'getByType');
@@ -113,6 +156,19 @@ export const getByTestId = (instance: ReactTestInstance) =>
113156
}
114157
};
115158

159+
export const getAllBy = (instance: ReactTestInstance) =>
160+
function getAllByFn(criteria: Function | { [string]: any }) {
161+
const test = typeof criteria === 'object' ? makeTest(criteria) : criteria;
162+
const results = instance.findAll(test);
163+
if (results.length === 0) {
164+
throw new ErrorWithStack(
165+
`No instances found with for criteria:\n${prettyFormat(criteria)}`,
166+
getAllByFn
167+
);
168+
}
169+
return results;
170+
};
171+
116172
export const getAllByName = (instance: ReactTestInstance) =>
117173
function getAllByNameFn(name: string | React.ComponentType<*>) {
118174
logDeprecationWarning('getAllByName', 'getAllByType');
@@ -186,12 +242,14 @@ export const getAllByTestId = (instance: ReactTestInstance) =>
186242
};
187243

188244
export const getByAPI = (instance: ReactTestInstance) => ({
245+
getBy: getBy(instance),
189246
getByTestId: getByTestId(instance),
190247
getByName: getByName(instance),
191248
getByType: getByType(instance),
192249
getByText: getByText(instance),
193250
getByPlaceholder: getByPlaceholder(instance),
194251
getByProps: getByProps(instance),
252+
getAllBy: getAllBy(instance),
195253
getAllByTestId: getAllByTestId(instance),
196254
getAllByName: getAllByName(instance),
197255
getAllByType: getAllByType(instance),

src/helpers/queryByAPI.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// @flow
22
import * as React from 'react';
33
import {
4+
getBy,
45
getByTestId,
56
getByName,
67
getByType,
78
getByText,
89
getByPlaceholder,
910
getByProps,
11+
getAllBy,
1012
getAllByTestId,
1113
getAllByName,
1214
getAllByType,
@@ -16,6 +18,15 @@ import {
1618
} from './getByAPI';
1719
import { logDeprecationWarning, createQueryByError } from './errors';
1820

21+
export const queryBy = (instance: ReactTestInstance) =>
22+
function queryByFn(criteria: Function | { [string]: any }) {
23+
try {
24+
return getBy(instance)(criteria);
25+
} catch (error) {
26+
return createQueryByError(error, queryByFn);
27+
}
28+
};
29+
1930
export const queryByName = (instance: ReactTestInstance) =>
2031
function queryByNameFn(name: string | React.ComponentType<*>) {
2132
logDeprecationWarning('queryByName', 'getByName');
@@ -71,6 +82,16 @@ export const queryByTestId = (instance: ReactTestInstance) =>
7182
}
7283
};
7384

85+
export const queryAllBy = (instance: ReactTestInstance) => (
86+
criteria: Function | { [string]: any }
87+
) => {
88+
try {
89+
return getAllBy(instance)(criteria);
90+
} catch (error) {
91+
return [];
92+
}
93+
};
94+
7495
export const queryAllByName = (instance: ReactTestInstance) => (
7596
name: string | React.ComponentType<*>
7697
) => {
@@ -133,12 +154,14 @@ export const queryAllByTestId = (instance: ReactTestInstance) => (
133154
};
134155

135156
export const queryByAPI = (instance: ReactTestInstance) => ({
157+
queryBy: queryBy(instance),
136158
queryByTestId: queryByTestId(instance),
137159
queryByName: queryByName(instance),
138160
queryByType: queryByType(instance),
139161
queryByText: queryByText(instance),
140162
queryByPlaceholder: queryByPlaceholder(instance),
141163
queryByProps: queryByProps(instance),
164+
queryAllBy: queryAllBy(instance),
142165
queryAllByTestId: queryAllByTestId(instance),
143166
queryAllByName: queryAllByName(instance),
144167
queryAllByType: queryAllByType(instance),

yarn.lock

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@
10051005
dependencies:
10061006
"@types/react" "*"
10071007

1008-
"@types/react@*", "@types/react@^16.7.11":
1008+
"@types/react@*", "@types/react@^16.8.6":
10091009
version "16.8.19"
10101010
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.19.tgz#629154ef05e2e1985cdde94477deefd823ad9be3"
10111011
integrity sha512-QzEzjrd1zFzY9cDlbIiFvdr+YUmefuuRYrPxmkwG0UQv5XF35gFIi7a95m1bNVcFU0VimxSZ5QVGSiBmlggQXQ==
@@ -6632,16 +6632,11 @@ react-devtools-core@^3.6.0:
66326632
shell-quote "^1.6.1"
66336633
ws "^3.3.1"
66346634

6635-
react-is@^16.8.1, react-is@^16.8.4:
6635+
react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
66366636
version "16.8.6"
66376637
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
66386638
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
66396639

6640-
react-is@^16.8.3:
6641-
version "16.8.3"
6642-
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
6643-
integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==
6644-
66456640
react-native@^0.59.8:
66466641
version "0.59.8"
66476642
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.59.8.tgz#ade4141c777c60f5ec4889d9811d0f80a9d56547"
@@ -6705,15 +6700,15 @@ react-proxy@^1.1.7:
67056700
lodash "^4.6.1"
67066701
react-deep-force-update "^1.0.0"
67076702

6708-
react-test-renderer@^16.8.3:
6709-
version "16.8.3"
6710-
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.3.tgz#230006af264cc46aeef94392e04747c21839e05e"
6711-
integrity sha512-rjJGYebduKNZH0k1bUivVrRLX04JfIQ0FKJLPK10TAb06XWhfi4gTobooF9K/DEFNW98iGac3OSxkfIJUN9Mdg==
6703+
react-test-renderer@^16.8.6:
6704+
version "16.8.6"
6705+
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
6706+
integrity sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==
67126707
dependencies:
67136708
object-assign "^4.1.1"
67146709
prop-types "^15.6.2"
6715-
react-is "^16.8.3"
6716-
scheduler "^0.13.3"
6710+
react-is "^16.8.6"
6711+
scheduler "^0.13.6"
67176712

67186713
react-transform-hmr@^1.0.4:
67196714
version "1.0.4"
@@ -6722,15 +6717,15 @@ react-transform-hmr@^1.0.4:
67226717
global "^4.3.0"
67236718
react-proxy "^1.1.7"
67246719

6725-
react@^16.8.3:
6726-
version "16.8.3"
6727-
resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
6728-
integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==
6720+
react@^16.8.6:
6721+
version "16.8.6"
6722+
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
6723+
integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
67296724
dependencies:
67306725
loose-envify "^1.1.0"
67316726
object-assign "^4.1.1"
67326727
prop-types "^15.6.2"
6733-
scheduler "^0.13.3"
6728+
scheduler "^0.13.6"
67346729

67356730
read-pkg-up@^1.0.1:
67366731
version "1.0.1"
@@ -7266,10 +7261,10 @@ sax@~1.1.1:
72667261
version "1.1.6"
72677262
resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
72687263

7269-
scheduler@^0.13.3:
7270-
version "0.13.3"
7271-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
7272-
integrity sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==
7264+
scheduler@^0.13.6:
7265+
version "0.13.6"
7266+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
7267+
integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
72737268
dependencies:
72747269
loose-envify "^1.1.0"
72757270
object-assign "^4.1.1"

0 commit comments

Comments
 (0)