tnrich
tnrich

Reputation: 8580

Cypress overwrite 'type' command to add a small wait throws promise error if .clear() is called before

Not sure quite why this is occurring. I'd like to add a .wait() before all type commands in my app by overwriting the type command. However I'm running into some issues with the promise quirks of cypress.

Here's some minimal code to reproduce

<html>
<body>
  <input/> 
</body>
</html>
//someSpec.js
cy.get('input').clear().type(name);
//commands.js
Cypress.Commands.overwrite(
  "type",
  (originalFn, subject, text, options) => {
    cy.wait(0).then(() => originalFn(subject,text,options))
  }
);

This will trigger a cypress error complaining about returning a promise:

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.clear()

The cy command you invoked inside the promise was:

  > cy.wait()

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.

Can anyone please help me get past this hurdle? Thanks!

Related issues: https://github.com/cypress-io/cypress/issues/3166

Upvotes: 2

Views: 1910

Answers (2)

tnrich
tnrich

Reputation: 8580

Here's what we ended up going with to try to fix the flakiness we were seeing with cypress typing:

Cypress.Commands.overwrite(
  "type",
  (originalFn, subject, text, options = {}) => {
    if (text === "{selectall}{del}") {
      return originalFn(subject, text, options); //pass thru .clear() calls
    } else {
      cy.wrap(subject, { log: false })
        .invoke("val")
        .then(prevValue => {
          if (
            options.passThru ||
            options.parseSpecialCharSequences === false ||
            (text.includes && text.includes("{")) //if special chars are getting used just pass them thru
          ) {
            // eslint-disable-next-line cypress/no-unnecessary-waiting
            cy.wait(0, { log: false }).then(
              { timeout: options.timeout || 40000 },
              () => originalFn(subject, text, options)
            );
          } else {
            // eslint-disable-next-line cypress/no-unnecessary-waiting
            cy.wait(0, { log: false }).then(
              { timeout: options.timeout || 40000 },
              () => originalFn(subject, text, options)
            );

            const valToCheck =
              options.assertVal ||
              `${options.noPrevValue ? "" : prevValue}${text}`;
            if (options.containsSelector) {
              cy.contains(options.containsSelector, valToCheck);
            } else {
              // Adds guarding that asserts that the value is typed.
              cy.wrap(subject, { log: false }).then($el => {
                // $el is a wrapped jQuery element
                if (!($el.val() === valToCheck)) {
                  if (options.runCount > 5) {
                    //if the type fails more than 5 times then throw an error
                    console.error("Error! Tried re-typing 5 times to no avail");
                    throw new Error(
                      "Error! Tried re-typing 5 times to no avail"
                    );
                  } else {
                    //if the type failed, retry it again up to 5 times
                    cy.wrap(subject)
                      .clear()
                      .type(valToCheck, {
                        ...options,
                        runCount: (options.runCount || 0) + 1
                      });
                  }
                } else {
                  //continue on, the type completed successfully!
                }
              });
            }
          }
        });
    }
  }
);

It riffs off of what Fody posted, adds a wait(0), and does a manual check that the input value is what is expected rerunning the type() command up to 5 times if the input value does not equal what is expected.

So far it has worked well for us and we're not seeing any more of the annoying half-typed inputs.

Here's how to type the additional options:

interface CustomTypeOptions extends Cypress.TypeOptions {
  /*
   * passThru=true just runs the OG type command
   */
  passThru: boolean;
  /* val to assert against after the type() has finished (instead of just using previous input value + what was typed)  */
  assertVal: boolean;
  /* do not include what was previously in the input value in the assertion post .type() finishing */
  noPrevValue: boolean;
}
 
/**
     * Type into a DOM element.
     *
     * @see https://on.cypress.io/type
     * @example
     *    cy.get('input').type('Hello, World')
     *    // type "hello" + press Enter
     *    cy.get('input').type('hello{enter}')
     */
    type(
      text: string,
      options?: Partial<CustomTypeOptions>
    ): Chainable<Subject>;

Upvotes: 2

Fody
Fody

Reputation: 31924

Under the hood cy.clear() is using cy.type('{selectall}{del}'), so you can avoid the wait on clearing by checking for that text.

Cypress.Commands.overwrite("type", (originalFn, subject, text, options) => {
    if (text === '{selectall}{del}') {
      console.log('clearing')
      return originalFn(subject,text,options)   // explicit return required
    } else {
      cy.wait(0).then(() => originalFn(subject,text,options))  // return value is queued
    }
  }
)

Tested with

<html>
<body>
  <input value="something"/> 
</body>
</html>
cy.get('input')
  .clear()
  .then(input => { 
    expect(input.val()).to.equal('')
    return input
  })
  .type('name')
  .then(input => { 
    expect(input.val()).to.equal('name')
    return input
  })
  .type('{selectall}{del}')
  .then(input => { 
    expect(input.val()).to.equal('')
  })

Upvotes: 2

Related Questions