How to check if operation has finished in cypress

Viewed 21

I am new to cypress and testing as such, I want to do e2e user journey for my project. This is an online store with the cart implemented on the backend so when I click on the add to cart button the request is made and until I get the response from the CMS the button is disabled. Response is usually quite fast but when I try to test it it takes long time, sometime it passes when I set wait to 3000 and sometimes it fails on 5000. I have a similar story with login, it passes 90% of a time when I check if a page was redirected properly and sometimes it fails. I have red that it is an anti pattern to do conditionals in tests so how can Imtest if a respose came back and only then proceed with the rest of the test? here is my test:

describe("Logged in user journey", () => {
  it("customer is able to do the full journey from login to purchase", () => {
    context("User logs in to make a puchase", () => {
      cy.visit("/auth/login");
      cy.getByData("email").type(Cypress.env("login"));
      cy.getByData("password").type(Cypress.env("password"));
      cy.get("form").find("button").click();
      cy.location().should((loc) => {
        expect(loc.pathname).to.eq("/auth/dashboard");
      });
      cy.get("section").should("contain.text", Cypress.env("login"));
    });

    context("user adds two items of this same type", () => {
      cy.visit("/products");
      cy.getByData("add-to-cart-button").first().click();
      cy.getByData("cart-icon").first().should("contain.text", "1");
      cy.wait(3000).then(() => {
        cy.getByData("add-to-cart-button").first().click();
        cy.visit("/cart");
        cy.getByData("cart-item-quantity").should("contain.text", "2");
      });
    });
  });
});

and this is the handler

const addItemToCart = (newItem: CartItem) => {
    setClickedItemSlug(newItem.product.slug);
    upsertOrderMutation({
      variables: {
        id: {
          id: cartIdFromStorage,
        },
        data: {
          create: createOrderInput(newItem),
          update: updateOrderInput(cartItems, newItem),
        },
      },
    })
      .then(({ data }) => {
        const newCartId = data?.upsertOrder?.id;
        const newCartTotal = data?.upsertOrder?.total;

        if (!cartIdFromStorage && newCartId) {
          setCartIdInStorage(newCartId);
        }

        setClickedItemSlug("");
        if (!newCartTotal) return;
        setCartTotal(newCartTotal);

        const upsertedOrderItem = data?.upsertOrder?.orderItems.find(
          (upsertedItem) => upsertedItem?.product?.slug === newItem.product.slug
        );
        setCartItems((prevCart) => {
          const existingItem = prevCart.find(
            (cartItem) => cartItem.product.slug === newItem.product.slug
          );
          if (!existingItem) {
            return [
              ...prevCart,
              { ...newItem, id: upsertedOrderItem?.id || "" },
            ];
          }
          return prevCart.map((cartItem) => {
            if (cartItem.product.slug === newItem.product.slug) {
              return {
                ...cartItem,
                quantity: upsertedOrderItem?.quantity as number,
                id: upsertedOrderItem?.id || "",
              };
            }
            return cartItem;
          });
        });
      })
      .catch((error) => {
        if (error instanceof Error) {
          console.log("Error upserting order " + error.message);
        }
      });

In teh handler there is a state setter

setClickedItemSlug(newItem.product.slug); 

and this handler make the button disabled, so perhaps I could wait for a button to be enabled again or the state of setClickedItemSlug would be an empty string, is it possible? And how could I do that?

2 Answers

If you want to have more stable tests, you should also avoid having any tests dependent on the set up of the previous test(s). It looks like your second tests depends on the application already being logged into a user account.

A few other ways to help make your tests stronger:

  • spy/stub calls made by your app
  • add assertions before your invoke an action
describe("Logged in user journey", () => {
  it("customer is able to do the full journey from login to purchase", () => {
    context("User logs in to make a puchase", () => {
      cy.visit("/auth/login");
      cy.getByData("email").should('be.visible').type(Cypress.env("login"));
      cy.getByData("password").should('be.visible').type(Cypress.env("password"));
      // maybe check for any successfully calls for login
      cy.get("form").find("button").click();
      // you can get pathname with location
      cy.location('pathname').should('eq','/auth/dashboard');
      // wait on any calls to complete
      cy.get("section").should("contain.text", Cypress.env("login"));
    });

    context("user adds two items of this same type", () => {
      // add before()/beforeEach() to set up login
      cy.visit("/products");
      // spy on upsertOrderMutation for adding to cart
      cy.getByData("add-to-cart-button").first().should('be.visible').click();
      cy.getByData("cart-icon").first().should("contain.text", "1");
      // wait on upsertOrderMutation spy and UI should be ready for next steps
      cy.getByData("add-to-cart-button").first().click();
      cy.visit("/cart");
      cy.getByData("cart-item-quantity").should("contain.text", "2");
    });
  });
});

The first thing to note is that context() should not be inside it().

context() is a simile for describe(), as such it should be on the outer level of nesting.

I'm not sure if that causes the issues, but Cypress will definitely not be expecting it.

The context/it nesting should look like this

describe("Logged in user journey", () => {
  context("customer is able to do the full journey from login to purchase", () => {

    it("User logs in to make a purchase", () => {
      ...
    });

    it("user adds two items of this same type", () => {
      ...
    });
  });
});

The login flakiness should be fixed with extra timeout, and it's worth adding a "sleep" command after the action point, because of the React hooks.

it("User logs in to make a purchase", () => {

  cy.visit("/auth/login");
  cy.getByData("email").type(Cypress.env("login"));
  cy.getByData("password").type(Cypress.env("password"));
  cy.get("form").find("button").click();

  cy.wait(0)                         // "sleep" the test to allow react hooks to run  

  cy.location('pathname', {timeout:10000})     // add extra timeout here
    .should('eq', '/auth/dashboard');

  cy.get("section").should("contain.text", Cypress.env("login"));
});

Since the button enabled state is tied to the hooks state change, it's definitely worth checking the disabled/enabled cycle in the test.

it("user adds two items of this same type", () => {

  cy.visit("/products");
  cy.getByData("add-to-cart-button").first().click();

  cy.wait(0)         // "sleep" the test to allow react hooks to run  

  cy.getByData("add-to-cart-button").first()
    .should('be.disabled')                      // look for this state change

  cy.getByData("cart-icon").first().should("contain.text", "1");


  cy.getByData("add-to-cart-button").first()
    .should('be.disabled')                      // look for re-enable
    .click();

  cy.wait(0)         // "sleep" the test to allow react hooks to run  

  cy.visit("/cart");
  cy.getByData("cart-item-quantity").should("contain.text", "2");

});
Related