How to write end-to-end tests for the loading skeletons.
Loading skeletons are displayed while the real data is loading. For example, the login passwords are displayed after 1 second in the GIF below, and the loading skeleton is displayed first.
The skeleton itself is simple, just a DIV with some gradient CSS.
Let's confirm the loading skeleton is shown initially and then replaced by the real component. If the loading skeleton is always displayed, we can write a test similar to this one:
describe('Login form skeleton', () => { // visit the login page before each test beforeEach(() => { cy.visit('/') })
it('shows the loading skeleton first', () => { cy.get('#login_button_container').should('be.visible') cy.get('.skeleton').should('be.visible').and('have.length.greaterThan', 2) // skeleton should go away // and the login form is immediately visible (within 100ms) cy.get('.skeleton').should('not.exist') cy.get(LoginPage.selectors.username, { timeout: 100 }).should('be.visible') }) })
Tip: if the skeleton does NOT always appear (for example, if the data is cached and the skeleton is skipped), we can clear the data and/or slow down the network request to make the skeleton always appear.
Test the skeleton positioning
One of the disturbing aspects of the loading skeleton is mismatch between its positions and the loaded text. For example, take a look at this loop - can you see the "jump" between the skeleton heading DIV and the "Accepted usernames are:" H4?
We want to catch this skeleton position mismatch. Let's compare the "top" property of the skeleton DIV and the rendered H4 elements - they should be close to each other in order to avoid disorienting the user. We could write a test like this:
// visit the login page before each test beforeEach(() => { cy.visit('/') })
it('does not move from the top', () => { // confirm the skeleton heading does NOT change // it "top" position on the page too much (within tolerance) cy.get(skeletonHeading) .should('be.visible') .then(($el) => { const rect = $el[0].getBoundingClientRect() return rect.top }) // round to pixels for nicer comparison .then(Math.round) .as('initialTop', { type: 'static' }) cy.get(skeletonHeading).should('not.exist')
The test catches the heading skeleton "moving" 100 pixels when the real heading H4 element is shown.
Because I love writing concise and elegant code, I will use my cypress-map plugin to rewrite the above test a little bit to make it shorter and easier to read.
it('does not move from the top (cypress-map)', () => { // confirm the skeleton heading does NOT change // it "top" position on the page too much (within tolerance) cy.get(skeletonHeading) .should('be.visible') .invokeFirst('getBoundingClientRect') .its('top') // round to pixels for nicer comparison .then(Math.round) .as('initialTop', { type: 'static' }) cy.get(skeletonHeading).should('not.exist')
Now let's fix the skeleton "jump", I found the following problem in the CSS
1 2 3 4 5 6 7
.skeleton-heading { /* why are we shifting the skeleton heading by 100 pixels?!! */ position: relative; top: 100px; height: 24px; width: 60%; }
Let's remove the position: relative and top: 100px from the skeleton's heading CSS. The test is now green - the skeleton heading is pretty close to where the real H4 appears.
We can similarly check other loading skeleton dimensions: left, bottom, and right. We can also check other skeleton DIV elements, not just the heading.