Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.should('not.be.visible') fails when elem out of viewport #877

Closed
dwelle opened this issue Nov 6, 2017 · 20 comments
Closed

.should('not.be.visible') fails when elem out of viewport #877

dwelle opened this issue Nov 6, 2017 · 20 comments

Comments

@dwelle
Copy link

dwelle commented Nov 6, 2017

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
@jennifer-shehane jennifer-shehane added stage: needs investigating Someone from Cypress needs to look at this topic: visibility 👁 labels Nov 6, 2017
@brian-mann
Copy link
Member

brian-mann commented Nov 6, 2017

I don't believe this is a bug. We wrote the visibility calculations to take into account elements outside of the viewport.

What I mean is - an element is considered visible if the user in could in any way interact with it - even if they needed to scroll to it.

The reason this rule has to be in place is because scrolling is a mutation. If Cypress first attempted to scroll elements on every single be.visible assertion it could have dramatic side effects that can cause all kinds of problems.

Visibility is simply - is the element capable of being seen by the user? Yes? Visible.

@jennifer-shehane
Copy link
Member

This is the visibility logic in our code, in case you want to investigate before we are able to: https://github.com/cypress-io/cypress/blob/code-of-conduct/packages/driver/src/dom/visibility.coffee#L17

@jennifer-shehane
Copy link
Member

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.

@brian-mann
Copy link
Member

There are a few exceptions to the rules I listed above. If the element could be clipped by a parent in any capacity, then we check to see if this is currently the case.

In those situations you may need to scroll an element into view first before asserting on its visibility.

@brian-mann
Copy link
Member

Right. Imagine a scenario where this wasn't the case.

You would virtually always need to first scrollIntoView prior to all assertions because controlling the users viewport is needlessly complex.

The vast majority of the time, probably in the 99% percentile range - all you care about is whether or not the element could in fact be seen by the user in some natural capacity.

@dwelle
Copy link
Author

dwelle commented Nov 6, 2017

Yea, that's what I've thought and been relying on (current behavior).. I just misunderstood @jennifer-shehane in the chat and thought it wasn't a feature to begin with.

What's puzzling, is why the test code works at all, considering that visible assertion, taken from chai-jquery#visible should just be $.fn.is(':visible'), which doesn't take element's viewport/boundingBox into account at all. Is it because you're re-implemented the chai assertion (I think you mentioned that somewhere) and diverged from what the assertion does?

@brian-mann
Copy link
Member

brian-mann commented Nov 6, 2017

Correct. We diverged it completely and use our own algorithm. Then we stiched together the assertion to be our definition of visibility.

https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/config/jquery.coffee#L8

@dwelle
Copy link
Author

dwelle commented Nov 6, 2017

Yea, maybe assertions doc update is in order (maybe you've already mentioned it's planned, don't remember).

@AElmoznino
Copy link

AElmoznino commented May 8, 2019

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]')

@shreyansqt
Copy link

shreyansqt commented Jun 7, 2019

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.

@Whassup
Copy link

Whassup commented Jul 9, 2019

@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;
});

@ZijlkerVR
Copy link

ZijlkerVR commented Jul 16, 2019

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.

@thomaseizinger
Copy link

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");

@crymis
Copy link

crymis commented Jul 13, 2020

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)

@ryan-rushton
Copy link

ryan-rushton commented Jan 19, 2021

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);
  });

winsvold added a commit to navikt/dp-faktasider-frontend that referenced this issue Apr 30, 2021
…isse testene for nå. Har ikke hatt noen caser hvor slike tester hadde ordnet noe uansett, så tenker det ikke er verdt å bruke mere tid på det nå.

prøvde å  å sette opp custom comands eller assertions som beskrevet her: cypress-io/cypress#877
@jordyvandomselaar
Copy link

jordyvandomselaar commented Jul 13, 2021

I've combined a few answers given here:

Cypress.Commands.add("isNotInViewport", { prevSubject: true }, (element) => {
  const message = `Did not expect to find ${element[0].outerHTML} in viewport`;

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

    expect(rect.top).to.be.greaterThan(bottom, message);
    expect(rect.bottom).to.be.greaterThan(bottom, message);
    expect(rect.top).to.be.greaterThan(bottom, message);
    expect(rect.bottom).to.be.greaterThan(bottom, message);
  });
});

Cypress.Commands.add("isInViewport", { prevSubject: true }, (element) => {
  const message = `Expected to find ${element[0].outerHTML} in viewport`;

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

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

It's chainable, uses should() so it automatically retries and shows a nicer message upon failure.

Did not expect to find <strong>Week 37</strong> in viewport: expected 1336.09375 to be above 660

instead of

expected 1336.09375 to be above 660

@kezbeynon
Copy link

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;
});

@Rorymercer
Copy link

I was struggling with getting the above to work when the viewport had been scrolled - to test if an element had been scrolled to or not. I got this to give me my desired behaviour accounting for scrolling

/cypress/support.index.js

Cypress.Commands.add("isScrolledTo", { prevSubject: true }, (element) => {
    cy.get(element).should(($el) => {
        const bottom = Cypress.$(cy.state("window")).height();
        const rect = $el[0].getBoundingClientRect();

        expect(rect.top).not.to.be.greaterThan(bottom, `Expected element not to be below the visible scrolled area`);
        expect(rect.top).to.be.greaterThan(0 - rect.height, `Expected element not to be above the visible scrolled area`)
    });
});

In tests:

cy.get('#payment-calculator').isScrolledTo()

@dvnrsn
Copy link

dvnrsn commented Nov 29, 2021

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);
      });
    });
}

@d4niloArantes
Copy link

I needed to be able to set timeout and retries, like any other cypress command so this is my custom command:

Cypress.Commands.add('waitUntilIsNotInViewport', { prevSubject: true },
 (subject, timeout = Cypress.config('defaultCommandTimeout')) => {
    const height = Cypress.$(cy.state('window')).height();
    const width = Cypress.$(cy.state('window')).width();

    return cy.wrap(subject, { timeout }).should($el => {
      const rect = $el[0].getBoundingClientRect();
      const notVisibleVertically = rect.bottom < 0 || rect.top > height;
      const notVisibleHorizontally = rect.right < 0 || rect.left > width;

      expect(notVisibleVertically || notVisibleHorizontally,
        'the element should be outside the viewport').to.be.true;
    });
  }
);

@cypress-io cypress-io locked and limited conversation to collaborators Nov 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests