How I stopped using Cypress {force: true}

How I stopped using Cypress {force: true}

Recently I refactored a Cypress spec to stop using click({ force: true }). I was using the force functionality to be able to interact with elements that were off screen in a carousel like component due to some behaviors that I did not want to account for in the spec due to the complexity it would introduce. The carousel components show 6 internal components on the screen at a time with additional components not visible without scrolling the carousel. I could not just scroll through the carousel to make the elements be visible on screen due to the components within the carousel not all being of the same type and I only wanted to interact with a certain type of component within the carousel.

Before I could go onto the next step in my spec, I needed to click on a certain number of inner components to save them. If I did not save enough of them the next step would fail due to requirements within that part of the application.

Below is what this step looked like in my code originally. I was looping through each carousel and clicking on each specific button that matched the data-test id. This would result in buttons being clicked that were not visible on screen and cause flakiness with the spec.

// Gets each carousel on the page
cy.get("[data-test='carousel']").each(($carousel) => {
  new Cypress.Promise((resolveCarousel) => {
    cy.wrap($carousel).scrollIntoView();
    cy.wrap($carousel).within(() => {
      // Gets each inner carousel component within the current carousel
      cy.get("[data-test='specific-save-button']").each(($saveButton) => {
        new Cypress.Promise((resolveSaveButton) => {
          cy.intercept('POST', 'https://api.com').as('interceptSave');
          // Allows the component to be clicked even if it is not visible on screen
          cy.wrap($saveButton).click({ force: true });
          cy.wait('@interceptSave');
          resolveSaveButton();
        });
      });
    });
    resolveCarousel();
  });
});

With my original code I was experiencing a couple of issues. Sometimes an error was getting thrown on this step of the spec due to Cypress trying to click on an element that was no longer on the screen due to the DOM updating and the fact that force: true is not a recommended practice.

Be careful with this option. It's possible to force your tests to pass when the element is actually not interactable in your application. - Cypress Docs

By forcing the click to happen, it disables to normal Cypress checks and can make debugging much harder and hide potential bugs due to its standard behavior being overwritten. You can read more about forcing here: Cypress forcing docs.

What's the difference?

When you force an event to happen we will:

  • Continue to perform all default actions

  • Forcibly fire the event at the element

We will NOT perform these:

  • Scroll the element into view

  • Ensure it is visible

  • Ensure it is not disabled

  • Ensure it is not detached

  • Ensure it is not readonly

  • Ensure it is not animating

  • Ensure it is not covered

  • Fire the event at a descendent

Provided by the Cypress Docs

I was able to stop using force by making 3 changes to this step.

  1. I started using the data-test attribute of a component that was one level higher and not the save button.

  2. After the index was greater than 5 for the inner components that had been looped over for that carousel, I exited that loop early and moved onto the next carousel on the page.

  3. I started using the jQuery .attr() method to only interact with certain inner components that I could save. Since cy.get yields a jQuery object, you can get its attribute by invoking the .attr() method. Checkout the docs for all of the chainers that are available when asserting about a DOM object: Assertion Docs.

With these three changes in place, I stopped seeing the errors and my test has been passing consistently.

// Gets each carousel on the page
cy.get("[data-test='carousel']").each(($carousel) => {
  new Cypress.Promise((resolveCarousel) => {
    cy.wrap($carousel).scrollIntoView();
    cy.wrap($carousel).within(() => {
      // Gets each inner carousel component within the current carousel
      cy.get("[data-test='inner-carousel-component']").each(($innerCarouselComponent, index) => {
        // Exit the inner component loop after all of the on screen 
        // elements are looped through
        if (index > 6) {
          return false;
        }
        // Using the jQuery .attr method to only act on certain inner 
        // components since I am having to be less specific with my data-test
        if ($innerCarouselComponent.attr('data-some-attr') {
          new Cypress.Promise((resolveInnerCarouselComponent) => {
            cy.intercept('POST', 'https://api.com').as('interceptSave');
            // Allows the component to be clicked even if it is not visible on screen
            cy.wrap($innerCarouselComponent).click();
            cy.wait('@interceptPost');
            resolveInnerCarouselComponent();
          });
        };
      });
    });
    resolveCarousel();
  });
});

I hope this helps someone else that is trying to write reliable and stable Cypress specs and follow the best practices!

Have you had to solve complex use cases like this? I would love to hear how to solved them in the comments section and any other Cypress tips you might have.