javascripttestingcypressviewportend-to-end

Verify Element Is Within Viewport With Cypress


Cypress's visible matcher treats an element as visible based on a variety of factors, however it doesn't take the viewport into account, so an element that is scrolled off-screen is still treated as visible.

I need to test that a link to an on-page anchor is functioning correctly. Once the link is clicked, the page scrolls to the element with the id as defined in the href of the link (example/#some-id).

How can verify that the element is within the viewport?


Solution

  • The solution from @Undistraction worked at time of writing, but since then Cypress has separated Commands and Queries.

    Now, custom commands no longer have the ability to retry. In the current version 13.5.0 you should convert custom commands to custom queries to get retry.

    All that's needed is to return a synchronous inner function that gets called on every retry.

    Additionally, you can move invariant factors (such as the viewport dimensions) outside the innerFn to make things a bit quicker.

    This is the conversion for the main answer above.

    Cypress.Commands.addQuery('isWithinViewport', () => {
    
      const viewportWidth = Cypress.config('viewportWidth')     // see "bug" below
      const viewportHeight = Cypress.config('viewportHeight')
    
      const innerFn = (subject) => {   // Cypress retries this function on failure
    
        const {top, left, bottom, right} = subject[0].getBoundingClientRect();
        expect(top).to.be.at.least(0);
        expect(left).to.be.at.least(0);
        expect(right).to.be.at.most(viewportWidth);
        expect(bottom).to.be.at.most(viewportHeight);
      }
      return innerFn;
    })
    

    Testing

    To test it, I loaded https://example.com and used a very small viewport.

    Then scrolled to the bottom, to make the <h1> heading top-left outside the viewport.

    After a delay, scrolled back to the top.

    it('waits for the H1 to come into viewport', () => {
      cy.viewport(200,200)
      cy.visit('https://example.com');
    
      cy.scrollToBottomThenToTop()  // simulate delayed viewport changes
    
      cy.get('h1').isWithinViewport()
    })
    

    The custom command to scroll after delay:

    Cypress.Commands.add('scrollToBottomThenToTop', () => {
      cy.get('html', {log:false})
        .then($html => {
          Cypress.log({displayName: 'scrolling to: ', message: 'bottom'})
          $html[0].scrollTo({top: 100, left: 100, behavior: "instant"})
          setTimeout(() => {
            Cypress.log({displayName: 'scrolling to: ', message: 'top'})
            $html[0].scrollTo({top: 0, left: 0, behavior: "instant"})
          }, 200)
        })
    })
    

    In the Cypress log, you can see the query initially fails, then passes after the element scroll into view.

    enter image description here


    A bug in cy.viewport()?

    Note that Cypress.config('viewportWidth') and Cypress.config('viewportHeight') do not return values set per-test using cy.viewport(), but rather the globally configured settings.

    To get around this, I added an overwrite for the cy.viewport() command:

    Cypress.Commands.overwrite('viewport', (originalFn, width, height) => {
      Cypress.config('viewportWidth', width)
      Cypress.config('viewportHeight', height)
      return originalFn(width, height)
    })