Assert that element is not actionable in Cypress

Laura picture Laura · Aug 29, 2018 · Viewed 7.7k times · Source

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

Answer

Richard Matsen picture Richard Matsen · Sep 3, 2018

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("#button-covered-in-span").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).