Elements Visible In The Current Viewport

How to check if the loading elements disappeared in the current viewport without checking the outside ones.

Imagine your application loads in stages and shows several loading elements. There are two loading elements visible to the user right away (above the fold), and one more loading element below the fold. Here is a sample page showing this situation:

1
2
3
4
5
6
7
8
9
<body>
<section id="page1">
<div class="loading"></div>
<div class="loading"></div>
</section>
<section id="page2">
<div class="loading"></div>
</section>
</body>

The #page1 and #page2 sections are marked with different colors. Each is 1000x1000 pixels and if we zoom out look like this:

Two sections with three loading elements

Tip: I set the viewport width and height as 1000 pixels in the cypress.json file. To show the two sections at once, I modified the viewport height using per-test configuration:

1
2
3
it('loads', { viewportHeight: 2000 }, () => {
cy.visit('public/index.html')
})

🎁 You can find the source code and the tests shown in this bog post in the repo bahmutov/loading-elements.

Video: I have recorded a short video showing how to check the loading elements in the current viewport. You can watch the video below

The application "loads" and removes the first two loading elements.

public/app.js
1
2
3
4
5
6
7
8
9
10
11
const [loading1, loading2] = document.querySelectorAll('#page1 .loading')

setTimeout(() => {
console.log('hiding the first loading element')
loading1.style.display = 'none'
}, 2000)

setTimeout(() => {
console.log('removing the second loading element')
loading2.parentNode.removeChild(loading2)
}, 3000)

If we want to check if the loading elements above the fold disappear, we cannot just check the visibility - because it will include the third loading element that is still visible, even if the user cannot see it without scrolling.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
it('the loading element below the fold never goes away', () => {
cy.visit('public/index.html')
// at first, all loading elements are visible
cy.get('.loading').should('have.length', 3).and('be.visible')
// the loaders on the first page disappear
// but the loader on the second page is still visible
// so the next assertion fails
cy.get('.loading').should('not.be.visible')
})

The test fails, as the third loading element remains visible below the fold

How do we check if the loading elements inside the current viewport are no longer visible? By using the bounding rectangle of the elements before checking them! See the documentation for the Element.getBoundingClientRect which returns the rectangle in the current viewport. I took this chart from the documentation page:

The element bounding box, source: Mozilla Developer docs

If the bottom of the element is less than zero, then the element is above the current viewport. If the top of the element is larger than the viewport height, then the element is still below the current viewport. Similarly, we can check the Let me write a custom command to repeatedly check the DOM until the elements in the current viewport become hidden.

cypress/integration/support.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// <reference types="cypress" />

Cypress.Commands.add('invisibleInViewport', (selector) => {
cy.window({ log: false }).then((win) => {
// get the current viewport of the application
const { innerHeight, innerWidth } = win
cy.log(JSON.stringify({ innerHeight, innerWidth }))

cy.get(selector).should(($el) => {
$el.each((k, el) => {
// skip stray and hidden elements
if (!Cypress.dom.isAttached(el)) {
return
}
if (!Cypress.dom.isVisible(el)) {
return
}

// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
const rect = el.getBoundingClientRect()

if (rect.bottom < 0 || rect.top > innerHeight) {
// the element is outside the viewport vertically
return
}
if (rect.right < 0 || rect.left > innerWidth) {
// the element is outside the viewport horizontally
return
}

throw new Error(`loader ${k + 1} is visible`)
})
})
})

cy.log(`${selector} is invisible in viewport`)
})

Let's make our application even more complicated. We will hide / remove all loading elements one by one after 2, 3 and 4.5 seconds.

public/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const [loading1, loading2] = document.querySelectorAll('#page1 .loading')
const loading3 = document.querySelector('#page2 .loading')

setTimeout(() => {
console.log('hiding the first loading element')
loading1.style.display = 'none'
}, 2000)

setTimeout(() => {
console.log('removing the second loading element')
loading2.parentNode.removeChild(loading2)
}, 3000)

setTimeout(() => {
console.log('removing the third loading element')
loading3.parentNode.removeChild(loading3)
}, 4500)

Our test will wait for the above the folder loading elements to disappear before scrolling to the bottom of the page and verifying the last loading element is also gone from the view.

cypress/integration/spec2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
it('checks if the loading element is visible within the current viewport', () => {
cy.visit('public/index.html')

// at first, both loading elements are visible
cy.get('.loading').should('have.length', 2).and('be.visible')
cy.invisibleInViewport('.loading')

cy.scrollTo('bottom', { duration: 500 })
// there is one more loading element visible here
cy.get('.loading:visible').should('have.length', 1)
// then the last loading element goes away
cy.invisibleInViewport('.loading')
})

The above test works beautifully.

The test correctly looks at the loading elements in the current viewport

Happy Testing!