21

If an element is not actionable on the page (in this case, covered by another element) and you try to click it, Cypress will show an error like this:

CypressError: Timed out retrying: cy.click() failed because this element:

<span>...</span>

is being covered by another element:

Great! But is there any way to assert that this is the case, aka that the element cannot be clicked?

This doesn't work:

  • should.not.exist - the element does exist
  • should.be.disabled - the element is not disabled
  • should.not.be.visible - the element is visible (just covered by another, transparent element)
  • using cy.on('uncaught:exception', ...), since this is not an exception
Laura
  • 3,233
  • 3
  • 28
  • 45

3 Answers3

21

See the Cypress tests at click_spec.coffee.

it "throws when a non-descendent element is covering subject", (done) ->

  $btn = $("<button>button covered</button>")
    .attr("id", "button-covered-in-span")
    .prependTo(cy.$$("body"))

  span = $("<span>span on button</span>")
    .css(position: "absolute", 
         left: $btn.offset().left, 
         top: $btn.offset().top, 
         padding: 5, display: "inline-block", 
         backgroundColor: "yellow")
    .prependTo(cy.$$("body"))

  cy.on "fail", (err) =>
    ...
    expect(err.message).to.include "cy.click() failed because this element"
    expect(err.message).to.include "is being covered by another element"
    ...
    done()

  cy.get("#button-covered-in-span").click()

Simplest would be to mimic this test, even though docs recommend only using cy.on('fail') for debugging.

This is similar to a unit test using expect().to.throw() to check that an exception occurs as expected so I feel the pattern is justified here.

To be thorough, I would include a call to click({force: true}).

it('should fail the click() because element is covered', (done) => {

  // Check that click succeeds when forced
  cy.get('button').click({ force: true })

  // Use once() binding for just this fail
  cy.once('fail', (err) => {

    // Capturing the fail event swallows it and lets the test succeed

    // Now look for the expected messages
    expect(err.message).to.include('cy.click() failed because this element');
    expect(err.message).to.include('is being covered by another element');

    done();
  });

  cy.get("#button-covered-in-span").click().then(x => {
    // Only here if click succeeds (so test fails)
    done(new Error('Expected button NOT to be clickable, but click() succeeded'));
  })

})

As a custom command

I'm not sure how to make the chai extension you asked for, but the logic could be wrapped in a custom command

/cypress/support/index.js

Cypress.Commands.add("isNotActionable", function(selector, done) {
  cy.get(selector).click({ force: true })
  cy.once('fail', (err) => {
    expect(err.message).to.include('cy.click() failed because this element');
    expect(err.message).to.include('is being covered by another element');
    done();
  });
  cy.get(selector).click().then(x => {
    done(new Error('Expected element NOT to be clickable, but click() succeeded'));
  })
}) 

/cypress/integration/myTest.spec.js

it('should fail the click() because element is covered', (done) => {
  cy.isNotActionable('button', done)
});

Note

I was expecting done() to time out when the premise of the test (i.e. that the button is covered) is false.

This does not happen (reason unknown), but by chaining .then() off the 2nd click allows done() to be called with an error message. The then() callback will only be called if the click succeeds, otherwise the cy.once('fail') callback handles click failure (as per Cypress' own test).

Cory House
  • 14,235
  • 13
  • 70
  • 87
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • 1
    That looks promising - will try, and also try to wrap it as a chai extension (sth like `is.actionable` - though not sure if possible since it seems very specific). What value do you see in trying to see if the click succeeds with `force`? I might be thinking too narrowly with the specific case I'm trying to test, so I'm not seeing the point here? – Laura Sep 03 '18 at 12:22
  • Re trying with `force`, the idea is to cover cases where the element becomes unclickable for another reason, so `force` then fails. I haven't tested that out, was just a bit of insurance. – Richard Matsen Sep 03 '18 at 18:29
  • Makes sense - I haven't had the chance to test it yet sadly, but I think your way is the right one so I'm gonna accept this as the answer and hopefully get back to it soon to see if it can be somehow wrapped with chai. – Laura Sep 08 '18 at 18:03
  • The custom command here does not work. It relies on the `done()` from the `it()`, which is not present in the command. – Joshua Wade Sep 13 '18 at 15:20
  • I have spent the day trying to implement this answer in my project and nothing seems to be working. At this point I suspect a bug with Cypress. When I perform a click on my covered element from the original test, the operation fails. However, the click succeeds when I click from inside the custom command. I've even tried setting `{force: false}`, which is the default value I would expect. – Joshua Wade Sep 13 '18 at 19:16
  • It works for me, although `done` needs to be passed in to the command - but you would have noticed that. How are you setting up the covered element? – Richard Matsen Sep 13 '18 at 19:41
  • I've abandoned the `done` idea, as I ran into an issue with my test just stopping somewhere in the error handler. Instead of calling `done()`, my command throws its own exception if the click never fails. – Joshua Wade Sep 13 '18 at 19:47
  • My setup is a bit complex. I'm working on a reproducible test case right now; I'll comment here when I get one. – Joshua Wade Sep 13 '18 at 19:48
  • For a reference, I just copied @bkucera's code from the Cypress unit test linked at the top of my post. You will need `$ = Cypress.$.bind(Cypress)` from line 1 as well. – Richard Matsen Sep 13 '18 at 19:55
  • [Here](https://gist.github.com/SecondFlight/777fbfc595e58b3aae125b657535565f) is the command I'm using. I'll be trying it on a simple HTML page next. – Joshua Wade Sep 13 '18 at 20:10
  • You will need to index subject - `cy.wrap(subject[0]).click({force: false});`, and call your `method` passed in. But also the `if (!doneIt)` is not waiting for on fail to process. If you remove that last, the command works (at least when the element ***is*** covered). – Richard Matsen Sep 13 '18 at 20:35
  • Wow, I can't believe I missed that! I've been doing this for long enough, I should have caught that error. Thanks, I think it's working now. I'm going to touch up my edits to the command and post it here, as I think they could be useful to someone else. – Joshua Wade Sep 13 '18 at 20:57
  • Actually, you were right about the Cypress bug. As far as I can tell, `done()` never times out ***if the element is not covered***. First principle of TDD - write a failing test. So, if I offset the covering span by 100px then the 2nd click never fails, and the test spins on forever. I was expecting done() to timeout but it never does. – Richard Matsen Sep 13 '18 at 21:20
  • If I console.log(done) in the command, it shows `ƒ (err) { doneEarly(); originalDone(err); return null; }` which seems to indicate they have wrapped mocha's async done function. Can't pin it down on latest source, and I am using Cypress v2.1.0 so need to upgrade and see what's what. – Richard Matsen Sep 13 '18 at 21:28
  • Yes, that perfectly describes my original issue. I've managed to find another bug: using [my command](https://gist.github.com/SecondFlight/777fbfc595e58b3aae125b657535565f) works just fine, but it will silently skip any commands queued after it in the same `it` block. I'll report both of these bugs. – Joshua Wade Sep 14 '18 at 17:55
  • I've submitted the bug reports: [done() hanging issue](https://github.com/cypress-io/cypress/issues/2478), [custom command clearing command queue](https://github.com/cypress-io/cypress/issues/2474) – Joshua Wade Sep 14 '18 at 20:48
1

Another way without having to catch the error when the element is clicked is to check whether or not pointer-events has been set.

For example:

cy.get('element').should('have.css', 'pointer-events', "none") // not clickable
cy.get('element').should('have.css', 'pointer-events', "auto") // clickable
  • 1
    But the question is about element covering element, not pointer events. – kegne Oct 07 '22 at 23:15
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 12 '22 at 09:10
0

Not a general answer, but it may be sufficient just to specify the z-indexes if the elements are known and known to overlap:

cy.get('element1').should('have.css', 'z-index', '2');
cy.get('element2').should('have.css', 'z-index', '1');
jasaarim
  • 1,806
  • 15
  • 19