Intro
In this blog post, we’ll build a simple to-do app using Next.js and TypeScript. We’ll also cover how to write unit, integration, and end-to-end (E2E) tests using Jest and Cypress. Additionally, we’ll address any configuration issues that may arise during the process.
Setting Up the Project
First, let’s set up a new Next.js project with TypeScript.
Step 1: Create a New Next.js App
npx create-next-app@latest todo-app --typescript
cd todo-app
npm install
This command sets up a new Next.js project with TypeScript support.
Step 2: Create the To-Do Component
Create a new file `components/Todo.tsx` with the following content:
import React, { useState, ChangeEvent } from 'react';
export default function Todo() {
const [task, setTask] = useState<string>('');
const [todos, setTodos] = useState<string[]>([]);
const addTodo = () => {
if (task.trim()) {
setTodos([...todos, task]);
setTask('');
}
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setTask(e.target.value);
};
return (
<div>
<h1>To-Do List</h1>
<input
type="text"
value={task}
onChange={handleInputChange}
placeholder="Add a new task"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}
Step 3: Integrate the To-Do Component
Edit `pages/index.tsx` to include the To-Do component:
import React from 'react';
import Head from 'next/head';
import Todo from '../components/Todo';
export default function Home() {
return (
<div>
<Head>
<title>To-Do App</title>
<meta name="description" content="A simple to-do app built with Next.js" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<Todo />
</main>
</div>
);
}
Writing Tests
Step 4: Install Testing Libraries
Install Jest, Testing Library, and Cypress:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @types/jest @types/react @types/node ts-jest jest-environment-jsdom babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript cypress concurrently wait-on
Step 5: Configure Babel for Jest
Create `babel.config.jest.js`:
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
};
Step 6: Configure Jest
Create `jest.config.js`:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
transform: {
'^.+\\.(ts|tsx|js|jsx)$': ['babel-jest', { configFile: './babel.config.jest.js' }],
},
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
Step 7: Jest Setup File
Create `jest.setup.ts`:
import '@testing-library/jest-dom';
Step 8: Write Unit and Integration Tests
Create `tests/Todo.test.tsx`:
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Todo from '../components/Todo';
describe('Todo Component', () => {
test('renders the to-do input and add button', () => {
render(<Todo />);
const inputElement = screen.getByPlaceholderText(/Add a new task/i);
const buttonElement = screen.getByText(/Add/i);
expect(inputElement).toBeInTheDocument();
expect(buttonElement).toBeInTheDocument();
});
test('adds a new task to the list', () => {
render(<Todo />);
const inputElement = screen.getByPlaceholderText(/Add a new task/i);
const buttonElement = screen.getByText(/Add/i);
fireEvent.change(inputElement, { target: { value: 'New Task' } });
fireEvent.click(buttonElement);
const todoElement = screen.getByText(/New Task/i);
expect(todoElement).toBeInTheDocument();
});
test('clears input after adding a task', () => {
render(<Todo />);
const inputElement = screen.getByPlaceholderText(/Add a new task/i) as HTMLInputElement;
const buttonElement = screen.getByText(/Add/i);
fireEvent.change(inputElement, { target: { value: 'Another Task' } });
fireEvent.click(buttonElement);
expect(inputElement.value).toBe('');
});
});
Step 9: Write End-to-End Tests
Create `cypress/e2e/todo.cy.ts`:
describe('Todo App', () => {
it('should add a new task to the list', () => {
cy.visit('http://localhost:3000'); // Ensure this URL is correct
// Interact with the input field and add button
cy.get('input[placeholder="Add a new task"]').type('New Task');
cy.get('button').contains('Add').click();
// Verify the new task is added to the list
cy.contains('li', 'New Task').should('be.visible');
});
});
Step 10: Run the Tests
Add the following scripts to `package.json`:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"test:e2e": "concurrently \"npm run dev\" \"wait-on http://localhost:3000 && npm run cypress:run\"",
"test": "jest"
}
Run the unit and integration tests:
npm test
Run the end-to-end tests:
npm run test:e2e
Conclusion
In this blog post, we built a simple to-do app using Next.js and TypeScript, and wrote unit, integration, and end-to-end tests using Jest and Cypress. We also resolved configuration issues related to using SWC for Next.js and Babel for Jest. This comprehensive approach ensures that our application is thoroughly tested and robust.
Feel free to expand this app and add more features and tests as needed. Happy coding!