Skip to content

Commit 9caa54a

Browse files
committed
chore: jest matchers.
1 parent cae0583 commit 9caa54a

File tree

7 files changed

+253
-1
lines changed

7 files changed

+253
-1
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@
6060
"typescript": "^5.0.2"
6161
},
6262
"dependencies": {
63-
"pretty-format": "^29.0.0"
63+
"jest-matcher-utils": "^29.6.2",
64+
"pretty-format": "^29.0.0",
65+
"redent": "^3.0.0"
6466
},
6567
"peerDependencies": {
6668
"jest": ">=28.0.0",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from 'react';
2+
import { View, Text } from 'react-native';
3+
import { render, screen } from '../..';
4+
import '../extend-expect';
5+
6+
function ShowChildren({ show }: { show: boolean }) {
7+
return show ? (
8+
<View>
9+
<Text testID="text">Hello</Text>
10+
</View>
11+
) : (
12+
<View />
13+
);
14+
}
15+
16+
test('toBeOnTheScreen() on attached element', () => {
17+
render(<View testID="test" />);
18+
const element = screen.getByTestId('test');
19+
expect(element).toBeOnTheScreen();
20+
expect(() => expect(element).not.toBeOnTheScreen())
21+
.toThrowErrorMatchingInlineSnapshot(`
22+
"expect(element).not.toBeOnTheScreen()
23+
24+
expected element tree not to contain element but found:
25+
<View
26+
testID="test"
27+
/>"
28+
`);
29+
});
30+
31+
test('toBeOnTheScreen() on detached element', () => {
32+
render(<ShowChildren show />);
33+
const element = screen.getByTestId('text');
34+
35+
screen.update(<ShowChildren show={false} />);
36+
expect(element).toBeTruthy();
37+
expect(element).not.toBeOnTheScreen();
38+
expect(() => expect(element).toBeOnTheScreen())
39+
.toThrowErrorMatchingInlineSnapshot(`
40+
"expect(element).toBeOnTheScreen()
41+
42+
element could not be found in the element tree"
43+
`);
44+
});
45+
46+
test('toBeOnTheScreen() on null element', () => {
47+
expect(null).not.toBeOnTheScreen();
48+
expect(() => expect(null).toBeOnTheScreen())
49+
.toThrowErrorMatchingInlineSnapshot(`
50+
"expect(element).toBeOnTheScreen()
51+
52+
element could not be found in the element tree"
53+
`);
54+
});
55+
56+
test('example test', () => {
57+
render(
58+
<View>
59+
<View testID="child" />
60+
</View>
61+
);
62+
63+
const child = screen.getByTestId('child');
64+
expect(child).toBeOnTheScreen();
65+
66+
screen.update(<View />);
67+
expect(child).not.toBeOnTheScreen();
68+
});

src/matchers/extend-expect.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { toBeOnTheScreen } from './to-be-on-the-screen';
2+
3+
expect.extend({
4+
toBeOnTheScreen,
5+
});

src/matchers/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { toBeOnTheScreen } from './to-be-on-the-screen';

src/matchers/to-be-on-the-screen.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3+
import { checkReactElement, printElement } from './utils';
4+
5+
export function toBeOnTheScreen(
6+
this: jest.MatcherContext,
7+
element: ReactTestInstance
8+
) {
9+
if (element !== null) {
10+
checkReactElement(element, toBeOnTheScreen, this);
11+
}
12+
13+
const pass =
14+
element === null ? false : getScreenRoot() === getRootElement(element);
15+
16+
const errorFound = () => {
17+
return `expected element tree not to contain element but found:\n${printElement(
18+
element
19+
)}`;
20+
};
21+
22+
const errorNotFound = () => {
23+
return `element could not be found in the element tree`;
24+
};
25+
26+
return {
27+
pass,
28+
message: () => {
29+
return [
30+
matcherHint(
31+
`${this.isNot ? '.not' : ''}.toBeOnTheScreen`,
32+
'element',
33+
''
34+
),
35+
'',
36+
RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
37+
].join('\n');
38+
},
39+
};
40+
}
41+
42+
function getRootElement(element: ReactTestInstance) {
43+
let root = element;
44+
while (root.parent) {
45+
root = root.parent;
46+
}
47+
return root;
48+
}
49+
50+
function getScreenRoot() {
51+
try {
52+
// eslint-disable-next-line import/no-extraneous-dependencies
53+
const { screen } = require('@testing-library/react-native');
54+
if (!screen) {
55+
throw new Error('screen is undefined');
56+
}
57+
58+
return screen.UNSAFE_root ?? screen.container;
59+
} catch (error) {
60+
throw new Error(
61+
'Could not import `screen` object from @testing-library/react-native.\n\n' +
62+
'Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies.'
63+
);
64+
}
65+
}

src/matchers/utils.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import {
3+
RECEIVED_COLOR,
4+
matcherHint,
5+
printWithType,
6+
printReceived,
7+
} from 'jest-matcher-utils';
8+
import prettyFormat, { plugins } from 'pretty-format';
9+
import redent from 'redent';
10+
11+
class ReactElementTypeError extends Error {
12+
constructor(
13+
received: unknown,
14+
matcherFn: jest.CustomMatcher,
15+
context: jest.MatcherContext
16+
) {
17+
super();
18+
19+
/* istanbul ignore next */
20+
if (Error.captureStackTrace) {
21+
Error.captureStackTrace(this, matcherFn);
22+
}
23+
let withType = '';
24+
try {
25+
withType = printWithType('Received', received, printReceived);
26+
} catch (e) {
27+
// Deliberately empty.
28+
}
29+
30+
/* istanbul ignore next */
31+
this.message = [
32+
matcherHint(
33+
`${context.isNot ? '.not' : ''}.${matcherFn.name}`,
34+
'received',
35+
''
36+
),
37+
'',
38+
`${RECEIVED_COLOR('received')} value must be a React Element.`,
39+
withType,
40+
].join('\n');
41+
}
42+
}
43+
44+
export function printElement(element: ReactTestInstance | null) {
45+
if (element == null) {
46+
return 'null';
47+
}
48+
49+
return redent(
50+
prettyFormat(
51+
{
52+
// This prop is needed persuade the prettyFormat that the element is
53+
// a ReactTestRendererJSON instance, so it is formatted as JSX.
54+
$$typeof: Symbol.for('react.test.json'),
55+
type: element.type,
56+
props: element.props,
57+
},
58+
{
59+
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
60+
printFunctionName: false,
61+
printBasicPrototype: false,
62+
highlight: true,
63+
}
64+
),
65+
2
66+
);
67+
}
68+
69+
export function checkReactElement(
70+
element: ReactTestInstance | null | undefined,
71+
matcherFn: jest.CustomMatcher,
72+
context: jest.MatcherContext
73+
): asserts element is ReactTestInstance {
74+
if (!element) {
75+
throw new ReactElementTypeError(element, matcherFn, context);
76+
}
77+
78+
// @ts-expect-error internal _fiber property of ReactTestInstance
79+
if (!element._fiber && !VALID_ELEMENTS.includes(element.type.toString())) {
80+
throw new ReactElementTypeError(element, matcherFn, context);
81+
}
82+
}

yarn.lock

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4664,6 +4664,16 @@ jest-diff@^29.0.1, jest-diff@^29.6.1:
46644664
jest-get-type "^29.4.3"
46654665
pretty-format "^29.6.1"
46664666

4667+
jest-diff@^29.6.2:
4668+
version "29.6.2"
4669+
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46"
4670+
integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==
4671+
dependencies:
4672+
chalk "^4.0.0"
4673+
diff-sequences "^29.4.3"
4674+
jest-get-type "^29.4.3"
4675+
pretty-format "^29.6.2"
4676+
46674677
jest-docblock@^29.4.3:
46684678
version "29.4.3"
46694679
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8"
@@ -4741,6 +4751,16 @@ jest-matcher-utils@^29.0.1, jest-matcher-utils@^29.6.1:
47414751
jest-get-type "^29.4.3"
47424752
pretty-format "^29.6.1"
47434753

4754+
jest-matcher-utils@^29.6.2:
4755+
version "29.6.2"
4756+
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535"
4757+
integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==
4758+
dependencies:
4759+
chalk "^4.0.0"
4760+
jest-diff "^29.6.2"
4761+
jest-get-type "^29.4.3"
4762+
pretty-format "^29.6.2"
4763+
47444764
jest-message-util@^29.6.1:
47454765
version "29.6.1"
47464766
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.1.tgz#d0b21d87f117e1b9e165e24f245befd2ff34ff8d"
@@ -6279,6 +6299,15 @@ pretty-format@^29.0.0, pretty-format@^29.0.3, pretty-format@^29.6.1:
62796299
ansi-styles "^5.0.0"
62806300
react-is "^18.0.0"
62816301

6302+
pretty-format@^29.6.2:
6303+
version "29.6.2"
6304+
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47"
6305+
integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==
6306+
dependencies:
6307+
"@jest/schemas" "^29.6.0"
6308+
ansi-styles "^5.0.0"
6309+
react-is "^18.0.0"
6310+
62826311
process-nextick-args@~2.0.0:
62836312
version "2.0.1"
62846313
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"

0 commit comments

Comments
 (0)