Skip to content

Building and Testing a Next.js To-Do App with TypeScript

Posted on:June 28, 2024 at 04:00 PM

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!