Checking element visibility might seem simple, “It’s just on the page!”. However, there are several issues with visibility checks.
Why do we even need visibility checks?
While it may seem trivial, let’s discuss why visibility checks are needed. You might want to check that something is visible or invisible before performing an action (an explicit wait-for), for example checking that the “user preferences” button is invisible if the user is not logged in. You also may want to verify that something is visible or invisible after performing an action, such as seeing an item appears in your cart after adding it or observing the dismissal of an error message after clicking on an “x” button (you’d be surprised how many times people check that no errors were thrown without actually validating that the correct behavior happened).
So why doesn’t it work well?
While the term visibility check appears clear, it’s actually quite complex. A simple definition would be, “it’s visible when I can see it!” However, what does that mean? Is it visible when it’s behind an element with opacity 0? What about opacity 0.2 or 0.5? Is it visible when only half of the element is on the screen, or a third, or two thirds? An element with a visibility:hidden style is still part of the layout, so would you consider it visible? What about elements that are actually invisible and use some CSS magic to make them appear visible. For example, material-ui checkboxes and radio button inputs are invisible (have opacity 0), and there are actually styled elements that are not the input themselves that are displayed. Another challenge is how to handle iframes where one obscures the other (an iframe doesn’t even know that it’s completely hidden).
The WebDriver specification defines some approximation of a visibility check when trying to interact with an element (e.g. click), which always happens when you interact with an element using WebDriver (i.e. Selenium). In essence, it defines that an element is visible (not obscured) if it can be clicked in the center of the element. This means that even an element that is visible but has pointer-events:none, is obscured (thus cannot be clicked). Moreover, the WebDriver specification itself recommends to use the Selenium definition, but in-itself only acknowledges that their recommendation “will give a simplified approximation of an element’s visibility, but please note that it relies only on tree-traversal, and only covers a subset of visibility checks”.
How does it work in Selenium and other Automation frameworks?
Selenium has a method called isShown. Except for some edge cases (for example, the body is always visible, input=hidden are always hidden, elements in overflow and other rules) the algorithm checks that the element has height and width greater than 0px (by default, also non-zero opacity), that its visibility is not “hidden” and that its display property is not “none”. There are also some additional rules for Shadow DOMs. In addition, the WebDriver specification does enforce some kind of visibility check (“interactability test”).
Selenium’s visibility check has multiple issues. It will return that an element is visible even if the element is not in the viewport. In addition, even if it is in the viewport, Selenium does not check if the element itself is obscured, for example by a modal dialog, and It also fails to check visibility regarding outer frames.
Other frameworks also have different definitions. Puppeteer considers an element visible if it, or one of its parents, does not have display:none, it does not have visibility:hidden, and it has a positive width, height, top or left. Playwright has a similar check, except that it enforces positive width and height. Cypress is similar to Playwright, and In addition, also checks that position:fixed elements coordinates are in the screen, or not covered up. They also have a few other checks for overflow. In addition, Puppeteer also has the “isIntersectingViewport” method, which checks that an element is inside the viewport, using an IntersectionObserver, which checks that the element has a non-zero intersection with the screen and Playwright has the “scrollIntoViewIfNeeded” method, which scrolls the element into view if it doesn’t have an intersection ratio of 1.0 (again, using an IntersectionObserver).
Testim’s visibility check
Testim’s visibility check uses a mixture of both Selenium’s visibility check and the WebDriver’s interactability implementation. It checks if the frames from the top of the page to the relevant frame are visible (i.e. if the iframe that contains the element is hidden then the element is not visible), something that Selenium does not do, and tries to check that either the middle of the element or the top-left of the element are interactable (this means that obscured elements are not considered visible, thus normalizing the check).
To be more exact, in some cases, Testim remembers the location that works. E.g. if the action is a click, and the click was recorded, Testim remembers the location of the element and uses those coordinates. This helps cases that the element is half-hidden (center is hidden), but was still interactable
In addition, it calculates the opacity (which is a multiplicity of all of the DOM tree’s opacity, similar to how Selenium uses the DOM tree) and compares it to the element’s opacity when the test was recorded. If the opacity is reduced drastically compared to the original, then the element is considered invisible. Note that this means that an element had opacity 0 when the test was recorded, it is considered visible when the test is executed even if it has opacity 0. For example, the input element for Angular Material UI checkboxes and radio buttons have opacity 0 but can still be interacted with, as they put the invisible element above the element that only looks like a checkbox (using z-index). Thus, the user actually clicks on an invisible input.
Summary
All in all visibility checks are much more complicated than they seem upfront. Different frameworks have different definitions. Frames, Shadow DOM, partially obscured elements or elements behind overlays create a complex and unexpected challenge for users and framework writers alike. At Testim, we try to provide the best of all worlds, creating a more consistent visibility check that is closer to what users expect, and with fewer issues when running your CI pipeline.