renders transactions item variations in feed

Before continuing, make sure you have read the Transaction Feeds Overview & Setup lesson first.

describe("renders and paginates all transaction feeds", () => {
    it("renders transactions item variations in feed", () => {
      cy.intercept("GET", "/transactions/public*", {
        headers: {
          "X-Powered-By": "Express",
          Date: new Date().toString(),
        },
        fixture: "public-transactions.json",
      }).as("mockedPublicTransactions");

      // Visit page again to trigger call to /transactions/public
      cy.visit("/");

      cy.wait("@notifications");
      cy.wait("@mockedPublicTransactions")
        .its("response.body.results")
        .then((transactions) => {
          const getTransactionFromEl = ($el: JQuery<Element>): TransactionResponseItem => {
            const transactionId = $el.data("test").split("transaction-item-")[1];
            return _.find(transactions, (transaction) => {
              return transaction.id === transactionId;
            })!;
          };

          cy.log("🚩Testing a paid payment transaction item");
          cy.contains("[data-test*='transaction-item']", "paid").within(($el) => {
            const transaction = getTransactionFromEl($el);
            const formattedAmount = Dinero({
              amount: transaction.amount,
            }).toFormat();

            expect([TransactionStatus.pending, TransactionStatus.complete]).to.include(
              transaction.status
            );

            expect(transaction.requestStatus).to.be.empty;

            cy.getBySelLike("like-count").should("have.text", `${transaction.likes.length}`);
            cy.getBySelLike("comment-count").should("have.text", `${transaction.comments.length}`);

            cy.getBySelLike("sender").should("contain", transaction.senderName);
            cy.getBySelLike("receiver").should("contain", transaction.receiverName);

            cy.getBySelLike("amount")
              .should("contain", `-${formattedAmount}`)
              .should("have.css", "color", "rgb(255, 0, 0)");
          });

          cy.log("🚩Testing a charged payment transaction item");
          cy.contains("[data-test*='transaction-item']", "charged").within(($el) => {
            const transaction = getTransactionFromEl($el);
            const formattedAmount = Dinero({
              amount: transaction.amount,
            }).toFormat();

            expect(TransactionStatus.complete).to.equal(transaction.status);

            expect(transaction.requestStatus).to.equal(TransactionRequestStatus.accepted);

            cy.getBySelLike("amount")
              .should("contain", `+${formattedAmount}`)
              .should("have.css", "color", "rgb(76, 175, 80)");
          });

          cy.log("🚩Testing a requested payment transaction item");
          cy.contains("[data-test*='transaction-item']", "requested").within(($el) => {
            const transaction = getTransactionFromEl($el);
            const formattedAmount = Dinero({
              amount: transaction.amount,
            }).toFormat();

            expect([TransactionStatus.pending, TransactionStatus.complete]).to.include(
              transaction.status
            );
            expect([
              TransactionRequestStatus.pending,
              TransactionRequestStatus.rejected,
            ]).to.include(transaction.requestStatus);

            cy.getBySelLike("amount")
              .should("contain", `+${formattedAmount}`)
              .should("have.css", "color", "rgb(76, 175, 80)");
          });
          cy.visualSnapshot("Transaction Item");
        });
    });

    _.each(feedViews, (feed, feedName) => {
      it(`paginates ${feedName} transaction feed`, () => {
        cy.getBySelLike(feed.tab)
          .click()
          .should("have.class", "Mui-selected")
          .contains(feed.tabLabel, { matchCase: false })
          .should("have.css", { "text-transform": "uppercase" });
        cy.getBySel("list-skeleton").should("not.exist");
        cy.visualSnapshot(`Paginate ${feedName}`);

        cy.wait(`@${feed.routeAlias}`)
          .its("response.body.results")
          .should("have.length", Cypress.env("paginationPageSize"));

        // Temporary fix: <https://github.com/cypress-io/cypress-realworld-app/issues/338>
        if (isMobile()) {
          cy.wait(10);
        }

        cy.log("📃 Scroll to next page");
        cy.getBySel("transaction-list").children().scrollTo("bottom");

        cy.wait(`@${feed.routeAlias}`)
          .its("response.body")
          .then(({ results, pageData }) => {
            expect(results).have.length(Cypress.env("paginationPageSize"));
            expect(pageData.page).to.equal(2);
            cy.visualSnapshot(`Paginate ${feedName} Next Page`);
            cy.nextTransactionFeedPage(feed.service, pageData.totalPages);
          });

        cy.wait(`@${feed.routeAlias}`)
          .its("response.body")
          .then(({ results, pageData }) => {
            expect(results).to.have.length.least(1);
            expect(pageData.page).to.equal(pageData.totalPages);
            expect(pageData.hasNextPages).to.equal(false);
            cy.visualSnapshot(`Paginate ${feedName} Last Page`);
          });
      });
    });
  });

You can find out more information about the custom Cypress commands used in this test here.

First, we are using cy.intercept() to intercept any GET request to the /transactions/public* route.

cy.intercept("GET", "/transactions/public*", {

We are also adding two additional headers to the response's headers. These headers will be appended to the original response headers, leaving the original ones intact.

headers: {
          "X-Powered-By": "Express",
          Date: new Date().toString(),
        },

We are then using a fixture to mock the response's payload. This fixture can be found inside of cypress/fixtures

fixture: "public-transactions.json",
      }).as("mockedPublicTransactions");

Then we visit the root route to trigger the GET request to /transactions/public

// Visit page again to trigger call to /transactions/public
cy.visit("/")

Next, we wait for two intercepts.

cy.wait("@notifications")
cy.wait("@mockedPublicTransactions")

We then grab the results from the @mockedPublicTransactions intercept. Remember, these results are coming from our public-transactions.json fixture. .its("response.body.results")

.its("response.body.results")

Then, we have a function called getTransactionFromEl which finds the transactionID from the transaction element in the DOM. This function is a little complicated, so let's break it down line by line.

.then((transactions) => {
          const getTransactionFromEl = ($el: JQuery<Element>): TransactionResponseItem => {
            const transactionId = $el.data("test").split("transaction-item-")[1];
            return _.find(transactions, (transaction) => {
              return transaction.id === transactionId;
            })!;
          };
const getTransactionFromEl = ($el: JQuery<Element>): TransactionResponseItem => {

Our function getTransactionFromEl accepts a jQuery element as a parameter and returns a TransactionResponseItem which is a TypeScript interface which can be found in src/models/transaction.ts around line 50 .

const transactionId = $el.data("test").split("transaction-item-")[1]

Next, we get the transactionID from the data-test attribute from the DOM. For example the HTML for one of our transactions looks like this.

<li
    class="MuiListItem-root MuiListItem-gutters MuiListItem-alignItemsFlexStart"
  data-test="transaction-item-183VHWyuQMS">

Once we have the string located within the data-test attribute, we use .split() to grab the transactionID

"transaction-item-183VHWyuQMS".split("transaction-item-")[1]
// "183VHWyuQMS"

Then, we use _.find() from Lodash to locate the transaction from the @mockedPublicTransactions request. using the ID we just located from the DOM .

return _.find(transactions, (transaction) => {
              return transaction.id === transactionId;
            })!;

We then use cy.log() to output a custom message to the Cypress Command Log.

cy.log("🚩Testing a paid payment transaction item")

Next, we are looking for a "paid" transaction, which in this case is the first transaction in the list.

cy.contains("[data-test*='transaction-item']", "paid").within(($el) => {

We then grab the transaction with our getTransactionFromEl function. Remember, this is going to return the transaction from our intercepted response, which is a fixture.

const transaction = getTransactionFromEl($el)

Here is the transaction from the fixture:

{
      "amount": 8647,
      "balanceAtCompletion": 8958,
      "createdAt": "2019-12-10T21:38:16.311Z",
      "description": "Payment: db4uxOm7d to IMbeyzHTj9",
      "id": "si_aNEMbyCA",
      "modifiedAt": "2020-05-06T08:15:48.263Z",
      "privacyLevel": "private",
      "receiverId": "IMbeyzHTj9",
      "requestResolvedAt": "2020-06-09T19:01:15.675Z",
      "requestStatus": "",
      "senderId": "db4uxOm7d",
      "source": "GYDJUNEaOK7",
      "status": "complete",
      "uuid": "41754166-ea5b-448a-9a8a-374ce387c714",
      "receiverName": "Kevin",
      "senderName": "Amir",
      "likes": [],
      "comments": []
    },

We then use a 3rd part library called Dinero.js to properly format the amount.

const formattedAmount = Dinero({
  amount: transaction.amount,
}).toFormat()

This will convert the "amount": 8647 from the fixture above to $86.47

expect([TransactionStatus.pending, TransactionStatus.complete]).to.include(
  transaction.status
)

Then we write an expectation asserting that our transactions status must be either "pending" or "complete." Both of these statuses are coming from a TypeScript enum which can be found in src/models/transacation.ts around line 4.

export enum TransactionStatus {
  pending = "pending",
  incomplete = "incomplete",
  complete = "complete",
}

We then write another assertion to make sure that the requestStatus is empty.

expect(transaction.requestStatus).to.be.empty

We then have a couple assertions to make sure that the UI's likes and comment count are correct.

cy.getBySelLike("like-count").should("have.text", `${transaction.likes.length}`)
cy.getBySelLike("comment-count").should(
  "have.text",
  `${transaction.comments.length}`
)

Next, we confirm that the sender and receiver of the transaction are the correct persons.

cy.getBySelLike("sender").should("contain", transaction.senderName)
cy.getBySelLike("receiver").should("contain", transaction.receiverName)

Finally, we are asserting that the amount displayed in the DOM is correct and has the correct css. In the case of this transaction amount, since it is negative, the UI should display a "-" before the dollar amount and make it red.

cy.getBySelLike("amount")
  .should("contain", `-${formattedAmount}`)
  .should("have.css", "color", "rgb(255, 0, 0)")

Now that you understand how we are testing for "paid" transaction items, you can see we are more or less doing the same thing for both "charged" and "requested" transactions in the rest of the test.