4
votes

I'm trying to build an overwrite for cy.click() to add an additional assertion that must be true before an element is clicked. I know I could do this with a custom command, but I'd rather do it by overriding the built in one so that our existing tests all get the fix without having to be updated, and so we don't need to remember to use a different click method in the future. My current code is:

Cypress.Commands.overwrite('click', (originalFn, subject, options) => {
    cy.wrap(subject).should('not.have.attr', 'disabled').then(() => {
        return originalFn(subject,options);
    })
});

Basically, it should check one extra assertion (waiting for it to become true, since using should) then do the built-in click. My reasoning behind this is that the built in click assertions don't recognize the disabled attribute on certain elements (e.g. <a> is always considered enabled even if it has the disabled attribute, https://github.com/cypress-io/cypress/issues/5903). This works on tests that just click, but fails with the following message when cy.type or cy.select are called, possibly because those use click internally?

cypress_runner.js:199855 CypressError: Timed out retrying: Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.

The command that returned the promise was:

  > `cy.type()`

The cy command you invoked inside the promise was:

  > `cy.wrap()`

Because Cypress commands are already promise-like, you don't need to wrap them or return your own promise.

Cypress will resolve your command with whatever the final Cypress command yields.

The reason this is an error instead of a warning is because Cypress internally queues commands serially whereas Promises execute as soon as they are invoked. Attempting to reconcile this would prevent Cypress from ever resolving.

I found https://github.com/cypress-io/cypress/issues/3838, but it got closed without actually giving a solution to this issue. What is the proper way to overwrite click without causing issues with other methods that seem to call it internally?

1
What if you use expect(): expect(subject).to.not.have.attr('disabled'); return originalFn(subject,options);Leonardo Martinez
Not getting the error, please post the full test. Tried it clicking a button and an <input type="button". The reference to cy.type() in the error is suspicious.Ackroydd
@Ackroydd Clicking a button or input works fine. The issue is when you call cy.type() or cy.select() with this overwrite, since they seem to call cy.click() initially. Adding the overwrite to the kitchen sink example (github.com/cypress-io/cypress-example-kitchensink) and running tests that select or type reproduces the issue.Travis
@LeonardoMartinez I tried that, but expect doesn't wait for the condition to become true like cy.should() does. In my case the element is disabled at first and becomes enabled later (by JS code that is loaded asynchronously), so expect fails immediately.Travis
Woops sorry, bad eyesight.Ackroydd

1 Answers

0
votes

Based on the issue referenced, I don't think there's a "proper" way to overwrite click, but for your use-case a solution is to look at the force option.

When .type() issues a click it sets the force option, see type.js

force: true, // force the click, avoid waiting

Same with select.js

And since force ignores disabled attributes, there's no need to check disabled when calling .click({force: true}).

The following seems to work ok, but I'm not sure it covers every scenario

Cypress.Commands.overwrite('click', (originalFn, subject, options) => {

  if (!options?.force) {
    cy.wrap(subject).should('not.have.attr', 'disabled').then(() => {
      return originalFn(subject,options);
    })
  } else {
    return originalFn(subject,options);
  }
})

For situations checking something other than disabled, there's an undocumented state property current which gives the command type

Cypress.Commands.overwrite('click', (originalFn, subject, options) => {

  const currentCommand = cy.state('current').attributes.name;

  if (currentCommand === 'click') {
    cy.wrap(subject).should('not.have.attr', 'disabled').then(() => {
      return originalFn(subject,options);
    })
  } else {
    return originalFn(subject,options);
  }
})