├── .eslintrc.json ├── .gitignore ├── README.md ├── jest.config.js ├── jest.setup.js ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── __tests__ │ │ └── Home.test.tsx │ ├── components │ │ ├── AddTodo │ │ │ ├── AddTodo.tsx │ │ │ └── __tests__ │ │ │ │ └── AddTodo.test.tsx │ │ ├── Header │ │ │ ├── Header.tsx │ │ │ └── __tests__ │ │ │ │ └── Header.test.tsx │ │ ├── Navbar.tsx │ │ ├── TodoItem │ │ │ ├── TodoItem.tsx │ │ │ └── __tests__ │ │ │ │ └── TodoItem.test.tsx │ │ └── TodoList │ │ │ ├── TodoList.tsx │ │ │ └── __tests__ │ │ │ └── TodoList.test.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx └── types │ └── Todo.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:testing-library/react", 5 | "plugin:jest-dom/recommended" 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "Testing a Next.js App" 2 | 3 | ## with Jest, React Testing Library, TypeScript 4 | 5 | --- 6 | 7 | ### Author Links 8 | 9 | 👋 Hello, I'm Dave Gray. 10 | 11 | 👉 [My Courses](https://courses.davegray.codes/) 12 | 13 | ✅ [Check out my YouTube Channel with hundreds of tutorials](https://www.youtube.com/DaveGrayTeachesCode). 14 | 15 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn) 16 | 17 | ☕ [Buy Me A Coffee](https://buymeacoffee.com/DaveGray) 18 | 19 | 🚀 Follow Me: 20 | 21 | - [Twitter](https://twitter.com/yesdavidgray) 22 | - [LinkedIn](https://www.linkedin.com/in/davidagray/) 23 | - [Blog](https://yesdavidgray.com) 24 | - [Reddit](https://www.reddit.com/user/DaveOnEleven) 25 | 26 | --- 27 | 28 | ### Description 29 | 30 | 📺 [YouTube Video](https://youtu.be/XTNqyEBPAFw) for this repository. 31 | 32 | ### 📚 References 33 | - 🔗 [Next.js Official Site](https://nextjs.org/) 34 | - 🔗 [TypeScript Official Site](https://www.typescriptlang.org/) 35 | - 🔗 [Jest Official Site](https://jestjs.io/) 36 | - 🔗 [React Testing Library Official Site](https://testing-library.com/docs/react-testing-library/intro) 37 | 38 | --- 39 | 40 | ### ⚙ Free Web Dev Tools 41 | - 🔗 [Google Chrome Web Browser](https://google.com/chrome/) 42 | - 🔗 [Visual Studio Code (aka VS Code)](https://code.visualstudio.com/) 43 | - 🔗 [ES7 React Snippets](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets) 44 | 45 | --- 46 | 47 | ### 🎓 Academic Honesty 48 | 49 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiarism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/). -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: './', 6 | }) 7 | 8 | // Add any custom config to be passed to Jest 9 | /** @type {import('jest').Config} */ 10 | const config = { 11 | setupFilesAfterEnv: ['/jest.setup.js'], 12 | testEnvironment: 'jest-environment-jsdom', 13 | preset: 'ts-jest', 14 | verbose: true, 15 | } 16 | 17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 18 | module.exports = createJestConfig(config) 19 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | import '@testing-library/jest-dom/extend-expect' -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-testing-example-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest", 11 | "test:watch": "jest --watchAll" 12 | }, 13 | "dependencies": { 14 | "@types/node": "20.5.7", 15 | "@types/react": "18.2.21", 16 | "@types/react-dom": "18.2.7", 17 | "autoprefixer": "10.4.15", 18 | "eslint": "8.48.0", 19 | "eslint-config-next": "13.4.19", 20 | "next": "^13.4.19", 21 | "postcss": "8.4.29", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-icons": "^4.10.1", 25 | "tailwindcss": "3.3.3", 26 | "typescript": "5.2.2" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/jest-dom": "^5.16.5", 30 | "@testing-library/react": "^14.0.0", 31 | "@testing-library/user-event": "^14.4.3", 32 | "eslint-plugin-jest-dom": "^5.1.0", 33 | "eslint-plugin-testing-library": "^6.0.1", 34 | "jest": "^29.6.4", 35 | "jest-environment-jsdom": "^29.6.4", 36 | "ts-jest": "^29.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/__tests__/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | import Home from '../page' 4 | 5 | describe('Home', () => { 6 | 7 | it('should add a new todo', async () => { 8 | render() // ARRANGE 9 | 10 | // ACT 11 | const input = screen.getByPlaceholderText('New Todo') 12 | await userEvent.type(input, 'My new todo') 13 | expect(input).toHaveValue('My new todo') // ASSERT 14 | 15 | // ACT 16 | const button = screen.getByRole('button', { 17 | name: 'Submit' 18 | }) 19 | await userEvent.click(button) 20 | expect(input).toHaveValue("") // ASSERT 21 | 22 | const data = await screen.findByText('My new todo') 23 | expect(data).toHaveTextContent('My new todo') 24 | }) 25 | 26 | it('should update a todo', async () => { 27 | render() // ARRANGE 28 | 29 | // ACT 30 | const checkbox = screen.getAllByRole('checkbox')[0] as HTMLInputElement 31 | expect(checkbox.checked).toBeFalsy() 32 | await userEvent.click(checkbox) 33 | expect(checkbox.checked).toBeTruthy() // ASSERT 34 | 35 | }) 36 | 37 | it('should delete a todo', async () => { 38 | render() // ARRANGE 39 | 40 | const todoText = screen.queryByText('Write Code 💻') 41 | expect(todoText).toBeInTheDocument() // ASSERT 42 | 43 | // ACT 44 | const button = screen.getAllByTestId('delete-button')[0] 45 | await userEvent.click(button) 46 | 47 | expect(todoText).not.toBeInTheDocument() // ASSERT 48 | 49 | }) 50 | 51 | }) -------------------------------------------------------------------------------- /src/app/components/AddTodo/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FormEvent } from "react" 2 | import type { Todo } from "@/types/Todo" 3 | 4 | type Props = { 5 | setTodos: React.Dispatch> 6 | } 7 | 8 | export default function AddItemForm({ setTodos }: Props) { 9 | const [item, setItem] = useState("") 10 | 11 | const handleSubmit = async (e: FormEvent) => { 12 | e.preventDefault() 13 | 14 | if (!item) return 15 | 16 | setTodos(prev => { 17 | const highestId = [...prev].sort((a, b) => b.id - a.id)[0].id 18 | 19 | return [...prev, { userId: 1, title: item, completed: false, id: highestId + 1 }] 20 | }) 21 | 22 | setItem("") 23 | 24 | } 25 | 26 | return ( 27 |
28 | 29 | 30 | setItem(e.target.value)} 36 | className="text-2xl p-1 rounded-lg flex-grow w-full" 37 | placeholder="New Todo" 38 | autoFocus 39 | /> 40 | 41 | 48 | 49 |
50 | ) 51 | } -------------------------------------------------------------------------------- /src/app/components/AddTodo/__tests__/AddTodo.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | import AddTodo from '../AddTodo' 4 | 5 | const mockSetTodos = jest.fn() 6 | 7 | describe('AddTodo', () => { 8 | 9 | describe('Render', () => { 10 | 11 | it('should render the input', () => { 12 | render() // ARRANGE 13 | 14 | const input = screen.getByPlaceholderText('New Todo') //ACT 15 | 16 | expect(input).toBeInTheDocument()// ASSERT 17 | }) 18 | 19 | it('should render a disabled submit button', () => { 20 | render() // ARRANGE 21 | 22 | //ACT 23 | const button = screen.getByRole('button', { 24 | name: 'Submit' 25 | }) 26 | 27 | expect(button).toBeDisabled()// ASSERT 28 | }) 29 | 30 | }) 31 | 32 | describe('Behavior', () => { 33 | 34 | it('should be able to add text to the input', async () => { 35 | render() // ARRANGE 36 | 37 | const input = screen.getByPlaceholderText('New Todo') //ACT 38 | await userEvent.type(input, 'hey there') 39 | expect(input).toHaveValue("hey there")// ASSERT 40 | }) 41 | 42 | it('should enable the submit button when text is input', async () => { 43 | render() // ARRANGE 44 | 45 | const input = screen.getByPlaceholderText('New Todo') //ACT 46 | await userEvent.type(input, 'hey there') 47 | 48 | const button = screen.getByRole('button', { 49 | name: 'Submit' 50 | }) 51 | 52 | expect(button).toBeEnabled() // ASSERT 53 | }) 54 | 55 | it('should empty the text input when submitted', async () => { 56 | render() // ARRANGE 57 | 58 | const input = screen.getByPlaceholderText('New Todo') //ACT 59 | await userEvent.type(input, 'hey there') 60 | const button = screen.getByRole('button', { 61 | name: 'Submit' 62 | }) 63 | await userEvent.click(button) 64 | 65 | expect(input).toHaveValue("")// ASSERT 66 | }) 67 | 68 | it('should call setTodos when submitted', async () => { 69 | render() // ARRANGE 70 | 71 | const input = screen.getByPlaceholderText('New Todo') //ACT 72 | await userEvent.type(input, 'hey there') 73 | const button = screen.getByRole('button', { 74 | name: 'Submit' 75 | }) 76 | await userEvent.click(button) 77 | 78 | expect(mockSetTodos).toBeCalled()// ASSERT 79 | }) 80 | 81 | }) 82 | }) -------------------------------------------------------------------------------- /src/app/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | export default function Header({ title }: { title: string }) { 2 | return ( 3 |

4 | {title} 5 |

6 | ) 7 | } -------------------------------------------------------------------------------- /src/app/components/Header/__tests__/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import Header from '../Header' 3 | 4 | describe('Header', () => { 5 | 6 | it('should render the "Next Todos" heading', () => { 7 | render(
) // ARRANGE 8 | 9 | //ACT 10 | const header = screen.getByRole('heading', { 11 | name: 'Next Todos' 12 | }) 13 | 14 | expect(header).toBeInTheDocument()// ASSERT 15 | }) 16 | 17 | it('should render "Dave" as a heading', async () => { 18 | render(
) // ARRANGE 19 | 20 | //ACT 21 | const header = screen.getByRole('heading', { 22 | name: 'Dave' 23 | }) 24 | 25 | expect(header).toBeInTheDocument()// ASSERT 26 | }) 27 | }) -------------------------------------------------------------------------------- /src/app/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Header from "./Header/Header" 2 | 3 | export default function Navbar() { 4 | return ( 5 | 10 | ) 11 | } -------------------------------------------------------------------------------- /src/app/components/TodoItem/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { FaTrash } from "react-icons/fa" 3 | import { ChangeEvent, MouseEvent } from 'react' 4 | // import updateTodo from "@/lib/updateTodo" 5 | // import deleteTodo from "@/lib/deleteTodo" 6 | import type { Todo } from "@/types/Todo" 7 | 8 | 9 | type Props = { 10 | todo: Todo, 11 | setTodos: React.Dispatch> 12 | } 13 | 14 | export default function TodoItem({ todo, setTodos }: Props) { 15 | 16 | const handleChange = async (e: ChangeEvent) => { 17 | //const updatedTodo = await updateTodo(todo) 18 | setTodos(prevTodos => [...prevTodos.filter(prev => prev.id !== todo.id), { ...todo, completed: !todo.completed }]) 19 | } 20 | 21 | const handleDelete = async (e: MouseEvent) => { 22 | //await deleteTodo(todo) 23 | setTodos(prev => [...prev.filter(td => td.id !== todo.id)]) 24 | } 25 | 26 | return ( 27 |
28 | 31 |
32 | 40 | 41 | 47 |
48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /src/app/components/TodoItem/__tests__/TodoItem.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | import TodoItem from '../TodoItem' 4 | 5 | const mockTodo = { 6 | "userId": 1, 7 | "title": "Wave hello! 👋", 8 | "completed": false, 9 | "id": 1 10 | } 11 | 12 | const mockSetTodos = jest.fn() 13 | 14 | describe('AddTodo', () => { 15 | 16 | describe('Render', () => { 17 | 18 | it('should render an article', () => { 19 | render() // ARRANGE 20 | 21 | //ACT 22 | const article = screen.getByRole('article') 23 | 24 | expect(article).toBeInTheDocument()// ASSERT 25 | }) 26 | 27 | it('should render a label', () => { 28 | render() // ARRANGE 29 | 30 | //ACT 31 | const label = screen.getByTestId('todo-item') 32 | 33 | expect(label).toBeInTheDocument()// ASSERT 34 | }) 35 | 36 | it('should render a checkbox', () => { 37 | render() // ARRANGE 38 | 39 | //ACT 40 | const checkbox = screen.getByRole('checkbox') 41 | 42 | expect(checkbox).toBeInTheDocument()// ASSERT 43 | }) 44 | 45 | it('should render a button', () => { 46 | render() // ARRANGE 47 | 48 | //ACT 49 | const button = screen.getByRole('button') 50 | 51 | expect(button).toBeInTheDocument()// ASSERT 52 | }) 53 | 54 | }) 55 | 56 | describe('Behavior', () => { 57 | 58 | 59 | it('should call setTodos when checkbox clicked', async () => { 60 | render() // ARRANGE 61 | 62 | //ACT 63 | const checkbox = screen.getByRole('checkbox') 64 | await userEvent.click(checkbox) 65 | 66 | expect(mockSetTodos).toBeCalled()// ASSERT 67 | }) 68 | 69 | it('should call setTodos when button clicked', async () => { 70 | render() // ARRANGE 71 | 72 | //ACT 73 | const button = screen.getByRole('button') 74 | await userEvent.click(button) 75 | 76 | expect(mockSetTodos).toBeCalled()// ASSERT 77 | }) 78 | 79 | }) 80 | }) -------------------------------------------------------------------------------- /src/app/components/TodoList/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import TodoItem from "../TodoItem/TodoItem" 2 | import type { Todo } from "@/types/Todo" 3 | 4 | type Props = { 5 | todos: Todo[], 6 | setTodos: React.Dispatch> 7 | } 8 | 9 | export default function TodoList({ todos, setTodos }: Props) { 10 | 11 | let content 12 | if (todos.length === 0) { 13 | content =

No Todos Available

14 | } else { 15 | const sortedTodos = todos.sort((a, b) => b.id - a.id) 16 | 17 | content = ( 18 | <> 19 | {sortedTodos.map(todo => ( 20 | 21 | ))} 22 | 23 | ) 24 | } 25 | 26 | return content 27 | } -------------------------------------------------------------------------------- /src/app/components/TodoList/__tests__/TodoList.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import TodoList from '../TodoList' 3 | 4 | const mockTodos = [ 5 | { 6 | "userId": 1, 7 | "title": "Wave hello! 👋", 8 | "completed": false, 9 | "id": 1 10 | }, 11 | { 12 | "userId": 1, 13 | "title": "Get Coffee ☕☕☕", 14 | "completed": false, 15 | "id": 2 16 | }, 17 | ] 18 | 19 | const mockSetTodos = jest.fn() 20 | 21 | describe('TodoList', () => { 22 | 23 | it('should render "No Todos Available" when the array is empty', () => { 24 | render() // ARRANGE 25 | 26 | //ACT 27 | const message = screen.getByText('No Todos Available') 28 | 29 | expect(message).toBeInTheDocument()// ASSERT 30 | }) 31 | 32 | it('should render a list with the correct number of items', () => { 33 | render( 34 | 35 | ) // ARRANGE 36 | 37 | //ACT 38 | const todosArray = screen.getAllByRole('article') 39 | 40 | expect(todosArray.length).toBe(2)// ASSERT 41 | }) 42 | 43 | it('should render the todos in the correct order', () => { 44 | render( 45 | 46 | ) // ARRANGE 47 | 48 | //ACT 49 | const firstItem = screen.getAllByTestId("todo-item")[0] 50 | 51 | expect(firstItem).toHaveTextContent("Get Coffee ☕☕☕")// ASSERT 52 | }) 53 | 54 | }) -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/next-testing-example-app/a590acd513196b108a78dcc67afd27a2f0e30775/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import Navbar from "./components/Navbar" 3 | 4 | export const metadata = { 5 | title: 'Next Todos', 6 | description: 'Created for practice', 7 | } 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | return ( 15 | 16 | 17 | 18 |
19 | {children} 20 |
21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import TodoList from "./components/TodoList/TodoList" 4 | import AddTodo from "./components/AddTodo/AddTodo" 5 | import { useState } from "react" 6 | import type { Todo } from "@/types/Todo" 7 | 8 | 9 | export default function Home() { 10 | const [todos, setTodos] = useState([ 11 | { 12 | "userId": 1, 13 | "title": "Wave hello! 👋", 14 | "completed": false, 15 | "id": 1 16 | }, 17 | { 18 | "userId": 1, 19 | "title": "Get Coffee ☕☕☕", 20 | "completed": false, 21 | "id": 2 22 | }, 23 | { 24 | "userId": 1, 25 | "title": "Go to Work ⚒", 26 | "completed": false, 27 | "id": 3 28 | }, 29 | { 30 | "userId": 1, 31 | "title": "Write Code 💻", 32 | "completed": false, 33 | "id": 4, 34 | } 35 | ]) 36 | 37 | return ( 38 | <> 39 | 40 | 41 | 42 | ) 43 | } -------------------------------------------------------------------------------- /src/types/Todo.ts: -------------------------------------------------------------------------------- 1 | export type Todo = { 2 | userId: number, 3 | id: number, 4 | title: string, 5 | completed: boolean, 6 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | "jest.setup.js" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } --------------------------------------------------------------------------------