cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
47.07k stars 3.19k forks source link

cy.click() failed because element is effective width and height of 0x0, even though it is not. #695

Closed peterklijn closed 4 years ago

peterklijn commented 7 years ago

Hi there,

I'm encountering some flaky tests due to an element not being considered visible by Cypress, I don't understand what makes an element visible or not visible by Cypress and was hoping someone could clarify it for me.

Since version 0.20 tests randomly decide to fail with this message: This element '<a.sc-cqpYsc.cmkxre>' is not visible because it has an effective width and height of: '0 x 0' pixels.. However, when I inspect it is definitely has a size bigger then 0 by 0, as you can see in the screenshots.

Failing test due to click timeout: screen shot 2017-09-29 at 11 44 37

Item highlighted by Cypress: screen shot 2017-09-29 at 11 33 11

Chrome inspect of the element: screen shot 2017-09-29 at 11 34 22

Now, I can "fix" this by adding { force: true } to the click, but I would like to understand why this results in flaky behaviour (both headless and using the cypress UI).


Is this a Feature or Bug?

Bug?

Current behavior:

cy.click() failed because element is not visible

brian-mann commented 7 years ago

Much of the algorithm used to determine visibility is documented here:

https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability

Your issue is very clear which is helpful, thank you. I'm a bit confused when you say flaky though. Flake generally refers to situations where a test sometimes passes or sometimes fails.

It seems that you're saying the test always fails which would not indicate a flaky test - you've just found a bug in our visibility algorithm :-P

While you've done an excellent job giving us a ton of great information, the problem is that there is more than just the styles of this <a> causing this problem.

Is there any way you would be able to remove all of the excess HTML (or maybe even just do a File -> Save Page As) so you can export the entire DOM including the styles. From there we should be able to internally reproduce this and get it resolved.

Also if this is a public site (or even a private site we could log into) we'd be happy to take a look at this.

We've had a few users complain about this exact scenario - where Cypress considers an element hidden because its effective width and height is 0, but we've been unable to reproduce any situation to trigger that. We have several hundred tests around visibility (and they all pass) so we're going to need help creating the scenario where this happens.

peterklijn commented 7 years ago

Thanks for responding, I haven't been clear about that, sorry. The test does indeed sometimes passes, and sometimes not. Locally it passes roughly 95% of the time, on our CI environment it passes roughly 10% of the time.

I've done some testing and I'm pretty certain it has nothing to do with the styling of the a tag, but with API calls happening while the .click() is being triggered.

Our app currently does a lot of api calls on page load (something we should improve upon :)), and I managed to get the tests more stable by doing the following:

cy.route('/api/order/retailer-orders**').as('getRetailerOrders');
    cy.route('api/connection/connections').as('getConnections');
    cy.route('/api/connection/relations').as('getRelations');
    cy.route('/api/order/basket').as('getBasket');
    cy.route('/api/order/retailer-orders/*').as('getRetailerOrder');
    cy.route('/api/brand/brands/*').as('getBrand');

    // .. some other checks ..

    cy
      .wait('@getRetailerOrders')
      .wait('@getConnections')
      .wait('@getRelations')
      .wait('@getBasket')
      .wait('@getRetailerOrder')
      .wait('@getBrand');

  cy.get('a[href="/styles"]').click(); // <--- This is where it sometimes fails.

Now it's a bit early to call the test "stable" but it at least a lot more stable.

About your other question, I'm going to try and isolate this issue by making a simpler app, but that will take some time.

brian-mann commented 7 years ago

Could any of those XHR's affect the re-rendering of the <a> ?

peterklijn commented 7 years ago

Not to my knowledge, the <a> is in the header op the page. The XHR's are for content.

Interestingly, if I use ....click({ force: true }); it sometimes fails because the click doesn't trigger a page change. We use react-router for our navigation, so I have a gut feeling it might have to do with that react-router component not being completely initialised as the page is still kind-of loading / waiting on async calls.

brian-mann commented 7 years ago

I believe the problem here is that it's finding the <a> but then it's getting detached / wiped from the DOM by the time the .click() rolls around. If this is the case, there's actually nothing wrong with the visibility algorithm (it's just miscommunicating the root cause).

I can manually recreate this situation and opened a new issue here: https://github.com/cypress-io/cypress/issues/696

There is something causing the <a> to be detached / rerendered.

Likely what you'll need to do is add more guards prior to interacting with the element.

{ force: true } is failing because when the element is removed its event handlers are also removed, or the browser will no longer apply the default action to a detached element (such as to follow the link).

So what ends up happening is that by forcing the click event to happen, Cypress issues it, but it's being issued to a detached element, which does nothing.

brian-mann commented 7 years ago

As per your suggestion - you will need to come up with some mechanism to 'guard' Cypress from executing its commands too soon until you know your application has completely initialized.

You could directly communicate with your react app and register an event listener, or you could search for some content on the page that you know becomes available. For instance your react app could add an attribute to the or to know its completely done.

You could also individually wait for all the XHR's although react is going to process them asynchronously so there will still be a small gap even after waiting on them.

Once you establish that it will solve all of your flake. I would likely wrap this up into a custom command or just override visit so it doesn't resolve until your app is fully loaded.

There are no events in JS that will help you do this, it's completely application specific.

puneet0191 commented 7 years ago

I have the same issue when I am trying to type into an input field, the behaviour is rather flaky with Cypress, 5 out of 10 times the steps works.

I am changing the language of Installation in Joomla, which changes the fields based on the language selected by users. Before I begin Typing into the field, I ensure the field is visible, and then try to type, but I get the same error which says element is not visible because it has an effective width and height of: '0 x 0' pixels. Although in the screenshot it is clearly visible. image I am just trying to understand if we have a way of saying, hey Cypress do not proceed with the test until you have xyz element visible and you can interact with it.

chathuraa commented 7 years ago

I have the same issue. We are using angular. Some times the click registers and the other times does not.

brian-mann commented 7 years ago

@puneet0191 and @chathuraa did you read my comments above in this thread? I believe both of your situations are caused by the element becoming detached as Cypress begins to type. Can you verify nothing on your end is re-rendering the element.

@puneet0191 in your case you are highlighting the element in dev tools which is not necessarily the element that cypress found because it could have been replaced by being rerendered.

chathuraa commented 7 years ago

@brian-mann, thanks for the quick response. I believe the page render several times. Unfortunately reducing rerender is all the way at the bottom of our backlog. Sorry for my ignorance, is there a work around?

peterklijn commented 7 years ago

@chathuraa I worked around it by making sure the re-renders are done before executing the assertions which in my case is when all backend calls are done. See this comment

kristiehowboutdat commented 6 years ago

In my case, I was trying to click on a link (after waiting for the API result) that was being mounted and unmounted when API results came in. Switching it to a React pure component and making sure it wasn't unnecessarily unmounted fixed the problem. FWIW, I'm also using React Router 4 and this was trying to click on a <Link> element. Thank you Cypress team for such an awesome dev experience

tracykm commented 6 years ago

This has also been a real issue for us. We're using React and I'm still struggling to find the right combo of xhr waits for each page to get our tests reliable. But this thread has been helpful to understand the cause 👍

peternye commented 6 years ago

Timeouts because of zero-width elements happens all the time to us; I'm guessing it's because the element is only partially rendered. It would be great if there was a way to wait for an element of non-zero width; then I'd replace get() with a version that checked for both visibility and non-zero size. But I haven't found a way to do this. You can do an assertion on the css width, but that's not the same thing. Also {force: true} doesn't work in this case. The only solution currently I've found is to add wait()s.

jennifer-shehane commented 6 years ago

@peternye You should be able to write an assertion that checks on non-zero width where the tests will not move forward until it is greater than non-zero:

// these assertions below are the same
cy.get('#query-btn').invoke('width').should('be.gt', 0)
cy.get('#query-btn').invoke('width').should('be.greaterThan', 0)

You could basically assert against any property of an element, like innerWidth and outerWidth also.

See Assertions

peternye commented 6 years ago

Thanks, @jennifer-shehane! Exactly what I was looking for.

lachis commented 6 years ago

@jennifer-shehane this is exactly what we needed as well. we are going to implement a custom command to handle these assertions.

Cheers!

ikornienko commented 6 years ago

@jennifer-shehane thank you for the suggestion! Can you clarify, why cy.get('#query-btn').invoke('width').should('be.gt', 0) won't fail, but cy.get('#query-btn').click() fails? What I mean is that if Cypress is smart enough to do retries / reselects of the element when assertion doesn't work, why cannot it do the same for the actions? Of course, if you had an action earlier in a chain, it should fail (no recovery if action was done on element and then it became detached, i.e. if it's cy.get('#query-btn').click().parent() and after click() element is detached), but if everything in the chain were just selectors, Cypress could've retried them all if by the time we're doing first action element becomes detached. Or maybe I don't understand how assertions work in Cypress...

nekromoff commented 6 years ago

Same problem here:

Out of around 12 forms elements, Cypress is unable to "type" in 2 of them. All of them have the same visibility. However, the other 10 are without an issue. {force:true} does not work, either, no typing is done.

obrazok obrazok obrazok

obrazok

vjau commented 6 years ago

I have the same problem with a flaky behaviour. I know the element which i try to click is not removed from the dom, but there is an automatic scrolling of the viewport when the element is displayed. I get the 0x0 pixels error, but even with @jennifer-shehane workaround, the test runner is not able to click it.

With those tests :

cy.get('#candidatsList > li:nth-child(1) > span').should("contain", "Créer RDV");
cy.get('#candidatsList > li:nth-child(1) > span').invoke('width').should('be.gt', 0);
cy.get('#candidatsList > li:nth-child(1) > span').click();

First two "get" succeeds, but not the third one.

I don't think that there is a "scroll end" event i could hook to, so i don't know how i could guard this step without using cy.wait

egucciar commented 6 years ago

I agree with @vjau , no matter what we are doing to guard the click, the click fails though the element is clearly visible.

brian-mann commented 6 years ago

@Bkucera and I have been reworking the visibility algorithms and are also in the process of adding native events. These bugs will be fixed. I don't have a timeline yet, but as we fix the visibility stuff we will release patches.

At the moment you can try setting the input's value programmatically and then manually trigger the change events (or other stuff) so that your application binds to them correctly.

cy.get('input').invoke('val, 'asdf').trigger('change', { force: true }) // something like this

Just take programmatic shortcuts which will avoid the checking layers

nekromoff commented 6 years ago

Didn't work for me. I tried to programmatically make them visible, but to no avail.

On Thu, Jun 14, 2018 at 7:10 PM, Brian Mann notifications@github.com wrote:

@Bkucera https://github.com/Bkucera and I have been reworking the visibility algorithms and are also in the process of adding native events. These bugs will be fixed. I don't have a timeline yet, but as we fix the visibility stuff we will release patches.

At the moment you can try setting the input's value programmatically and then manually trigger the change events (or other stuff) so that your application binds to them correctly.

cy.get('input').invoke('val, 'asdf').trigger('change', { force: true }) // something like this

Just take programmatic shortcuts which will avoid the checking layers

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/cypress-io/cypress/issues/695#issuecomment-397369806, or mute the thread https://github.com/notifications/unsubscribe-auth/AIJ3zabjO0hR7dK3-Gt0IW_TF7QjUkyRks5t8pj3gaJpZM4Poclb .

aepplerplangrid commented 6 years ago

I would just like to say I'm experiencing this same problem, am able to verify the the existence of the element via height and width but getting the same "element not visible" when trying the click. I do believe it has to do with timing, if I hardcode a wait it passes fine. This is especially weird because the .should('be.visible').invoke('width').should('be.greaterThan', 0) and .should('be.visible').invoke('height').should('be.greaterThan', 0) steps I run before the click command both pass while the click fails.

brian-mann commented 6 years ago

Try cy.get(...).click({ force: true })

nekromoff commented 6 years ago

Tried that to no avail.

On Thu, Jun 21, 2018, 22:21 Brian Mann notifications@github.com wrote:

Try cy.get(...).click({ force: true })

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/cypress-io/cypress/issues/695#issuecomment-399230133, or mute the thread https://github.com/notifications/unsubscribe-auth/AIJ3zUeHjuniqd4kB8tbN5mpAaJCCzOpks5t_AA5gaJpZM4Poclb .

aepplerplangrid commented 6 years ago

yeah if it force clicks it works some of the time - I think the issue is it's still clicking before the element loads. I'm noticing this behavior with a dropdown element that loads a list (but doesn't need to fire off an xhr request to do so)

brian-mann commented 6 years ago

Cypress can't click an element before it loads. It waits until the element exists. It's possible that your actual click handler isn't wired up even though its visible and available.

I would simply add an assertion about the element (or something else) that can guard Cypress from proceeding until the element is ready to receive the click event. It may be something about its dimensions, or content on the page, or waiting for an XHR, etc.

vjau commented 6 years ago

@brian-mann with the scrolling issue i reported earlier, AFAIK there is no way to know when the scroll has ended. The element exists but the browser is not able to click it while scrolling.

aepplerplangrid commented 6 years ago

but doesn't the .should('be.visible').invoke('width').should('be.greaterThan', 0) passing before a click command ensure that the element is visible? Or am I missing an assertion?

aepplerplangrid commented 6 years ago
cy.get(selectors.addAdminTestEdit).should('be.visible').click();
cy.get(selectors.addAdminTestEditRemoveAdmin).should('be.visible').invoke('width').should('be.greaterThan', 0)
cy.get(selectors.addAdminTestEditRemoveAdmin).should('be.visible').invoke('height').should('be.greaterThan', 0)
cy.wait(300)
cy.get(selectors.addAdminTestEditRemoveAdmin).click();

will pass always - but if I take out the cy.wait(300) the click() will fail about half the time

panzelva commented 6 years ago

I have same problem with AngularJS website. I need to include cy.wait() or the click() fails with:

This element '<a>' is not visible because it has an effective width and height of: '0 x 0' pixels.

Running on Cypress 3.0.1

afiliptsov commented 6 years ago

Had same issue when the click failure in 10% chance. cy.get('#query-btn').invoke('width').should('be.gt', 0) Didn`t helped

{ force: true }and cy.wait(150); Worked ( kind of ) But its definitely workaround not a solution for me.

But the app which i was testing was not build on react and i was referencing ID locators. Maybe that where it comes from.

OmriAharon commented 6 years ago

Happens to us too, nothing works except cy.wait(100).

patelnav commented 6 years ago

@brian-mann : Do you have a repro environment where you can see this bug reliably?

I believe it does have to do with XHRs coming in late. In my case React is reflowing a bunch of elements to the DOM that already existed.

I'm not familiar with Cypress internals but perhaps attaching attaching a selector to the click event: cy.click() could close the gap between the get and the click. Might be useful to debug what's going on.

genox commented 6 years ago

We have the same (or similar) issue. Tests run fine on a desktop using chrome or electron but headless fails randomly (platform independent). cy.click() in particular. While I'm on the bandwagon for Cypress, this seems to be a bit shaky, especially for complex SPAs. Or maybe we're doing it wrong - but I honestly can't see a way to assure a 100% hit rate here. I would love to read how to prevent things like that from happening.

kamavingah commented 6 years ago

@OmriAharon

Happens to us to, nothing works except cy.wait(100).

Same here - nothing worked other than this workaround. Also using react.

ArnaudPel commented 6 years ago

Same issue while testing our Angular app. A better error message as suggested in #696 would have helped indeed, but debugging remained quite hard for me.

(Edited)

I had written a big comment about the whole day I spent experimenting around similar issues, but feel quite stupid now that I've finally realized (after getting away from the code) that it was a caching issue. I had a list of elements displayed first from the cache, that were getting re-rendered later when the xhr came back. So I can now confirm with 100% certainty that it was a bloody asynchronous rendering issue. Quite amazing that it was often happening at the exact same moment, but maybe Cypress was freeing the stack consistently at the same step. Dunno.

Suggestion of improvement

I suppose this issue means that Cypress doesn't retry the whole commands chain when the click fails. Would retrying the whole chain, or maybe just going up the chain until finding an element still attached to the DOM, make such commands more stable ? Something like #1548 maybe.

kuceb commented 6 years ago

@ArnaudPel I fully agree, Cypress not retrying the entire command chain is a source of these types of confusions and bugs. We have been talking about fixing this for a while now, and will probably be next after retries or cross browser support.

ArnaudPel commented 6 years ago

@Bkucera thank you for your quick reply. I've edited my comment after realizing I was all at sea yesterday. We seem to agree that retrying the whole chain would have made the test pass. But now I realize that without this failure I would never have noticed this caching thing :) Which, in my case, was not intended.

Trying to think about a way to avoid this in the future, maybe Cypress could enrich the error message with something like right before this failure, xhr request [url] completed ? But I understand it's already visible in the Live version, and might be too much.

kuceb commented 6 years ago

@ArnaudPel

Would retrying the whole chain, or maybe just going up the chain until finding an element still attached to the DOM

Cypress does currently do this. The problem happens when Cypress finds an element, but it's not the one you expected, so the element never gets detached from the dom.

Saibamen commented 5 years ago

Still failing in 3.1.4

Multiple wait() doesn't work for me (less crashes but still randomly...)

Saibamen commented 5 years ago

This same with cy.select()

glomotion commented 5 years ago

Not to pile on here - but incase it helps, here is an interesting instance of this issue happening, where all suggested fixes above are not helping to work around the issue.

i am running these assertions:

const setupPageAddressListFirstSuggestion = '[data-cypress-test-reference="address-suggestion-first-item"]'
cy.get(setupPageAddressListFirstSuggestion).should('exist').and('not.be.disabled');
cy.get(setupPageAddressListFirstSuggestion).invoke('width').should('be.greaterThan', 0);
cy.get(setupPageAddressListFirstSuggestion).invoke('height').should('be.greaterThan', 0);
cy.get(setupPageAddressListFirstSuggestion).click();

here's how cypress 3.1.4 processes these assertions: screen-shot-2019-01-08-at-4 20 20-pm

I've also tried aliasing the element, then using the alias. same results.

(the item that i'm clicking is indeed generated/regenerated by multiple react renders - based on an api response, hence the difficulty - though its interesting how every assertion before the click, contradicts the final click assertion's error message. :]).

For now, I've just stubbed out the harmony api calls, and have them constantly return the same data > which minimises the react re-rendering > which seems to stabilise the click event. It would be nice though, to not have to do this.

Saibamen commented 5 years ago

@jennifer-shehane I don't see any commits in develop branch for solving this issue. Anyone working to fix this?

gabbersepp commented 5 years ago

Hi, I have the same issues. using should("be.visible") also fails because of this problem. It would be good if one can make a clear statement whether or not this will be fixed. Otherwise we also do not know if we should handle this in our code or not.

jennifer-shehane commented 5 years ago

No work has been done specifically on this issue.

chrispickford commented 5 years ago

I've tried all of the workarounds mentioned here:

No matter what I try, it seems to fail about 20% of the time with the "effective width and height of 0x0" error.

My project is an AngularJS SPA and while the element being clicked is dynamic in the sense it is conditionally added to the DOM. when running in electron browser mode I can clearly see the element and it still fails!

Please prioritise this bug as it's causing our CI process to regularly fail.

rndm2 commented 5 years ago

We are experiencing same issue, are there any updates on this?

ethanengland commented 5 years ago

Similar issue with our project in Vue, hoping for an update soon.