4

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?

user16217248
  • 3,119
  • 19
  • 19
  • 37
Travis
  • 43
  • 4
  • What if you use expect(): expect(subject).to.not.have.attr('disabled'); return originalFn(subject,options); – Leonardo Martinez Jan 18 '21 at 19:08
  • Not getting the error, please post the full test. Tried it clicking a button and an ` – Ackroydd Jan 18 '21 at 20:43
  • @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 (https://github.com/cypress-io/cypress-example-kitchensink) and running tests that `select` or `type` reproduces the issue. – Travis Jan 19 '21 at 14:58
  • @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 Jan 19 '21 at 15:15
  • Woops sorry, bad eyesight. – Ackroydd Jan 20 '21 at 22:42

1 Answers1

4

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);
  }
})
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • This is a good idea, and will be added since force should skip this additional check. It does fix `type`, but unfortunately doesn't seem to fix `select` as it issues 2 clicks (presumably one to open the dropdown and a second to pick an option) and only passes force to the second click. However, this did point me in a helpful direction. All of these internal calls pass `verify: false` to click, which we never pass. So skipping this check in that case seems to solve the issue for now. – Travis Jan 20 '21 at 14:31
  • I added the following check and it seems to be working, clicks from my tests do check the new assertion but ones called internally from click, select, etc. do not. `if (!options || (!options.force && options.verify !== false))` – Travis Jan 20 '21 at 14:43
  • 1
    IMO overwriting commands is over-rated. I would prefer the more explicit approach of a new command, or just prefix the `click` with `.should('not.have.attr', 'disabled')`. – Richard Matsen Jan 20 '21 at 20:23