React: Write end to end test using cypress

React: Write end to end test using cypress

Write end to end test using react cypress

Write end to end test using react cypress

What is cypress?

Cypress is a simple to use testing framework. It has many built-in features to make your life easier and it's easy to learn and use.
Cypress is very powerful, you can create unit tests without any configuration or scaffolding. You just write your code and then run tests in parallel with zero dependencies!. You can use cypress with other frameworks like Angular, Vue or React Native too!
It is a more developer-friendly tool that uses a unique DOM manipulation technique and operates directly in the browser.

What is the e-2-e test?

E-2-E testing is a type of test that tests the entire application. It can be also known as an integration test, because it tests the interaction between different components and modules.
It is not a unit test (as it does not have any dependency on individual modules or components), but rather an end to end one.

Why choose cypress?

Cypress is a great tool to use when you want to write end to end tests. It’s easy to use and fast, so you don’t need to worry about it being slow or complicated. Cypress also has many plugins that allow you to do more advanced testing on your app.

Setup cypress with react

React app

To set up cypress first create your react app, in this example we will be using a simple todo app using a create-react-app.

First create react app in your project folder

npx create-react-app todo

Clean all the unwanted files in the src folder and codes and create a folder structure like this or you can use your own structure

Folder and files structure

in app.jsx add

import { useEffect, useState } from "react";
import Addtodo from "./addtodo";
import "./app.css";
import Todo from "./todo";

const App = () => {
  const [todos, setTodos] = useState([]);
  const [keyword, setKeyword] = useState("All");
  const [filteredTodos, setFilteredTodos] = useState(todos);
  const addTodo = (todo) => {
    setTodos([todo, ...todos]);
    setKeyword("All");
  };

  const updateTodo = (id) => {
    let todosCopy = todos;
    const todoIndex = todosCopy.findIndex((el) => el.id === id);
    const todo = todosCopy.find((el) => el.id === id);
    todosCopy[todoIndex] = {
      ...todo,
      isActive: !todo.isActive,
    };
    setTodos([...todosCopy]);
  };

  const deleteTodo = (id) => {
    const newTodos = todos.filter((el) => el.id !== id);
    setTodos(newTodos);
  };

  useEffect(() => {
    filterTodos(keyword);
  }, [todos, keyword]);

  const filterTodos = (key) => {
    if (key === "All") {
      setFilteredTodos(todos);
      setKeyword("All");
      return;
    }
    if (key === "Active") {
      setFilteredTodos(todos.filter((el) => el.isActive === true));
      setKeyword("Active");
      return;
    }
    if (key === "Completed") {
      setFilteredTodos(todos.filter((el) => el.isActive === false));
      setKeyword("Completed");
      return;
    }
    if (key === "Clear") {
      setTodos(todos.filter((el) => el.isActive === true));
      setKeyword("All");
    }
  };

  return (
    <div className="App">
      <div className="container">
        <Addtodo addTodo={addTodo} />
        <ul>
          {filteredTodos.map((todo) => (
            <Todo
              todo={todo}
              key={todo.id}
              updateTodo={updateTodo}
              deleteTodo={deleteTodo}
            />
          ))}
        </ul>
        <div className="actions">
          <span>
            {todos.filter((el) => el.isActive === true).length} Active items
            left
          </span>
          &nbsp;
          <div>
            <button
              className="filter-all"
              onClick={() => {
                filterTodos("All");
              }}
            >
              All
            </button>
            &nbsp;
            <button
              className="filter-active"
              onClick={() => {
                filterTodos("Active");
              }}
            >
              Active
            </button>
            &nbsp;
            <button
              className="filter-completed"
              onClick={() => {
                filterTodos("Completed");
              }}
            >
              Completed
            </button>
            &nbsp;
            <button
              className="delete-all"
              onClick={() => {
                filterTodos("Clear");
              }}
            >
              Clear Completed
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default App;

In addTodo.jsx

import { useState } from "react";

const Addtodo = ({ addTodo }) => {
  const [todo, setTodo] = useState({
    id: "",
    title: "",
    isActive: true,
  });
  const { title } = todo;
  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          todo?.title?.trim()?.length > 0 && addTodo(todo);
          setTodo({
            id: "",
            title: "",
            isActive: true,
          });
        }}
      >
        <input
          placeholder="New todo"
          value={title}
          onChange={(e) => {
            let id = new Date();
            id = id.getTime();
            if (e.target.value?.trim().length > 0)
              setTodo({
                ...todo,
                id,
                title: e.target.value,
              });
          }}
        />
        <button className="submit-button" title="Add Todo">
          <svg
            width="12"
            height="12"
            viewBox="0 0 12 12"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M6.75 5.25V0H5.25V5.25H0V6.75H5.25V12H6.75V6.75H12V5.25H6.75Z"
              fill="white"
            />
          </svg>
        </button>
      </form>
    </div>
  );
};

export default Addtodo;

in todo.tsx add

const Todo = ({ todo, deleteTodo, updateTodo }) => {
  const { title, id, isActive } = todo;
  return (
    <li className={isActive ? "incomplete" : "complete"}>
      <div>
        <button
          className={isActive ? "mark mark-complete" : "mark mark-incomplete"}
          onClick={() => {
            updateTodo(id);
          }}
          title={isActive ? "Mark complete" : "Mark incomplete"}
        >
          <svg
            width="13"
            height="10"
            viewBox="0 0 13 10"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M10.455 0.45498C10.6663 0.253813 10.9475 0.1426 11.2392 0.144808C11.531 0.147015 11.8104 0.262471 12.0187 0.466812C12.2269 0.671152 12.3476 0.948396 12.3553 1.24004C12.363 1.53169 12.2571 1.81492 12.06 2.02998L6.07499 9.51498C5.97208 9.62583 5.84787 9.71478 5.70979 9.77653C5.57171 9.83828 5.4226 9.87155 5.27137 9.87435C5.12014 9.87715 4.9699 9.84942 4.82963 9.79283C4.68936 9.73624 4.56194 9.65194 4.45499 9.54498L0.485992 5.57598C0.375462 5.47299 0.286809 5.34879 0.225321 5.21079C0.163833 5.07279 0.13077 4.92382 0.128105 4.77276C0.12544 4.62171 0.153227 4.47167 0.209808 4.33158C0.26639 4.1915 0.350607 4.06425 0.457435 3.95742C0.564263 3.85059 0.691514 3.76638 0.831596 3.7098C0.971678 3.65321 1.12172 3.62543 1.27278 3.62809C1.42383 3.63076 1.5728 3.66382 1.7108 3.72531C1.8488 3.7868 1.973 3.87545 2.07599 3.98598L5.21699 7.12548L10.4265 0.48798C10.4359 0.476432 10.4459 0.465414 10.4565 0.45498H10.455Z"
              fill="green"
            />
          </svg>
        </button>
        &nbsp;
        {title}&nbsp;
      </div>
      <button
        onClick={() => {
          deleteTodo(id);
        }}
        className="delete"
        title="Delete todo"
      >
        <svg
          width="16"
          height="16"
          viewBox="0 0 20 20"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M8 4H12C12 3.46957 11.7893 2.96086 11.4142 2.58579C11.0391 2.21071 10.5304 2 10 2C9.46957 2 8.96086 2.21071 8.58579 2.58579C8.21071 2.96086 8 3.46957 8 4V4ZM6 4C6 2.93913 6.42143 1.92172 7.17157 1.17157C7.92172 0.421427 8.93913 0 10 0C11.0609 0 12.0783 0.421427 12.8284 1.17157C13.5786 1.92172 14 2.93913 14 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5C20 5.26522 19.8946 5.51957 19.7071 5.70711C19.5196 5.89464 19.2652 6 19 6H18.118L17.232 16.34C17.1468 17.3385 16.69 18.2686 15.9519 18.9463C15.2137 19.6241 14.2481 20.0001 13.246 20H6.754C5.75191 20.0001 4.78628 19.6241 4.04815 18.9463C3.31002 18.2686 2.85318 17.3385 2.768 16.34L1.882 6H1C0.734784 6 0.48043 5.89464 0.292893 5.70711C0.105357 5.51957 0 5.26522 0 5C0 4.73478 0.105357 4.48043 0.292893 4.29289C0.48043 4.10536 0.734784 4 1 4H6ZM13 10C13 9.73478 12.8946 9.48043 12.7071 9.29289C12.5196 9.10536 12.2652 9 12 9C11.7348 9 11.4804 9.10536 11.2929 9.29289C11.1054 9.48043 11 9.73478 11 10V14C11 14.2652 11.1054 14.5196 11.2929 14.7071C11.4804 14.8946 11.7348 15 12 15C12.2652 15 12.5196 14.8946 12.7071 14.7071C12.8946 14.5196 13 14.2652 13 14V10ZM8 9C8.26522 9 8.51957 9.10536 8.70711 9.29289C8.89464 9.48043 9 9.73478 9 10V14C9 14.2652 8.89464 14.5196 8.70711 14.7071C8.51957 14.8946 8.26522 15 8 15C7.73478 15 7.48043 14.8946 7.29289 14.7071C7.10536 14.5196 7 14.2652 7 14V10C7 9.73478 7.10536 9.48043 7.29289 9.29289C7.48043 9.10536 7.73478 9 8 9V9ZM4.76 16.17C4.8026 16.6694 5.03117 17.1346 5.40044 17.4735C5.76972 17.8124 6.25278 18.0003 6.754 18H13.246C13.7469 17.9998 14.2294 17.8117 14.5983 17.4728C14.9671 17.134 15.1954 16.6691 15.238 16.17L16.11 6H3.89L4.762 16.17H4.76Z"
            fill="red"
          />
        </svg>
      </button>
    </li>
  );
};

export default Todo;

in app.css add the following codes for UI

body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 90vh;
  background-color: #1e272e;
  color: white;
}

.container {
  background-color: #2b343b;
  padding: 2rem;
  max-width: 450px;
  width: 90%;
  min-width: 250px;
  margin: auto;
}

form {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 1.5rem;
}

input {
  height: 40px;
  width: calc(100% - 80px);
  background-color: #1e272e;
  border: none;
  border-bottom: 1px solid #1e272e;
  padding: 0 1rem;
  transition: 0.25s ease-out;
  color: white;
}

button {
  background: none;
  color: white;
  border: none;
}

button.submit-button {
  width: 80px;
  height: 40px;
  background-color: cadetblue;
  color: white;
  border: none;
}

input:focus {
  border: none;
  background-color: #2b343b;
  border-bottom: 1px solid grey;
}

input:focus,
button:focus {
  outline: none !important;
}

ul {
  list-style: none;
  color: white;
  padding-inline-start: 0;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
}

ul::-webkit-scrollbar {
  width: 0.3rem;
}
ul::-webkit-scrollbar-track {
  background: gray;
}
ul::-webkit-scrollbar-thumb {
  background: darkgrey;
  border-radius: 0.5em;
}

ul li {
  display: flex;
  justify-content: space-between;
  background-color: #1e272e;
  padding: 0.5rem;
  margin: 0.5rem 0;
  transition: 0.22s;
}

ul li.complete {
  text-decoration: line-through;
  opacity: 0.5;
}

.actions {
  display: flex;
  justify-content: space-between;
  margin-top: 2rem;
}

.actions button {
  color: lightgray;
}

.actions button.delete-all {
  color: red;
}

Run npm start and this is how it should look

Todo UI

From this simple app, user should be able to;

  1. Create a new todo and preview the new todo on the list
  2. Mark a todo as complete or incomplete
  3. Delete a single todo
  4. Filter all, complete and incomplete todos
  5. Delete all complete todos

These are what we'll be testing using cypress

Cypress setup

  • install cypress in your project

    npm install cypress -D
    
  • Run cypress open to configure cypress in your project

    npx cypress open
    
  • This will open a window with two options, e2e testing and component testing, select e2e testing

Cypress window

  • This will open configure files page, scroll to the bottom and continue, cypress will generate all those price in your project folder

Files configuration

  • This will open browser selection page, select your preferable browser

Browser selection page

  • The following folder structure will be created in your project after configuring files

Cypress folder structure

  • Create e2e folder inside cypress folder and create todo.cy.js inside e2e folder and start writing the tests.
  • Once you create this file, in cypress window you should see a file you created in e2e test list like this

e2e spec file list

  • to run a specific test file, click on the file and it should run.

Write your first e2e tests

Before writing test on any scenario first we need to learn cypress commands, there are basic custom commands. basic commands are the built in cypress commands while custom commands are the commands you can create. For this post we'll learn about basic commands since we won't need any custom command.

The following is the list of some basic commands


cy.visit("url") // this launches url

cy.wait(300) // Wait for a certain time in milliseconds or for an aliased element prior to moving the following step.

cy.get("selector") // this returns single or multiple elements with provided selector i.e classname, tagname, id or title
// class selector can be written as cy.get(".classname")

cy.contains("text") // returns a single or multiple elements that contains text

cy.click() // clicks the selected element

cy.dblclick() // double clicks the selected element

cy.document() // It obtains window.document on the active page.

cy.type("text") // type text on a selected input

cy.get(".classname").eq(0) // for multiple elements you can select a specific element depending on the position of such element using eq command with element index

cy.should("exist") // checks if a selected element exists or not, there are multiple parameters thet can be used inside should for varios checks 
// i.e not.exist, be.visible, be.enabled
// it can be used with an alias .and for multiple checks ie
// cy.get(".classname").should("exist).and("be.visible")

For more basic commands visit here.

Writing cypress tests works almost the same as writing any other tests. Before all start the app by running

npm start

Assuming the app runs on http://localhost:3000, before any testing any scenario first launch a url using visit command.

describe("create todo spec", () => {
  before(() => {
    // this will launch the page in cypress browser
    cy.visit("http://localhost:3000");
  });
})

Lets write tests in todo.cy.js by scenarios listed before.

describe("create todo spec", () => {
  before(() => {
    // this will launch the page in cypress browser
    cy.visit("http://localhost:3000");
  });

  // 1. Create a todo scenario
  it("successfully create a todo", () => {
    // find an input by tagname
    cy.get("input").type("Sprint meeting");

    // submit form by title of submit button
    cy.get("[title='Add Todo']").click();

    // create another todo
    cy.get("input").type("Coding");

    // submit form by classname of submit button
    cy.get(".submit-button").click();

    // check if new todos exist
    cy.contains("Sprint meeting").should("exist");
    cy.contains("Coding").should("exist");
  });

  // 2. Mark todo as complete / incomplete
  it("Should mark todo", () => {
    // mark a first todo as complete
    cy.get("button.mark-complete").eq(0).click();

    // select using css selector
    cy.get("li:nth-child(2) button.mark").click();
  });

  // 3. Delete a todo
  it("Should delete a todo", () => {
    cy.get("li:nth-child(2) button.delete").click();
  }) 

  // 4. Filter all, complete and incomplete todos
  it("should filter between all, complete and incomplete todos", () => {
    // create multiple todos
    cy.get("input").type("Sprint meeting");
    cy.get("[title='Add Todo']").click();
    cy.get("input").type("Code");
    cy.get("[title='Add Todo']").click();
    cy.get("input").type("Exercising");
    cy.get("[title='Add Todo']").click();
    cy.get("input").type("Read");
    cy.get("[title='Add Todo']").click();
    cy.get("input").type("Eat");
    cy.get("[title='Add Todo']").click();
    cy.get("input").type("Code");
    cy.get("[title='Add Todo']").click();

    // mark some as complete
    cy.get("li:nth-child(2) button.mark").click();
    cy.get("li:nth-child(4) button.mark").click();
    cy.get("li:nth-child(6) button.mark").click();

    // filter complete only
    cy.get("button.filter-completed").click();

    // check if only complete exists
    cy.get(".complete").should("exist");
    cy.get(".incomplete").should("not.exist");

    // filter incomplete only
    cy.get("button.filter-active").click();
    cy.get(".complete").should("not.exist");
    cy.get(".incomplete").should("exist");

    // filter all
    cy.get("button.filter-all").click();
    cy.get(".complete").should("exist");
    cy.get(".incomplete").should("exist");
  }); 

  // 5. Delete all complete todos
  it("should delete all completed todos", () => {
    // find and click delete all completed todos
    cy.get("button.delete-all").click();
    cy.get(".complete").should("not.exist");
  });

})

Running cypress run command should show a video of all actions written on the test.

Cypress video

These tests can run using electron browser, this means that you can run these tests on terminal ie

npx cypress run

Cypress terminal results

Cypress can do multiple types tests using basic commands and different plugins such as file uploader. At clickpesa we use cypress to do the e2e tests making sure we have

The following are limitations of cypress

  • One cannot use Cypress to drive two browsers at the same time.
  • It doesn't provide support for multi-tabs.
  • Cypress only supports JavaScript for creating test cases.
  • Cypress doesn't provide support for browsers like Safari and IE at the moment.
  • Limited support for iFrames.

The most popular alternative to cypress is Selenium

If you like this article there are more like this in our blogs, follow us on dev.to/clickpesa, medium.com/clickpesa-engineering-blog and clickpesa.hashnode.dev

Happy Hacking!!