How Much Does It Cost to Hire a ReactJS Developer
Wondering how much it costs to hire a ReactJS developer? Learn about hourly rates, monthly costs, and factors affecting pricing for offshore and in-house talent.
End-to-End (E2E) testing is an essential step in validating that your application behaves correctly under real-world conditions. While unit tests focus on verifying small, isolated pieces of logic, E2E tests simulate complete user workflows such as logging into an application, creating tasks, and verifying expected outcomes.
In this tutorial, we will build a simple full-stack application and implement E2E tests to ensure it functions as intended. Specifically, we will cover:
Developing a React frontend with a login feature and a Todo application.
Building a Node.js backend API to manage tasks.
Writing Cypress E2E tests that validate login and Todo operations, including both positive and negative scenarios.
By the end, you will have a clear understanding of how to set up E2E testing for a modern web application and ensure reliability across critical user flows.
Here’s how the full project will look:
todo-app/
├── backend/ # Node.js + Express backend
│ ├── server.js
│ └── package.json
├── frontend/ # React frontend (Vite)
│ ├── src/
│ │ ├── App.jsx
│ │ └── App.css
│ ├── index.html
│ └── package.json
└── cypress/ # Cypress E2E tests
│ ├── e2e/
│ │ └── todo.cy.js
│ ├── support/
│ └── cypress.config.js
Create a backend/ folder and install dependencies:
mkdir backend && cd backend
npm init -y
npm install express cors body-parser
server.js – A simple Express server with in-memory storage:
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
app.use(cors());
app.use(bodyParser.json());
const PORT = 5000;
let users = [{ username: 'test', password: '1234' }];
let todos = [];
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (user) {
return res.json({ success: true, token: 'fake-jwt-token' });
}
res.status(401).json({ success: false, message: 'Invalid credentials.' });
});
app.get('/todos', (req, res) => {
res.json(todos);
});
app.post('/todos', (req, res) => {
const { task } = req.body;
todos.push({ id: Date.now(), task });
res.status(201).json({ success: true });
});
// Delete todo
app.delete('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
todos = todos.filter(todo => todo.id !== id);
res.json({ success: true });
});
// Update todo
app.put('/todos/:id', (req, res) => {
const id = parseInt(req.params.id);
const { task } = req.body;
const todo = todos.find(todo => todo.id === id);
if (todo) {
todo.task = task;
return res.json(todo);
}
res.status(404).json({ success: false, message: 'Todo not found' });
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
Users & Todos are stored in memory for simplicity.
The login route checks credentials and returns a fake JWT token on success.
CRUD operations are provided for todos (GET, POST, DELETE, PUT).
Create a React app:
npm create vite@latest frontend --template react
cd frontend
npm install
npm install axios
npm run dev
src/App.jsx – React frontend with login and todo functionality:
import { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
const API = "http://localhost:5000";
function App() {
const [token, setToken] = useState(null);
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
const [loginInfo, setLoginInfo] = useState({ username: "", password: "" });
const [editingId, setEditingId] = useState(null);
const [editingTask, setEditingTask] = useState("");
const [message, setMessage] = useState("");
const login = async () => {
if (!loginInfo.username.trim() && !loginInfo.password.trim()) {
setMessage("Username and Password are required.");
return;
}
try {
const res = await axios.post(`${API}/login`, loginInfo);
setToken(res.data.token);
} catch (err) {
console.log("error", err.response.data);
setMessage(err.response.data.message);
}
};
const fetchTodos = async () => {
const res = await axios.get(`${API}/todos`);
setTodos(res.data);
};
const addTodo = async () => {
if (!task.trim()) return;
await axios.post(`${API}/todos`, { task });
setTask("");
fetchTodos();
};
const deleteTodo = async (id) => {
await axios.delete(`${API}/todos/${id}`);
fetchTodos();
};
const startEdit = (todo) => {
setEditingId(todo.id);
setEditingTask(todo.task);
};
const cancelEdit = () => {
setEditingId(null);
setEditingTask("");
};
const saveEdit = async () => {
if (!editingTask.trim()) return;
await axios.put(`${API}/todos/${editingId}`, { task: editingTask });
setEditingId(null);
setEditingTask("");
fetchTodos();
};
useEffect(() => {
if (token) fetchTodos();
}, [token]);
if (!token) {
return (
<div className="container">
<div className="card">
<h2>Login</h2>
<input
type="text"
placeholder="Username"
value={loginInfo.username}
className="input1"
onChange={(e) =>
setLoginInfo({ ...loginInfo, username: e.target.value })
}
/>
<input
type="password"
placeholder="Password"
value={loginInfo.password}
className="input2"
onChange={(e) =>
setLoginInfo({ ...loginInfo, password: e.target.value })
}
/>
{message && (
<div id="err-msg" className="err-msg">
{message}
</div>
)}
<button id="login-btn" className="btn" onClick={login}>
Login
</button>
</div>
</div>
);
}
return (
<div className="container">
<div className="card">
<h2>Todo App</h2>
<div className="todo-form">
<input
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Enter task..."
/>
<button onClick={addTodo} className="btn">
Add
</button>
</div>
<ul className="todo-list">
{todos.map((todo, index) => (
<li key={todo.id} id={`task-${index}`} className="todo-item">
{editingId === todo.id ? (
<>
<input
value={editingTask}
id="edit-int"
onChange={(e) => setEditingTask(e.target.value)}
/>
<div className="todo-actions">
<button className="btn small" onClick={saveEdit}>
Save
</button>
<button className="btn small cancel" onClick={cancelEdit}>
Cancel
</button>
</div>
</>
) : (
<>
<span>{todo.task}</span>
<div className="todo-actions">
<button
className="btn small"
onClick={() => startEdit(todo)}
>
Edit
</button>
<button
className="btn small delete"
onClick={() => deleteTodo(todo.id)}
>
Delete
</button>
</div>
</>
)}
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
login() calls the backend login API and sets a token if successful.
fetchTodos() fetches the list of todos from the server.
addTodo(), deleteTodo(), startEdit(), and saveEdit() manage CRUD operations.
Conditional rendering: shows login form if not authenticated, else shows the Todo App.
src/App.css- Css file for styling
body {
margin: 0;
font-family: "Segoe UI", sans-serif;
background: #f2f2f2;
}
.container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.card {
background: #fff;
padding: 2rem;
border-radius: 12px;
width: 100%;
max-width: 500px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
}
h2 {
margin-bottom: 1.5rem;
text-align: center;
color: #333;
}
input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
}
.btn {
background: #007bff;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s ease;
}
.btn:hover {
background: #0056b3;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
background: #f8f9fa;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.75rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.todo-actions {
display: flex;
gap: 1rem; /* creates space between Edit, Delete, Save, Cancel */
flex-shrink: 0;
}
.todo-item input {
flex: 1;
margin-right: 1rem; /* space between input and buttons when editing */
}
.todo-item.editing {
align-items: flex-start;
flex-wrap: wrap;
}
.todo-form {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.todo-form input {
flex: 1;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-list li {
background: #f8f9fa;
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.75rem;
font-size: 1rem;
color: #333;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition: background 0.2s;
}
.todo-list li:hover {
background: #e9ecef;
}
.input1 {
margin-bottom: 1rem;
}
.input2 {
margin-bottom: 1rem;
}
.err-msg {
color: red;
margin-bottom: 10px;
}
UI screenshots,
npm install --save-dev cypress(This will create cypress folder)
npx cypress open
cypress.config.js:
export default defineConfig({
e2e: { baseUrl: "http://localhost:5173" },
});
cypress/e2e/todo.cy.js:
/* eslint-env cypress */
describe("React Todo App - Full Flow", () => {
const username = "test";
const password = "1234";
const visitUrl = "http://localhost:5173";
Cypress.Commands.add("login", (u = username, p = password) => {
cy.visit(visitUrl);
cy.get('input[placeholder="Username"]').type(u);
cy.get('input[placeholder="Password"]').type(p);
cy.get("#login-btn").click();
});
//Login Success
it("logs in successfully with correct credentials", () => {
cy.login();
cy.contains("Todo App").should("exist");
});
//Invalid credentials
it("shows error on invalid credentials", () => {
cy.login("wronguser", "wrongpass");
cy.get("#err-msg").should("contain", "Invalid credentials.");
});
//Empty credentials
it("prevents login with empty credentials", () => {
cy.visit("http://localhost:5173");
cy.get("#login-btn").click();
cy.get("#err-msg").should("contain", "Username and Password are required.");
});
//Add task
it("adds a new task", () => {
cy.login();
cy.get('input[placeholder="Enter task..."]').type("test1");
cy.contains("Add").click();
cy.get("#task-0").should("contain", "test1");
});
// Edit task
it("edits a task", () => {
cy.login();
cy.get("#task-0").should("contain", "test1");
cy.contains("Edit").click();
cy.get("#edit-int").clear().type("test1 updated");
cy.contains("Save").click();
cy.get("#task-0").should("contain", "test1 updated");
});
//Delete task
it("deletes a task", () => {
cy.login();
cy.get('input[placeholder="Enter task..."]').type("test2");
cy.contains("Add").click();
cy.get("#task-1");
cy.contains("Delete").click();
cy.get("ul").should("not.contain", "test1 updated");
});
});
Custom command cy.login() simplifies logging in for tests.
Each test checks a single functionality: login, invalid credentials, add, edit, and delete tasks.
Assertions like should("contain", ...) validate that UI reflects the expected state.
# Terminal 1 - Backend
cd backend && node server.js
# Terminal 2 - Frontend
cd frontend && npm run dev
# Terminal 3 - Cypress
npx cypress open
here you can screenshot of all the succeed test cases
Wondering how much it costs to hire a ReactJS developer? Learn about hourly rates, monthly costs, and factors affecting pricing for offshore and in-house talent.
Discover the top 10 reasons why ReactJS is the best choice for enterprise web app development — from scalability and performance to strong community support.
Explore a practical guide to integrating Angular components inside React and VueJS apps. Ideal for hybrid frontend teams seeking reusable solutions.
Get in touch with Prishusoft – your trusted partner for custom software development. Whether you need a powerful web application or a sleek mobile app, our expert team is here to turn your ideas into reality.