diff --git a/README.md b/README.md
index 6ad2e0d0..985ec8c5 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ to maintain.
* [`toHaveAttribute`](#tohaveattribute)
* [`toHaveClass`](#tohaveclass)
* [`toHaveStyle`](#tohavestyle)
+ * [`toBeVisible`](#tobevisible)
* [Inspiration](#inspiration)
* [Other Solutions](#other-solutions)
* [Guiding Principles](#guiding-principles)
@@ -194,6 +195,35 @@ This also works with rules that are applied to the element via a class name for
which some rules are defined in a stylesheet currently active in the document.
The usual rules of css precedence apply.
+### `toBeVisible`
+
+This allows you to check if an element is currently visible to the user.
+
+An element is visible if **all** the following conditions are met:
+
+* it does not have its css property `display` set to `none`
+* it does not have its css property `visibility` set to either `hidden` or
+ `collapse`
+* it does not have its css property `opacity` set to `0`
+* its parent element is also visible (and so on up to the top of the DOM tree)
+
+```javascript
+// add the custom expect matchers once
+import 'jest-dom/extend-expect'
+
+// ...
+//
+//
+expect(container.querySelector('header')).toBeVisible()
+expect(container.querySelector('h1')).not.toBeVisible()
+expect(container.querySelector('strong')).not.toBeVisible()
+// ...
+```
+
## Inspiration
This whole library was extracted out of Kent C. Dodds' [dom-testing-library][],
diff --git a/src/__tests__/index.js b/src/__tests__/index.js
index 3b330653..dc3e42ce 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.js
@@ -167,3 +167,35 @@ test('.toHaveStyle', () => {
document.body.removeChild(style)
document.body.removeChild(container)
})
+
+test('.toBeVisible', () => {
+ const {container} = render(`
+
+
+ Main title
+ Secondary title
+ Secondary title
+ Secondary title
+ Secondary title
+
+
+
+ `)
+
+ expect(container.querySelector('header')).toBeVisible()
+ expect(container.querySelector('h1')).not.toBeVisible()
+ expect(container.querySelector('h2')).not.toBeVisible()
+ expect(container.querySelector('h3')).not.toBeVisible()
+ expect(container.querySelector('h4')).not.toBeVisible()
+ expect(container.querySelector('h5')).toBeVisible()
+ expect(container.querySelector('strong')).not.toBeVisible()
+
+ expect(() =>
+ expect(container.querySelector('header')).not.toBeVisible(),
+ ).toThrowError()
+ expect(() =>
+ expect(container.querySelector('p')).toBeVisible(),
+ ).toThrowError()
+})
diff --git a/src/index.js b/src/index.js
index 386c19e0..6750ac4d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,6 +3,7 @@ import {toHaveTextContent} from './to-have-text-content'
import {toHaveAttribute} from './to-have-attribute'
import {toHaveClass} from './to-have-class'
import {toHaveStyle} from './to-have-style'
+import {toBeVisible} from './to-be-visible'
export {
toBeInTheDOM,
@@ -10,4 +11,5 @@ export {
toHaveAttribute,
toHaveClass,
toHaveStyle,
+ toBeVisible,
}
diff --git a/src/to-be-visible.js b/src/to-be-visible.js
new file mode 100644
index 00000000..38b67321
--- /dev/null
+++ b/src/to-be-visible.js
@@ -0,0 +1,39 @@
+import {matcherHint} from 'jest-matcher-utils'
+import {checkHtmlElement, getMessage} from './utils'
+
+function isStyleVisible(element) {
+ const {display, visibility, opacity} = getComputedStyle(element)
+ return (
+ display !== 'none' &&
+ visibility !== 'hidden' &&
+ visibility !== 'collapse' &&
+ opacity !== '0' &&
+ opacity !== 0
+ )
+}
+
+function isElementVisible(element) {
+ return (
+ isStyleVisible(element) &&
+ (!element.parentElement || isElementVisible(element.parentElement))
+ )
+}
+
+export function toBeVisible(element) {
+ checkHtmlElement(element)
+ const isVisible = isElementVisible(element)
+ return {
+ pass: isVisible,
+ message: () => {
+ const to = this.isNot ? 'not to' : 'to'
+ const is = isVisible ? 'is' : 'is not'
+ return getMessage(
+ matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
+ 'Expected',
+ `element ${to} be visible`,
+ 'Received',
+ `element ${is} visible`,
+ )
+ },
+ }
+}