Skip to content

Commit a97ed16

Browse files
feat: toBeOnTheScreen() matcher (#125)
1 parent c3dc5ce commit a97ed16

8 files changed

+192
-10
lines changed

README.md

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
- [`toBeEnabled`](#tobeenabled)
4545
- [`toBeEmptyElement`](#tobeemptyelement)
4646
- [`toContainElement`](#tocontainelement)
47+
- [`toBeOnTheScreen`](#tobeonthescreen)
4748
- [`toHaveProp`](#tohaveprop)
4849
- [`toHaveTextContent`](#tohavetextcontent)
4950
- [`toHaveStyle`](#tohavestyle)
@@ -72,15 +73,23 @@ These will make your tests more declarative, clear to read and to maintain.
7273

7374
These matchers should, for the most part, be agnostic enough to work with any React Native testing
7475
utilities, but they are primarily intended to be used with
75-
[RNTL](https://github.com/callstack/react-native-testing-library). Any issues raised with existing
76-
matchers or any newly proposed matchers must be viewed through compatibility with that library and
77-
its guiding principles first.
76+
[React Native Testing Library](https://github.com/callstack/react-native-testing-library). Any
77+
issues raised with existing matchers or any newly proposed matchers must be viewed through
78+
compatibility with that library and its guiding principles first.
7879

7980
## Installation
8081

8182
This module should be installed as one of your project's `devDependencies`:
8283

84+
#### Using `yarn`
85+
86+
```sh
87+
yarn add --dev @testing-library/jest-native
8388
```
89+
90+
#### Using `npm`
91+
92+
```sh
8493
npm install --save-dev @testing-library/jest-native
8594
```
8695

@@ -108,8 +117,10 @@ expect.extend({ toBeEmptyElement, toHaveTextContent });
108117

109118
## Matchers
110119

111-
`jest-native` has only been tested to work with `RNTL`. Keep in mind that these queries will only
112-
work on UI elements that bridge to native.
120+
`jest-native` has only been tested to work with
121+
[React Native Testing Library](https://github.com/callstack/react-native-testing-library). Keep in
122+
mind that these queries are intended only to work with elements corresponding to
123+
[host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components).
113124

114125
### `toBeDisabled`
115126

@@ -120,6 +131,7 @@ toBeDisabled();
120131
Check whether or not an element is disabled from a user perspective.
121132

122133
This matcher will check if the element or its parent has any of the following props :
134+
123135
- `disabled`
124136
- `accessibilityState={{ disabled: true }}`
125137
- `editable={false}` (for `TextInput` only)
@@ -183,11 +195,9 @@ expect(getByTestId('empty')).toBeEmptyElement();
183195

184196
---
185197

186-
**NOTE**
187-
188-
`toBeEmptyElement()` matcher has been renamed from `toBeEmpty()` because of the naming conflict with
189-
Jest Extended export with the
190-
[same name](https://github.com/jest-community/jest-extended#tobeempty).
198+
> **Note**<br/> This matcher has been previously named `toBeEmpty()`, but we changed that name in
199+
> order to avoid conflict with Jest Extendend matcher with the
200+
> [same name](https://github.com/jest-community/jest-extended#tobeempty).
191201
192202
---
193203

@@ -224,6 +234,35 @@ expect(parent).toContainElement(child);
224234
expect(parent).not.toContainElement(grandparent);
225235
```
226236

237+
### `toBeOnTheScreen`
238+
239+
```ts
240+
toBeOnTheScreen();
241+
```
242+
243+
Check that the element is present in the element tree.
244+
245+
You can check that an already captured element has not been removed from the element tree.
246+
247+
> **Note**<br/> This matcher requires React Native Testing Library v10.1 or later, as it includes
248+
> the `screen` object.
249+
250+
#### Examples
251+
252+
```tsx
253+
render(
254+
<View>
255+
<View testID="child" />
256+
</View>,
257+
);
258+
259+
const child = screen.getByTestId('child');
260+
expect(child).toBeOnTheScreen();
261+
262+
screen.update(<View />);
263+
expect(child).not.toBeOnTheScreen();
264+
```
265+
227266
### `toHaveProp`
228267

229268
```typescript

extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface JestNativeMatchers<R> {
66
toBeDisabled(): R;
77
toBeEmptyElement(): R;
88
toBeEnabled(): R;
9+
toBeOnTheScreen(): R;
910
toBeVisible(): R;
1011
toContainElement(element: ReactTestInstance | null): R;
1112
toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean }): R;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from 'react';
2+
import { View } from 'react-native';
3+
import { render } from '@testing-library/react-native';
4+
5+
jest.mock('@testing-library/react-native', () => ({
6+
...jest.requireActual('@testing-library/react-native'),
7+
screen: undefined,
8+
}));
9+
10+
test('toBeOnTheScreen() on null element', () => {
11+
const screen = render(<View testID="test" />);
12+
13+
const test = screen.getByTestId('test');
14+
expect(() => expect(test).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
15+
"Could not import \`screen\` object from @testing-library/react-native.
16+
17+
Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies."
18+
`);
19+
});

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

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

src/__types__/jest-explicit-extend.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { expect as jestExpect } from '@jest/globals';
66
jestExpect(null).toBeDisabled();
77
jestExpect(null).toBeEmptyElement();
88
jestExpect(null).toBeEnabled();
9+
jestExpect(null).toBeOnTheScreen();
910
jestExpect(null).toBeVisible();
1011
jestExpect(null).toContainElement(null);
1112
jestExpect(null).toHaveTextContent('');

src/__types__/jest-implicit-extend.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
expect(null).toBeDisabled();
44
expect(null).toBeEmptyElement();
55
expect(null).toBeEnabled();
6+
expect(null).toBeOnTheScreen();
67
expect(null).toBeVisible();
78
expect(null).toContainElement(null);
89
expect(null).toHaveTextContent('');

src/extend-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { toBeDisabled, toBeEnabled } from './to-be-disabled';
22
import { toBeEmptyElement, toBeEmpty } from './to-be-empty-element';
3+
import { toBeOnTheScreen } from './to-be-on-the-screen';
34
import { toContainElement } from './to-contain-element';
45
import { toHaveProp } from './to-have-prop';
56
import { toHaveStyle } from './to-have-style';
@@ -13,6 +14,7 @@ expect.extend({
1314
toBeEnabled,
1415
toBeEmptyElement,
1516
toBeEmpty, // Deprecated
17+
toBeOnTheScreen,
1618
toContainElement,
1719
toHaveProp,
1820
toHaveStyle,

src/to-be-on-the-screen.ts

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

0 commit comments

Comments
 (0)