Skip to content

Commit d81bc01

Browse files
committed
feat: add toBeVisible matcher
1 parent 0dbda71 commit d81bc01

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
- [`toHaveProp`](#tohaveprop)
4848
- [`toHaveTextContent`](#tohavetextcontent)
4949
- [`toHaveStyle`](#tohavestyle)
50+
- [`toBeVisible`](#tobevisible)
5051
- [Inspiration](#inspiration)
5152
- [Other solutions](#other-solutions)
5253
- [Contributors](#contributors)
@@ -298,6 +299,107 @@ expect(queryByText('Hello World')).toHaveStyle([{ color: 'black' }, { fontWeight
298299
expect(queryByText('Hello World')).not.toHaveStyle({ color: 'white' });
299300
```
300301

302+
### `toBeVisible`
303+
304+
```typescript
305+
toBeVisible();
306+
```
307+
308+
Check that the given element is visible.
309+
310+
An element is visible if **all** the following conditions are met:
311+
312+
- it does not have its style property `display` set to `none`.
313+
- it does not have its style property `opacity` set to `0`.
314+
- it is not a `Modal` component or it does not have the prop `visible` set to `false`.
315+
- its ancestor elements are also visible.
316+
317+
#### Examples
318+
319+
```javascript
320+
const { getByTestId } = render(<View testID="empty-view" />);
321+
322+
expect(getByTestId('empty-view')).toBeVisible();
323+
```
324+
325+
```javascript
326+
const { getByTestId } = render(<View testID="view-with-opacity" style={{ opacity: 0.2 }} />);
327+
328+
expect(getByTestId('view-with-opacity')).toBeVisible();
329+
```
330+
331+
```javascript
332+
const { getByTestId } = render(<Modal testID="empty-modal" />);
333+
334+
expect(getByTestId('empty-modal')).toBeVisible();
335+
```
336+
337+
```javascript
338+
const { getByTestId } = render(
339+
<Modal>
340+
<View>
341+
<View testID="view-within-modal" />
342+
</View>
343+
</Modal>,
344+
);
345+
346+
expect(getByTestId('view-within-modal')).toBeVisible();
347+
```
348+
349+
```javascript
350+
const { getByTestId } = render(<View testID="invisible-view" style={{ opacity: 0 }} />);
351+
352+
expect(getByTestId('invisible-view')).not.toBeVisible();
353+
```
354+
355+
```javascript
356+
const { getByTestId } = render(<View testID="display-none-view" style={{ display: 'none' }} />);
357+
358+
expect(getByTestId('display-none-view')).not.toBeVisible();
359+
```
360+
361+
```javascript
362+
const { getByTestId } = render(
363+
<View style={{ opacity: 0 }}>
364+
<View>
365+
<View testID="view-within-invisible-view" />
366+
</View>
367+
</View>,
368+
);
369+
370+
expect(getByTestId('view-within-invisible-view')).not.toBeVisible();
371+
```
372+
373+
```javascript
374+
const { getByTestId } = render(
375+
<View style={{ display: 'none' }}>
376+
<View>
377+
<View testID="view-within-display-none-view" />
378+
</View>
379+
</View>,
380+
);
381+
382+
expect(getByTestId('view-within-display-none-view')).not.toBeVisible();
383+
```
384+
385+
```javascript
386+
const { getByTestId } = render(
387+
<Modal visible={false}>
388+
<View>
389+
<View testID="view-within-not-visible-modal" />
390+
</View>
391+
</Modal>,
392+
);
393+
394+
expect(getByTestId('view-within-not-visible-modal')).not.toBeVisible();
395+
```
396+
397+
```javascript
398+
const { getByTestId } = render(<Modal testID="not-visible-modal" visible={false} />);
399+
400+
expect(getByTestId('not-visible-modal')).not.toBeVisible();
401+
```
402+
301403
## Inspiration
302404

303405
This library was made to be a companion for

extend-expect.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare global {
1010
toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean }): R;
1111
toBeEnabled(): R;
1212
toHaveStyle(style: object[] | object): R;
13+
toBeVisible(): R;
1314
}
1415
}
1516
}

src/__tests__/to-be-visible.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { Modal, View } from 'react-native';
3+
import { render } from '@testing-library/react-native';
4+
5+
describe('.toBeVisible', () => {
6+
test.each([
7+
['Empty view', <View testID="test" />],
8+
['View with opacity', <View testID="test" style={{ opacity: 0.2 }} />],
9+
['Modal', <Modal testID="test" />],
10+
[
11+
'View within modal',
12+
<Modal>
13+
<View>
14+
<View testID="test" />
15+
</View>
16+
</Modal>,
17+
],
18+
])('handles positive test case: %s', (name, input) => {
19+
const { getByTestId } = render(input);
20+
expect(getByTestId('test')).toBeVisible();
21+
});
22+
23+
test.each([
24+
['View with 0 opacity', <View testID="test" style={{ opacity: 0 }} />],
25+
["View with display 'none'", <View testID="test" style={{ display: 'none' }} />],
26+
[
27+
'Ancestor view with 0 opacity',
28+
<View style={{ opacity: 0 }}>
29+
<View>
30+
<View testID="test" />
31+
</View>
32+
</View>,
33+
],
34+
[
35+
"Ancestor view with display 'none'",
36+
<View style={{ display: 'none' }}>
37+
<View>
38+
<View testID="test" />
39+
</View>
40+
</View>,
41+
],
42+
[
43+
'View within not visible modal',
44+
<Modal visible={false}>
45+
<View>
46+
<View testID="test" />
47+
</View>
48+
</Modal>,
49+
],
50+
['Not visible modal', <Modal testID="test" visible={false} />],
51+
])('handles negative test case: %s', (name, input) => {
52+
const { getByTestId } = render(input);
53+
expect(getByTestId('test')).not.toBeVisible();
54+
});
55+
56+
it('handles non-React elements', () => {
57+
expect(() => expect({ name: 'Non-React element' }).not.toBeVisible()).toThrowError();
58+
expect(() => expect(true).not.toBeVisible()).toThrowError();
59+
});
60+
});

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { toHaveProp } from './to-have-prop';
44
import { toHaveTextContent } from './to-have-text-content';
55
import { toContainElement } from './to-contain-element';
66
import { toHaveStyle } from './to-have-style';
7+
import { toBeVisible } from './to-be-visible';
78

89
export {
910
toBeDisabled,
@@ -13,4 +14,5 @@ export {
1314
toHaveTextContent,
1415
toBeEnabled,
1516
toHaveStyle,
17+
toBeVisible,
1618
};

src/to-be-visible.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { matcherHint } from 'jest-matcher-utils';
2+
import { mergeAll } from 'ramda';
3+
4+
import { checkReactElement, printElement } from './utils';
5+
6+
function isStyleVisible(element) {
7+
const style = element.props.style || {};
8+
const { display = 'flex', opacity = 1 } = Array.isArray(style) ? mergeAll(style) : style;
9+
return display !== 'none' && opacity !== 0;
10+
}
11+
12+
function isAttributeVisible(element) {
13+
return element.type !== 'Modal' || element.props.visible !== false;
14+
}
15+
16+
function isElementVisible(element) {
17+
return (
18+
isStyleVisible(element) &&
19+
isAttributeVisible(element) &&
20+
(!element.parent || isElementVisible(element.parent))
21+
);
22+
}
23+
24+
export function toBeVisible(element) {
25+
checkReactElement(element, toBeVisible, this);
26+
const isVisible = isElementVisible(element);
27+
return {
28+
pass: isVisible,
29+
message: () => {
30+
const is = isVisible ? 'is' : 'is not';
31+
return [
32+
matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
33+
'',
34+
`Received element ${is} visible:`,
35+
printElement(element),
36+
].join('\n');
37+
},
38+
};
39+
}

0 commit comments

Comments
 (0)