Skip to content

Commit

Permalink
feat: add toBeVisible matcher (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
fluiddot authored Oct 20, 2022
1 parent beaf547 commit 1b65ce3
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 0 deletions.
118 changes: 118 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
- [`toHaveProp`](#tohaveprop)
- [`toHaveTextContent`](#tohavetextcontent)
- [`toHaveStyle`](#tohavestyle)
- [`toBeVisible`](#tobevisible)
- [Inspiration](#inspiration)
- [Other solutions](#other-solutions)
- [Contributors](#contributors)
Expand Down Expand Up @@ -311,6 +312,123 @@ expect(getByText('Hello World')).not.toHaveStyle({
});
```

### `toBeVisible`

```typescript
toBeVisible();
```

Check that the given element is visible to the user.

An element is visible if **all** the following conditions are met:

- it does not have its style property `display` set to `none`.
- it does not have its style property `opacity` set to `0`.
- it is not a `Modal` component or it does not have the prop `visible` set to `false`.
- it is not hidden from accessibility as checked by [`isInaccessible`](https://callstack.github.io/react-native-testing-library/docs/api#isinaccessible) function from React Native Testing Library
- its ancestor elements are also visible.

#### Examples

```javascript
const { getByTestId } = render(<View testID="empty-view" />);

expect(getByTestId('empty-view')).toBeVisible();
```

```javascript
const { getByTestId } = render(<View testID="view-with-opacity" style={{ opacity: 0.2 }} />);

expect(getByTestId('view-with-opacity')).toBeVisible();
```

```javascript
const { getByTestId } = render(<Modal testID="empty-modal" />);

expect(getByTestId('empty-modal')).toBeVisible();
```

```javascript
const { getByTestId } = render(
<Modal>
<View>
<View testID="view-within-modal" />
</View>
</Modal>,
);

expect(getByTestId('view-within-modal')).toBeVisible();
```

```javascript
const { getByTestId } = render(<View testID="invisible-view" style={{ opacity: 0 }} />);

expect(getByTestId('invisible-view')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(<View testID="display-none-view" style={{ display: 'none' }} />);

expect(getByTestId('display-none-view')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(
<View style={{ opacity: 0 }}>
<View>
<View testID="view-within-invisible-view" />
</View>
</View>,
);

expect(getByTestId('view-within-invisible-view')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(
<View style={{ display: 'none' }}>
<View>
<View testID="view-within-display-none-view" />
</View>
</View>,
);

expect(getByTestId('view-within-display-none-view')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(
<Modal visible={false}>
<View>
<View testID="view-within-not-visible-modal" />
</View>
</Modal>,
);

// Children elements of not visible modals are not rendered.
expect(queryByTestId('view-within-modal')).toBeNull();
```

```javascript
const { getByTestId } = render(<Modal testID="not-visible-modal" visible={false} />);

expect(getByTestId('not-visible-modal')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(<View testID="test" accessibilityElementsHidden />);

expect(getByTestId('test')).not.toBeVisible();
```

```javascript
const { getByTestId } = render(
<View testID="test" importantForAccessibility="no-hide-descendants" />,
);

expect(getByTestId('test')).not.toBeVisible();
```

## Inspiration

This library was made to be a companion for
Expand Down
1 change: 1 addition & 0 deletions extend-expect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare global {

/** @deprecated This function has been renamed to `toBeEmptyElement`. */
toBeEmpty(): R;
toBeVisible(): R;
}
}
}
28 changes: 28 additions & 0 deletions src/__tests__/__snapshots__/to-be-visible.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`.toBeVisible throws an error when expectation is not matched 1`] = `
"expect(element).not.toBeVisible()
Received element is visible:
Object {
"props": Object {
"children": undefined,
"testID": "test",
},
}"
`;

exports[`.toBeVisible throws an error when expectation is not matched 2`] = `
"expect(element).toBeVisible()
Received element is not visible:
Object {
"props": Object {
"children": undefined,
"style": Object {
"opacity": 0,
},
"testID": "test",
},
}"
`;
135 changes: 135 additions & 0 deletions src/__tests__/to-be-visible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react';
import { Modal, View } from 'react-native';
import { render } from '@testing-library/react-native';

describe('.toBeVisible', () => {
test('handles empty view', () => {
const { getByTestId } = render(<View testID="test" />);
expect(getByTestId('test')).toBeVisible();
});

test('handles view with opacity', () => {
const { getByTestId } = render(<View testID="test" style={{ opacity: 0.2 }} />);
expect(getByTestId('test')).toBeVisible();
});

test('handles view with 0 opacity', () => {
const { getByTestId } = render(<View testID="test" style={{ opacity: 0 }} />);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles view with display "none"', () => {
const { getByTestId } = render(<View testID="test" style={{ display: 'none' }} />);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles ancestor view with 0 opacity', () => {
const { getByTestId } = render(
<View style={{ opacity: 0 }}>
<View>
<View testID="test" />
</View>
</View>,
);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles ancestor view with display "none"', () => {
const { getByTestId } = render(
<View style={{ display: 'none' }}>
<View>
<View testID="test" />
</View>
</View>,
);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles empty modal', () => {
const { getByTestId } = render(<Modal testID="test" />);
expect(getByTestId('test')).toBeVisible();
});

test('handles view within modal', () => {
const { getByTestId } = render(
<Modal>
<View>
<View testID="view-within-modal" />
</View>
</Modal>,
);
expect(getByTestId('view-within-modal')).toBeVisible();
});

test('handles view within not visible modal', () => {
const { getByTestId, queryByTestId } = render(
<Modal testID="test" visible={false}>
<View>
<View testID="view-within-modal" />
</View>
</Modal>,
);
expect(getByTestId('test')).not.toBeVisible();
// Children elements of not visible modals are not rendered.
expect(() => expect(getByTestId('view-within-modal')).not.toBeVisible()).toThrow();
expect(queryByTestId('view-within-modal')).toBeNull();
});

test('handles not visible modal', () => {
const { getByTestId } = render(<Modal testID="test" visible={false} />);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles inaccessible view (iOS)', () => {
const { getByTestId, update } = render(<View testID="test" accessibilityElementsHidden />);
expect(getByTestId('test')).not.toBeVisible();

update(<View testID="test" accessibilityElementsHidden={false} />);
expect(getByTestId('test')).toBeVisible();
});

test('handles view within inaccessible view (iOS)', () => {
const { getByTestId } = render(
<View accessibilityElementsHidden>
<View>
<View testID="test" />
</View>
</View>,
);
expect(getByTestId('test')).not.toBeVisible();
});

test('handles inaccessible view (Android)', () => {
const { getByTestId, update } = render(
<View testID="test" importantForAccessibility="no-hide-descendants" />,
);
expect(getByTestId('test')).not.toBeVisible();

update(<View testID="test" importantForAccessibility="auto" />);
expect(getByTestId('test')).toBeVisible();
});

test('handles view within inaccessible view (Android)', () => {
const { getByTestId } = render(
<View importantForAccessibility="no-hide-descendants">
<View>
<View testID="test" />
</View>
</View>,
);
expect(getByTestId('test')).not.toBeVisible();
});

it('handles non-React elements', () => {
expect(() => expect({ name: 'Non-React element' }).not.toBeVisible()).toThrow();
expect(() => expect(true).not.toBeVisible()).toThrow();
});

it('throws an error when expectation is not matched', () => {
const { getByTestId, update } = render(<View testID="test" />);
expect(() => expect(getByTestId('test')).not.toBeVisible()).toThrowErrorMatchingSnapshot();

update(<View testID="test" style={{ opacity: 0 }} />);
expect(() => expect(getByTestId('test')).toBeVisible()).toThrowErrorMatchingSnapshot();
});
});
2 changes: 2 additions & 0 deletions src/extend-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { toContainElement } from './to-contain-element';
import { toHaveProp } from './to-have-prop';
import { toHaveStyle } from './to-have-style';
import { toHaveTextContent } from './to-have-text-content';
import { toBeVisible } from './to-be-visible';

expect.extend({
toBeDisabled,
Expand All @@ -14,4 +15,5 @@ expect.extend({
toHaveProp,
toHaveStyle,
toHaveTextContent,
toBeVisible,
});
48 changes: 48 additions & 0 deletions src/to-be-visible.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Modal, StyleSheet } from 'react-native';
import { matcherHint } from 'jest-matcher-utils';
import type { ReactTestInstance } from 'react-test-renderer';

import { checkReactElement, printElement } from './utils';

function isStyleVisible(element: ReactTestInstance) {
const style = element.props.style || {};
const { display, opacity } = StyleSheet.flatten(style);
return display !== 'none' && opacity !== 0;
}

function isAttributeVisible(element: ReactTestInstance) {
return element.type !== Modal || element.props.visible !== false;
}

function isVisibleForAccessibility(element: ReactTestInstance) {
const visibleForiOSVoiceOver = !element.props.accessibilityElementsHidden;
const visibleForAndroidTalkBack =
element.props.importantForAccessibility !== 'no-hide-descendants';
return visibleForiOSVoiceOver && visibleForAndroidTalkBack;
}

function isElementVisible(element: ReactTestInstance): boolean {
return (
isStyleVisible(element) &&
isAttributeVisible(element) &&
isVisibleForAccessibility(element) &&
(!element.parent || isElementVisible(element.parent))
);
}

export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) {
checkReactElement(element, toBeVisible, this);
const isVisible = isElementVisible(element);
return {
pass: isVisible,
message: () => {
const is = isVisible ? 'is' : 'is not';
return [
matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
'',
`Received element ${is} visible:`,
printElement(element),
].join('\n');
},
};
}

0 comments on commit 1b65ce3

Please sign in to comment.