cypress: .should('not.be.visible') fails when elem out of viewport

Current behavior:

cy.get().should('not.be.visible')

fails even when DOM element is not in viewport.

It seems I’m not the only one reporting this behavior.

Desired behavior:

Acc to doc, only actionable commands should autoscroll DOM to viewport.

How to reproduce:

// ensure small viewport
cy.viewport( 999, 200 );
// ensure scrollbar is disabled, for good measure (though it doesn't seem Cypress cares)
cy.window().then( window => {
    window.$("body").css("overflow-y", "hidden");
});
// manually test for whether elem is out of viewport -- PASSES
cy.get(".elem").first().then( $el => {

    const bottom = Cypress.$( cy.state("window") ).height();
    const rect = $el[0].getBoundingClientRect();

    expect( rect.top ).to.be.greaterThan( bottom );
    expect( rect.bottom ).to.be.greaterThan( bottom );
    expect( rect.top ).to.be.greaterThan( bottom );
    expect( rect.bottom ).to.be.greaterThan( bottom );
});
// FAILS
cy.get(".elem").first().should("not.be.visible");
  • Operating System: win7x64
  • Cypress Version: 1.0.3

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 20 (6 by maintainers)

Commits related to this issue

Most upvoted comments

For readers in the future, you can add custom commands in cypress/support/commands.js like so:

Cypress.Commands.add('isNotInViewport', element => {
  cy.get(element).then($el => {
    const bottom = Cypress.$(cy.state('window')).height()
    const rect = $el[0].getBoundingClientRect()

    expect(rect.top).to.be.greaterThan(bottom)
    expect(rect.bottom).to.be.greaterThan(bottom)
    expect(rect.top).to.be.greaterThan(bottom)
    expect(rect.bottom).to.be.greaterThan(bottom)
  })
})

Cypress.Commands.add('isInViewport', element => {
  cy.get(element).then($el => {
    const bottom = Cypress.$(cy.state('window')).height()
    const rect = $el[0].getBoundingClientRect()

    expect(rect.top).not.to.be.greaterThan(bottom)
    expect(rect.bottom).not.to.be.greaterThan(bottom)
    expect(rect.top).not.to.be.greaterThan(bottom)
    expect(rect.bottom).not.to.be.greaterThan(bottom)
  })
})

and then in your tests use it like so:

    cy.isNotInViewport('[data-cy=some-invisible-element]')
    cy.isInViewport('[data-cy=some-visible-element]')

Inspired by @Whassup, I rewrote the command as an assertion. Simply paste the following into a cypress/support/assertions.js file and do import './assertions'; in your cupress/support/index.js file.

const isInViewport = (_chai, utils) => {
  function assertIsInViewport(options) {

    const subject = this._obj;

    const bottom = Cypress.$(cy.state('window')).height();
    const rect = subject[0].getBoundingClientRect();

    this.assert(
      rect.top < bottom && rect.bottom < bottom,
      "expected #{this} to be in viewport",
      "expected #{this} to not be in viewport",
      this._obj
    )
  }

  _chai.Assertion.addMethod('inViewport', assertIsInViewport)
};

chai.use(isInViewport);

Usage:

cy.get("button").should("be.inViewport");

For readers in the future, you can add custom commands in cypress/support/commands.js like so:

Cypress.Commands.add('isNotInViewport', element => {
  cy.get(element).then($el => {
    const bottom = Cypress.$(cy.state('window')).height()
    const rect = $el[0].getBoundingClientRect()

    expect(rect.top).to.be.greaterThan(bottom)
    expect(rect.bottom).to.be.greaterThan(bottom)
    expect(rect.top).to.be.greaterThan(bottom)
    expect(rect.bottom).to.be.greaterThan(bottom)
  })
})

Cypress.Commands.add('isInViewport', element => {
  cy.get(element).then($el => {
    const bottom = Cypress.$(cy.state('window')).height()
    const rect = $el[0].getBoundingClientRect()

    expect(rect.top).not.to.be.greaterThan(bottom)
    expect(rect.bottom).not.to.be.greaterThan(bottom)
    expect(rect.top).not.to.be.greaterThan(bottom)
    expect(rect.bottom).not.to.be.greaterThan(bottom)
  })
})

and then in your tests use it like so:

    cy.isNotInViewport('[data-cy=some-invisible-element]')
    cy.isInViewport('[data-cy=some-visible-element]')

In my particular use case, the assertion fails since the element comes into view after a smooth scroll. As a solution, I replace .then with .should (after cy.get(element)) to allow retries.

Thanks @thomaseizinger

For my use case I changed the behavior so that also the current scroll-position is taken into account and the element is seen as in viewport, as long as it could partly be seen. Either the rect.top or rect.bottom value must be in the viewport.

const isInViewport = (_chai, utils) => {
	function assertIsInViewport(options) {
		const subject = this._obj

		const windowHeight = Cypress.$(cy.state('window')).height()
		const bottomOfCurrentViewport = windowHeight
		const rect = subject[0].getBoundingClientRect()

		this.assert(
			(rect.top > 0 && rect.top < bottomOfCurrentViewport) ||
				(rect.bottom > 0 && rect.bottom < bottomOfCurrentViewport),
			'expected #{this} to be in viewport',
			'expected #{this} to not be in viewport',
			subject,
		)
	}

	_chai.Assertion.addMethod('inViewport', assertIsInViewport)
}

chai.use(isInViewport)

@shreyansqt Thanks for your code

Updated it to be chain-able

//e.g
cy.get('button').isInViewPort().click()

// Command
Cypress.Commands.add('isInViewport', { prevSubject: true },(subject) => {
    const bottom = Cypress.$(cy.state('window')).height();
    const rect = subject[0].getBoundingClientRect();

    expect(rect.top).not.to.be.greaterThan(bottom);
    expect(rect.bottom).not.to.be.greaterThan(bottom);

    return subject;
});

I’m relatively new to Cypress and have tried to implement some of the suggestions above with regards to creating a new command in commands.ts

However, I am having problems with cy.state, I get the below error message and I am having difficultly trying to find a fix for it, can anyone help? ‘Property ‘state’ does not exist on type ‘cy & EventEmitter’’. All of the solutions seem to use cy.state so I am trying to figure out to fix this, any help much appreciated!

e.g. Cypress.Commands.add(‘isInViewport’, { prevSubject: true },(subject) => { const bottom = Cypress.$(cy.state(‘window’)).height(); const rect = subject[0].getBoundingClientRect();

expect(rect.top).not.to.be.greaterThan(bottom); expect(rect.bottom).not.to.be.greaterThan(bottom);

return subject; });

Inspired by previous very helpful commands I wrote a command which you can use to check position of element in all directions.

My case was testing a “caroussel” on mobile where you can slide through options left or right. Needed to verify it was not placing the options in a vertical list. However, the elements would be visible for a few pixels on the edges. That’s why my command checks the position of the element’s center in relation to viewport instead of the edges.

// Positions: inside, above, below, left, right
cy.get('center').positionToViewport('inside').click()
cy.get('left').positionToViewport('left')
cy.get('below').positionToViewport('below')

// Command
Cypress.Commands.add('positionToViewport', { prevSubject: true }, (element, position) => {
    cy.get(element).should($el => {
        const height = Cypress.$(cy.state('window')).height()
        const width = Cypress.$(cy.state('window')).width()
        const rect = $el[0].getBoundingClientRect()

        if(position == 'inside'){
            expect((rect.top + (rect.height/2)), 'element center not above viewport').to.be.greaterThan(0)
            expect((rect.top + (rect.height/2)), 'element center not below viewport').to.be.lessThan(height)
            expect((rect.left + (rect.width/2)), 'element center not left of viewport').to.be.greaterThan(0)
            expect((rect.left, + (rect.width/2)), 'element center not right of viewport').to.be.lessThan(width)
        }else if(position == 'above'){
            expect((rect.top + (rect.height/2)), 'element center above viewport').to.be.lessThan(0)
        }else if(position == 'below'){
            expect((rect.top + (rect.height/2)), 'element center below viewport').to.be.greaterThan(height)
        }else if(position == 'left'){
            expect((rect.left + (rect.width/2)), 'element center left of viewport').to.be.lessThan(0)
        }else if(position == 'right'){
            expect((rect.left + (rect.width/2)), 'element center right of viewport').to.be.greaterThan(width)
        }
    })
})

Any comments or improvements are welcome ofcourse.

For anyone that needs a command to check that something is not in the viewport with horizontal and vertical checks, I use this,

/**
 * A custom command to check whether the element is not visible within the viewport.
 */
Cypress.Commands.add('isNotInViewport', (element) => {
  cy.get(element).should(($el) => {
    const bottom = Cypress.$(cy.state('window')).height();
    const right = Cypress.$(cy.state('window')).width();
    const rect = $el[0].getBoundingClientRect();

    expect(rect).to.satisfy((rect) => rect.top < 0 || rect.top > bottom || rect.left < 0 || rect.left > right);
  });

A potential solution for Cypress 8+

function isInViewport(el) {
  cy.get(el)
    .then($el => {
      cy.window().then(window => {
        const { documentElement } = window.document;
        const bottom = documentElement.clientHeight;
        const right = documentElement.clientWidth;
        const rect = $el[0].getBoundingClientRect();
        expect(rect.top).to.be.lessThan(bottom);
        expect(rect.bottom).to.be.greaterThan(0);
        expect(rect.right).to.be.greaterThan(0);
        expect(rect.left).to.be.lessThan(right);
      });
    });
}

Yes, what @brian-mann explains above is actually true. The example in the kitchen sink works because we take into account elements being clipped by a parent container, but we don’t take into account the viewport size when calculating visibility. It’s complicated logic.

What you are doing above is essentially what you should continue doing, writing the code to manually check if the element is visible within the viewport.