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

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

Closed
peterklijn opened this issue Sep 29, 2017 · 117 comments
Labels
pkg/driver This is due to an issue in the packages/driver directory topic: command sequence topic: visibility 👁 type: bug

Comments

@peterklijn
Copy link

peterklijn commented Sep 29, 2017

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).


  • Operating System:
    locally: OSX 10.12.6
    CI: Debian Stretch (Docker container node:6.11.3-stretch)
  • Cypress Version:
    0.20.0 & 0.20.1
  • Browser Version:
    Version 61.0.3163.100 (Official Build) (64-bit) (locally)

Is this a Feature or Bug?

Bug?

Current behavior:

cy.click() failed because element is not visible

@brian-mann
Copy link
Member

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
Copy link
Author

peterklijn commented Sep 29, 2017

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
Copy link
Member

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

@peterklijn
Copy link
Author

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
Copy link
Member

brian-mann commented Sep 29, 2017

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: #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
Copy link
Member

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.

@jennifer-shehane jennifer-shehane added pkg/driver This is due to an issue in the packages/driver directory topic: visibility 👁 labels Oct 11, 2017
@puneet0191
Copy link

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
Copy link

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

@brian-mann
Copy link
Member

brian-mann commented Nov 8, 2017

@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
Copy link

@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
Copy link
Author

peterklijn commented Nov 8, 2017

@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
Copy link

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
Copy link

tracykm commented Apr 3, 2018

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
Copy link

peternye commented Apr 5, 2018

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
Copy link
Member

jennifer-shehane commented Apr 9, 2018

@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
Copy link

peternye commented Apr 9, 2018

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

@lachis
Copy link

lachis commented Apr 10, 2018

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

Cheers!

@ikornienko
Copy link

@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
Copy link

nekromoff commented May 4, 2018

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
Copy link

vjau commented Jun 7, 2018

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
Copy link
Contributor

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
Copy link
Member

@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
Copy link

nekromoff commented Jun 14, 2018 via email

@aepplerplangrid
Copy link

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
Copy link
Member

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

@nekromoff
Copy link

nekromoff commented Jun 21, 2018 via email

@kuceb
Copy link
Contributor

kuceb commented Oct 15, 2019

thanks @hdavidzhu, you are correct. Should have thrown a comment on here.

@davetapley
Copy link

This is especially depressing 😞

test

@Nandish47
Copy link

Any update on this issue? It's still reproducible in Cypress v3.6.1

@Rodyb
Copy link

Rodyb commented Nov 11, 2019

I am experiencing exactly the same in 3.6.1

@jennifer-shehane
Copy link
Member

There were improvements as part of 3.5.0 release #4945 that should have improved this error message for many instances where one was asserting on a detached DOM element. Now users will see a message saying that the DOM element is attached.

If you're still experiencing this in 3.6.1 - Please provide a fully reproducible example of the bug. I cannot stress this enough. If we cannot write a failing test in Cypress - then we cannot begin work on the issue.

@khayrat
Copy link

khayrat commented Nov 26, 2019

After trying several approaches I'm getting good results with

cy.get("#id")
    .should("be.visible")
    .then($btn => {
      $btn.click();
});

@Saibamen
Copy link
Contributor

@khayrat version?

@khayrat
Copy link

khayrat commented Nov 26, 2019

cypress/included:3.2.0 and cypress/included:3.6.1 - I'm running it in ci/docker. Natively I got stable results either with cy.get("#id").click()

@FrontendSophie
Copy link

@khayrat Thanks! works for me!

@ericxyan
Copy link

I have the similar issue. page.startDateButton.click() doesn't work but page.startDateButton.then($btn => $btn.click()) does work.

@seidtgeist
Copy link

Being subscribed to this thread is funny. People come up with workarounds that probably break X% of the time instead of someone resolving the root cause. Does anyone know why this is a problem in the first place and how it could be addressed? <3

@lukeapage
Copy link
Contributor

The root cause is the app you are testing is changing the dom underneath - so cypress gets an element but before it can click it, the app has removed it from the dom and re rendered. Sometimes this can be solved by asserting state before the click to ensure no more dom changes will occur and the app is stable.
Most solutions here just add a tiny delay which make it more likely that your app under test has finished (the wait for visible may be lucky enough to catch the state and retry or it may miss it - depending on timing)
IMO cypress should retry clicks if the dom element is removed, before it dispatches the event.
I’ve solved this all of the time in our app by proxying every async event (all ASAP implementations, set timeout, promises) and then overriding get element to wait for all async events to finish before getting a element - it makes things very stable. The only problem is that some events like set timeout 200ms or 3000ms are things you should wait for and others you shouldn’t so we had to be careful on the boundaries). With the above, click is 100% stable on a very complex react app.

@saas2813
Copy link

@lukeapage have a very good description of this problem.

I have been trying to adress something similar; When opening an edit page my application displays cached data while it queries the server for the latest version and then re-renders. Depending on system-load Cypress will manage to start clicking before the re-rendering is complete and I get the above problem or something similar. Of-course this is unusual on my own machine and very frequent on the build-server. The worst part is that the re-rendering in respons to loading from the server causes the effect of the previous clicks to be removed, thus adding a "force" will simply delay the error.

I need a way to make Cypress synchronise with Vue rendering and nothing I have tried have been dependable. I have tried making our "Is loading"-flag we show to the user (that the tests waits for to not be visible) display longer but the race is still there some way.

@fo-fo
Copy link

fo-fo commented Jan 15, 2020

I proposed a solution at #695 (comment) but it didn't get any feedback. The idea is that get() would return a proxy object. If it notices that the referenced DOM node has been removed, it would try to find it again using the same selector. If it cannot find it within a given timeout, it would fail (as get() normally does), but if it does find it, then it would return it. In this case it wouldn't matter how many times the underlying DOM node gets recreated, all that would matter is that the DOM node referenced by the specified selector exists when the given action is applied to it (e.g., click()). The DOM node might change between different actions (e.g., check for visibility and a click) but I wouldn't usually expect this to be an issue.

That said, the fact that the DOM node is getting recreated could be a sign of flakiness in the application (if the component loses its state when the DOM node gets removed). But I think in many cases (especially legacy applications) it's not practically possible to fix these type of issues.

@graemeRharvey
Copy link

I need a way to make Cypress synchronise with Vue rendering and nothing I have tried have been dependable. I have tried making our "Is loading"-flag we show to the user (that the tests waits for to not be visible) display longer but the race is still there some way.

@saas2813 Is the correct approach here not to alias that call to the backend, then wait for that server request to provide a response...and then proceed accordingly? Perhaps I'm missing something regarding the implementation, but we do this frequently without issue (in a React-based app).

@grantspeelman
Copy link

I am not sure if this is useful but i get this issue with chrome but don't seem to get it with firefox.

Chrome
Screenshot_20200331_210328

Firefox
Screenshot_20200331_210417

@lencioni
Copy link

At Airbnb we have a React app that uses the Progressive Hydration technique discussed in this presentation: https://www.youtube.com/watch?v=k-A2VfuUROg

As such, we have chunks of our DOM that become hydrated in idle callbacks after the page load event has fired. Due to the way that Cypress works, this has caused quite a bit of flake in our tests because elements often end up getting swapped out between the .get() and the next chained method (e.g. between .get('button') and .click() here cy.get('button').click();).

The naive approach to work around this is to add cy.wait(n) calls all over the place with arbitrary timeouts. This is not satisfying because if you pick a large timeout, you make your tests unnecessarily slow, and if you don't pick a high enough timeout then you still end up with flake. As the product changes over time, these timeouts may need to be adjusted, and it is hard to know when to do that.

To make this better, we've developed a custom Cypress command called waitUntilSettled. This will install a MutationObserver and recursively call requestIdleCallback on the page. After the idle callback resolves and the DOM has not yet been mutated, we proceed with the test. We are just starting to use this and it seems to be helping us, so I thought I would share the code here.

https://gist.github.com/lencioni/7ba04e0f92558f49454c19c44cf3bc5c

We are currently only testing in browsers that support requestIdleCallback, so it is possible that when we roll this out to other browsers where we have to polyfill that, we will need to adjust this some--e.g. maybe we should wait for a couple of consecutive non-mutated DOM checks before moving forward.

I hope this helps anyone else running into these types of issues. In the future, it might be nice if Cypress was enhanced to do this style of waiting when visiting routes, so we could avoid dropping these cy.waitUntilSettled() calls in a bunch of places.

@jennifer-shehane
Copy link
Member

jennifer-shehane commented Jul 6, 2020

Are people still regularly seeing this error in the latest version of Cypress? There have been a lot fewer comments in here since January.

My theory is that people are now more regularly seeing the error:

This element `<div>` is not visible because it is detached from the DOM

which is a more accurate error message than the effective width and height one when the element is no longer in the DOM. We have an issue to address this here: #7306

@vjau
Copy link

vjau commented Jul 6, 2020

Sadly i have not been able to upgrade because of performance problems with the latest versions of Cypress that will probably never be fixed, so i'm considering switching to another solution.

@jennifer-shehane
Copy link
Member

Closing this issue and referring to #7306 since there have been no comments on this issue since 4.4.1 of Cypress was released, where we implemented a clearer error message in this case that used to show as the 0x0 error message:

This element `<div>` is not visible because it is detached from the DOM

If you're experiencing a bug similar to this in Cypress, please open a new issue with a fully reproducible example that we can run. There may be a specific edge case with the issue that we need more detail to fix.

@Gjodim
Copy link

Gjodim commented Feb 13, 2023

I was also having troubles with this with a specific case so i tried chaining the two assertions:
element.should('have.length.greaterThan', 0).and('be.visible')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg/driver This is due to an issue in the packages/driver directory topic: command sequence topic: visibility 👁 type: bug
Projects
None yet
Development

No branches or pull requests