javascriptcypresscypress-custom-commands

What is the proper way to overwrite the cy.click() command in Cypress?


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). 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 this, 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?


Solution

  • 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);
      }
    })