,
+ `)
+ expect(container.querySelector('button')).toBeVisible()
+ expect(() =>
+ expect(container.querySelector('button')).not.toBeVisible(),
+ ).toThrowError()
+ })
+
+ it('does not allow child elements to override invisibility by increasing their own opacity', () => {
+ const {container} = render(`
+
+
+
+ `)
+ expect(container.querySelector('button')).not.toBeVisible()
+ expect(() =>
+ expect(container.querySelector('button')).toBeVisible(),
+ ).toThrowError()
+ })
+ })
+
+ describe('detached element', () => {
+ it('is not visible', () => {
+ const subject = document.createElement('div')
+ expect(subject).not.toBeVisible()
+ expect(() => {
+ expect(subject).toBeVisible()
+ }).toThrowError()
+ })
})
describe('with a element', () => {
diff --git a/src/to-be-visible.js b/src/to-be-visible.js
index 518b7d56..12786a6a 100644
--- a/src/to-be-visible.js
+++ b/src/to-be-visible.js
@@ -1,33 +1,63 @@
import {checkHtmlElement} from './utils'
-function isStyleVisible(element) {
+function getElementVisibilityStyle(element) {
+ if (!element) return 'visible'
const {getComputedStyle} = element.ownerDocument.defaultView
+ const {visibility} = getComputedStyle(element)
+ return visibility || getElementVisibilityStyle(element.parentElement)
+}
- const {display, visibility, opacity} = getComputedStyle(element)
- return (
- display !== 'none' &&
- visibility !== 'hidden' &&
- visibility !== 'collapse' &&
- opacity !== '0' &&
- opacity !== 0
- )
+function isVisibleSummaryDetails(element, previousElement) {
+ return element.nodeName === 'DETAILS' &&
+ previousElement.nodeName !== 'SUMMARY'
+ ? element.hasAttribute('open')
+ : true
}
-function isAttributeVisible(element, previousElement) {
+function isElementTreeVisible(element, previousElement = undefined) {
+ const {getComputedStyle} = element.ownerDocument.defaultView
+ const {display, opacity} = getComputedStyle(element)
return (
+ display !== 'none' &&
+ opacity !== '0' &&
!element.hasAttribute('hidden') &&
- (element.nodeName === 'DETAILS' && previousElement.nodeName !== 'SUMMARY'
- ? element.hasAttribute('open')
+ isVisibleSummaryDetails(element, previousElement) &&
+ (element.parentElement
+ ? isElementTreeVisible(element.parentElement, element)
: true)
)
}
-function isElementVisible(element, previousElement) {
- return (
- isStyleVisible(element) &&
- isAttributeVisible(element, previousElement) &&
- (!element.parentElement || isElementVisible(element.parentElement, element))
- )
+function isElementVisibilityVisible(element) {
+ const visibility = getElementVisibilityStyle(element)
+ return visibility !== 'hidden' && visibility !== 'collapse'
+}
+
+/**
+ * Computes the boolean value that determines if an element is considered visible from the
+ * `toBeVisible` custom matcher point of view.
+ *
+ * Visibility is controlled via two different sets of properties and styles.
+ *
+ * 1. One set of properties allow parent elements to fully controls its sub-tree visibility. This
+ * means that if higher up in the tree some element is not visible by this criteria, it makes the
+ * entire sub-tree not visible too, and there's nothing that child elements can do to revert it.
+ * This includes `display: none`, `opacity: 0`, the presence of the `hidden` attribute`, and the
+ * open state of a details/summary elements pair.
+ *
+ * 2. The other aspect influencing if an element is visible is the CSS `visibility` style. This one
+ * is also inherited. But unlike the previous case, this one can be reverted by child elements.
+ * A parent element can set its visibiilty to `hidden` or `collapse`, but a child element setting
+ * its own styles to `visibility: visible` can rever that, and it makes itself visible. Hence,
+ * this criteria needs to be checked independently of the other one.
+ *
+ * Hence, the apprach taken by this function is two-fold: it first gets the first set of criteria
+ * out of the way, analyzing the target element and up its tree. If this branch yields that the
+ * element is not visible, there's nothing the element could be doing to revert that, so it returns
+ * false. Only if the first check is true, if proceeds to analyze the `visibility` CSS.
+ */
+function isElementVisible(element) {
+ return isElementTreeVisible(element) && isElementVisibilityVisible(element)
}
export function toBeVisible(element) {