├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── babel.config.json
├── index.html
├── jest.config.json
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── assets
│ └── img
│ ├── github.svg
│ ├── gumroad.png
│ └── logo.png
├── src
├── App.jsx
├── chapter-2
│ └── __tests__
│ │ └── sum.test.js
├── chapter-3
│ ├── Greeting.jsx
│ ├── IncrementCounter.jsx
│ ├── WelcomeMessage.jsx
│ └── __tests__
│ │ ├── Greeting.test.jsx
│ │ ├── IncrementCounter.test.jsx
│ │ └── WelcomeMessage.test.jsx
├── chapter-4
│ ├── DateDisplay.jsx
│ ├── DelayedMessage.jsx
│ ├── Greeting.jsx
│ ├── ThemeContext.jsx
│ ├── ThemedButton.jsx
│ ├── UserProfile.jsx
│ ├── UserProfileAxios.jsx
│ ├── __tests__
│ │ ├── DateDisplay.test.jsx
│ │ ├── DelayedMessage.test.jsx
│ │ ├── Greeting.test.jsx
│ │ ├── ThemeButton.test.jsx
│ │ ├── UserProfile.test.jsx
│ │ ├── UserProfileAxios.test.jsx
│ │ ├── __snapshots__
│ │ │ └── Greeting.test.jsx.snap
│ │ └── useCounter.test.jsx
│ ├── formatDate.js
│ └── useCounter.jsx
├── chapter-5
│ ├── FetchAndDisplay.jsx
│ ├── Form.jsx
│ ├── List.jsx
│ ├── Wrapper.jsx
│ └── __tests__
│ │ ├── FetchAndDisplay.test.jsx
│ │ └── Wrapper.test.jsx
├── chapter-6
│ ├── Counter.jsx
│ ├── EmailForm.jsx
│ ├── ToggleMessage.jsx
│ ├── __tests__
│ │ ├── Counter.test.jsx
│ │ ├── EmailForm.test.jsx
│ │ ├── ToggleMessage.test.jsx
│ │ └── useFetch.test.jsx
│ └── useFetch.jsx
├── chapter-7
│ ├── ThemeContext.jsx
│ ├── ThemedButton.jsx
│ └── __tests__
│ │ └── ThemedButton.test.jsx
├── chapter-8
│ ├── Accordion.jsx
│ ├── Modal.jsx
│ ├── TabNavigation.jsx
│ └── __tests__
│ │ ├── Accordion.test.jsx
│ │ ├── Modal.test.jsx
│ │ └── TabNavigation.test.jsx
├── index.css
├── main.jsx
├── setupTests.js
└── test-utils
│ └── customRender.js
├── tailwind.config.js
└── vite.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:testing-library/react", "plugin:prettier/recommended"],
4 | "settings": {
5 | "react": {
6 | "version": "detect"
7 | }
8 | },
9 | "env": {
10 | "browser": true,
11 | "es2021": true,
12 | "node": true,
13 | "jest": true
14 | },
15 | "rules": {
16 | "react/react-in-jsx-scope": "off",
17 | "react/prop-types": "off",
18 | "testing-library/no-wait-for-multiple-assertions": "off"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules/
11 | coverage/
12 | dist
13 | dist-ssr
14 | *.local
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "printWidth": 250
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 khem-academy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📘 Mastering React Testing
2 |
3 | Welcome to the GitHub repository for the book "Mastering React Testing"! This book is your ultimate guide to mastering comprehensive testing techniques in React applications.
4 |
5 | ## 🛍️ Where to Purchase the Book
6 |
7 | You can purchase "Mastering React Testing" on Gumroad. Visit the following link to get your copy and level up your React testing skills:
8 |
9 | Gumroad - Mastering React Testing
10 |
11 | ## 📖 Table of Contents
12 |
13 | 1. **🎯 The Importance of Testing**: We discuss the benefits of testing and its role in creating reliable, maintainable, and scalable applications.
14 | 2. **🛠️ Setting Up the Testing Environment**: We introduce Jest, the popular testing framework for JavaScript, and React Testing Library, which simplifies testing React components.
15 | 3. **🧪 Testing React Components**: We delve into the significance of testing user interactions with components, such as button clicks, form submissions, and input field changes. We examine the use of `userEvent` from the React Testing Library to simulate these interactions in a more realistic and comprehensive way. This allows us to validate the component's behavior in response to user actions.
16 | 4. **🚀 Advanced Testing Techniques**: We explore techniques such as testing components with asynchronous behavior, using context and hooks, snapshot testing, mocking functions and modules, and testing custom hooks.
17 | 5. **🔗 Integration Testing**: We highlight the importance of integration testing and provide examples of writing integration tests with Jest and React Testing Library.
18 | 6. **🌟 Testing Strategies and Patterns**: We discuss Test-Driven Development (TDD), code coverage, and common testing patterns.
19 | 7. **⚙️ Optimizing and Scaling React Testing**: We cover performance considerations, organizing and structuring test code.
20 | 8. **♿ Accessibility Testing**: We emphasize the importance of accessibility testing and provide examples of writing accessibility tests using Jest and React Testing Library.
21 |
22 | ## 🌐 Resources for Further Learning
23 |
24 | To continue learning about testing React applications, consider exploring the following resources:
25 |
26 | 1. Jest documentation
27 | 2. React Testing Library documentation
28 | 3. UserEvent documentation - Advanced simulation of browser interactions
29 | 4. React Design Patterns and Best Practices by Michele Bertoli
30 | 5. Testing JavaScript by Kent C. Dodds
31 |
32 | ## 🌟 Staying Up-to-Date with React Testing Developments
33 |
34 | To stay current with the latest developments in React testing, follow these resources and communities:
35 |
36 | 1. React official blog
37 | 2. React Testing Library GitHub repository
38 | 3. Jest GitHub repository
39 | 4. React subreddit
40 |
41 | ## 📚 Book Code Examples
42 |
43 | All the code examples used in the book can be found in this GitHub repository. Feel free to clone or fork the repository to follow along with the book.
44 |
45 | ## ❤️ Made with Love by Khem Sok
46 |
47 | I'm excited to embark on this journey with you, and I hope that the knowledge and techniques shared in this book will make a positive impact on your testing practices. Let's dive deep and explore the world of React testing together!
48 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | ["@babel/preset-react", { "runtime": "automatic" }]
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Mastering React Testing
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "testEnvironment": "jsdom",
4 | "setupFilesAfterEnv": ["./src/setupTests.js"]
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mastering-react-testing",
3 | "homepage": "https://khem-academy.github.io/mastering-react-testing",
4 | "private": true,
5 | "version": "0.0.0",
6 | "type": "module",
7 | "author": "khem sok",
8 | "scripts": {
9 | "dev": "vite",
10 | "predeploy": "npm run build",
11 | "deploy": "gh-pages -d dist",
12 | "build": "vite build",
13 | "preview": "vite preview",
14 | "lint": "eslint 'src/**/*.js' 'src/**/*.jsx'",
15 | "lint:fix": "eslint --fix 'src/**/*.js' 'src/**/*.jsx'",
16 | "test": "jest"
17 | },
18 | "dependencies": {
19 | "axios": "^1.3.4",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0"
22 | },
23 | "devDependencies": {
24 | "@babel/eslint-parser": "^7.21.8",
25 | "@babel/preset-env": "^7.20.2",
26 | "@babel/preset-react": "^7.18.6",
27 | "@testing-library/jest-dom": "^5.16.5",
28 | "@testing-library/react": "^14.0.0",
29 | "@testing-library/user-event": "^14.4.3",
30 | "@types/react": "^18.0.28",
31 | "@types/react-dom": "^18.0.11",
32 | "@vitejs/plugin-react": "^3.1.0",
33 | "autoprefixer": "^10.4.14",
34 | "babel-jest": "^29.5.0",
35 | "eslint": "^8.40.0",
36 | "eslint-config-prettier": "^8.8.0",
37 | "eslint-plugin-prettier": "^4.2.1",
38 | "eslint-plugin-react": "^7.32.2",
39 | "eslint-plugin-testing-library": "^5.10.3",
40 | "gh-pages": "^5.0.0",
41 | "jest": "^29.5.0",
42 | "jest-environment-jsdom": "^29.5.0",
43 | "msw": "^1.2.1",
44 | "postcss": "^8.4.21",
45 | "prettier": "^2.8.8",
46 | "react-test-renderer": "^18.2.0",
47 | "tailwindcss": "^3.3.0",
48 | "vite": "^4.2.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/img/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/img/gumroad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khem-academy/mastering-react-testing/937c15de10a672c60d0b23cc2f53d30a9bddc354/public/assets/img/gumroad.png
--------------------------------------------------------------------------------
/public/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khem-academy/mastering-react-testing/937c15de10a672c60d0b23cc2f53d30a9bddc354/public/assets/img/logo.png
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | function App() {
2 | return (
3 |
4 |
5 |
6 |
7 |
Mastering React Testing
8 |
Level up your React skills with comprehensive testing techniques - The ultimate guide to mastering React testing!
9 |
23 |
24 |
25 |
26 |
Made with 💕 by Khem Sok
27 |
28 |
29 | )
30 | }
31 |
32 | export default App
33 |
--------------------------------------------------------------------------------
/src/chapter-2/__tests__/sum.test.js:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b
3 | }
4 |
5 | test('sum adds numbers correctly', () => {
6 | expect(sum(1, 2)).toBe(3)
7 | })
8 |
--------------------------------------------------------------------------------
/src/chapter-3/Greeting.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Greeting = ({ name }) => {
4 | return Hello, {name}!
5 | }
6 |
7 | export default Greeting
8 |
--------------------------------------------------------------------------------
/src/chapter-3/IncrementCounter.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const IncrementCounter = () => {
4 | const [count, setCount] = useState(0)
5 |
6 | const increment = () => {
7 | setCount(count + 1)
8 | }
9 |
10 | return (
11 |
12 |
Count: {count}
13 |
14 | Increment
15 |
16 |
17 | )
18 | }
19 |
20 | export default IncrementCounter
21 |
--------------------------------------------------------------------------------
/src/chapter-3/WelcomeMessage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const WelcomeMessage = ({ isLoggedIn }) => {
4 | return {isLoggedIn ?
Welcome back!
:
Please log in.
}
5 | }
6 |
7 | export default WelcomeMessage
8 |
--------------------------------------------------------------------------------
/src/chapter-3/__tests__/Greeting.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import Greeting from '../Greeting'
4 |
5 | test('renders a greeting with the provided name', () => {
6 | render( )
7 | expect(screen.getByText('Hello, John!')).toBeInTheDocument()
8 | })
9 |
--------------------------------------------------------------------------------
/src/chapter-3/__tests__/IncrementCounter.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import IncrementCounter from '../IncrementCounter'
5 |
6 | test('increments the count on button click', async () => {
7 | const user = userEvent.setup()
8 |
9 | render( )
10 |
11 | const incrementButton = screen.getByTestId('increment-button')
12 | await user.click(incrementButton)
13 |
14 | expect(screen.getByTestId('counter-display')).toHaveTextContent('Count: 1')
15 | })
16 |
--------------------------------------------------------------------------------
/src/chapter-3/__tests__/WelcomeMessage.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import WelcomeMessage from '../WelcomeMessage'
4 |
5 | describe('WelcomeMessage tests', () => {
6 | test('renders welcome message for logged-in users', () => {
7 | render( )
8 | expect(screen.getByTestId('welcome-message')).toBeInTheDocument()
9 | })
10 |
11 | test('renders guest message for non-logged-in users', () => {
12 | render( )
13 | expect(screen.getByTestId('guest-message')).toBeInTheDocument()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/chapter-4/DateDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { formatDate } from './formatDate'
3 |
4 | const DateDisplay = ({ date }) => {
5 | return {formatDate(date)}
6 | }
7 |
8 | export default DateDisplay
9 |
--------------------------------------------------------------------------------
/src/chapter-4/DelayedMessage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | const DelayedMessage = ({ message, delay }) => {
4 | const [isVisible, setIsVisible] = useState(false)
5 |
6 | useEffect(() => {
7 | const timer = setTimeout(() => {
8 | setIsVisible(true)
9 | }, delay)
10 |
11 | return () => clearTimeout(timer)
12 | }, [delay])
13 |
14 | return {isVisible && {message} }
15 | }
16 |
17 | export default DelayedMessage
18 |
--------------------------------------------------------------------------------
/src/chapter-4/Greeting.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Greeting = ({ name }) => {
4 | return Hello, {name}!
5 | }
6 |
7 | export default Greeting
8 |
--------------------------------------------------------------------------------
/src/chapter-4/ThemeContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | const ThemeContext = createContext('light')
4 | export default ThemeContext
5 |
--------------------------------------------------------------------------------
/src/chapter-4/ThemedButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import ThemeContext from './ThemeContext'
3 |
4 | const ThemedButton = () => {
5 | const theme = useContext(ThemeContext)
6 |
7 | return Themed Button
8 | }
9 |
10 | export default ThemedButton
11 |
--------------------------------------------------------------------------------
/src/chapter-4/UserProfile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | const UserProfile = ({ userId }) => {
4 | const [user, setUser] = useState(null)
5 | const [isLoading, setIsLoading] = useState(true)
6 |
7 | useEffect(() => {
8 | const fetchUser = async () => {
9 | setIsLoading(true)
10 | const response = await fetch(`https://api.example.com/users/${userId}`)
11 | const data = await response.json()
12 | setUser(data)
13 | setIsLoading(false)
14 | }
15 |
16 | fetchUser()
17 | }, [userId])
18 |
19 | if (isLoading) {
20 | return Loading...
21 | }
22 |
23 | return (
24 |
25 |
{user.name}
26 |
{user.email}
27 |
28 | )
29 | }
30 |
31 | export default UserProfile
32 |
--------------------------------------------------------------------------------
/src/chapter-4/UserProfileAxios.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import axios from 'axios'
3 |
4 | const UserProfileAxios = ({ userId }) => {
5 | const [user, setUser] = useState(null)
6 | const [isLoading, setIsLoading] = useState(true)
7 |
8 | useEffect(() => {
9 | const fetchUser = async () => {
10 | setIsLoading(true)
11 | const response = await axios.get(`https://api.example.com/users/${userId}`)
12 | setUser(response.data)
13 | setIsLoading(false)
14 | }
15 |
16 | fetchUser()
17 | }, [userId])
18 |
19 | if (isLoading) {
20 | return Loading...
21 | }
22 |
23 | return (
24 |
25 |
{user.name}
26 |
{user.email}
27 |
28 | )
29 | }
30 |
31 | export default UserProfileAxios
32 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/DateDisplay.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import DateDisplay from '../DateDisplay'
4 | import * as formatDateModule from '../formatDate'
5 |
6 | jest.mock('../formatDate')
7 |
8 | test('renders date using formatDate function', () => {
9 | formatDateModule.formatDate.mockImplementation(() => '1/1/2023')
10 | render( )
11 | expect(screen.getByText('1/1/2023')).toBeInTheDocument()
12 | expect(formatDateModule.formatDate).toHaveBeenCalledWith('2023-01-01')
13 | })
14 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/DelayedMessage.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen, waitFor, act } from '@testing-library/react'
3 | import DelayedMessage from '../DelayedMessage'
4 |
5 | jest.useFakeTimers()
6 |
7 | test('displays a message after the specified delay', async () => {
8 | render( )
9 | expect(screen.queryByText('Hello, World!')).toBeNull()
10 |
11 | await act(async () => {
12 | jest.advanceTimersByTime(3000)
13 | })
14 |
15 | await waitFor(() => {
16 | expect(screen.getByText('Hello, World!')).toBeInTheDocument()
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/Greeting.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import Greeting from '../Greeting'
4 |
5 | test('Greeting component renders correctly', () => {
6 | const tree = renderer.create( ).toJSON()
7 | expect(tree).toMatchSnapshot()
8 | })
9 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/ThemeButton.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import ThemeContext from '../ThemeContext'
4 | import ThemedButton from '../ThemedButton'
5 |
6 | describe('ThemedButton tests', () => {
7 | test('renders button with light theme', () => {
8 | render(
9 |
10 |
11 |
12 | )
13 | expect(screen.getByText('Themed Button')).toHaveClass('button')
14 | })
15 |
16 | test('renderse buttonw ith dark theme', () => {
17 | render(
18 |
19 |
20 |
21 | )
22 | expect(screen.getByText('Themed Button')).toHaveClass('button button-dark')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/UserProfile.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen, waitFor } from '@testing-library/react'
3 | import UserProfile from '../UserProfile'
4 |
5 | // Mock the fetch function to return fake data
6 | global.fetch = jest.fn(() =>
7 | Promise.resolve({
8 | json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' }),
9 | })
10 | )
11 |
12 | test('fetches and displays user data', async () => {
13 | render( )
14 | expect(screen.getByText('Loading...')).toBeInTheDocument()
15 |
16 | await waitFor(() => {
17 | expect(screen.getByText('John Doe')).toBeInTheDocument()
18 | expect(screen.getByText('john@example.com')).toBeInTheDocument()
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/UserProfileAxios.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen, waitFor } from '@testing-library/react'
3 | import { rest } from 'msw'
4 | import { setupServer } from 'msw/node'
5 | import UserProfileAxios from '../UserProfileAxios'
6 |
7 | const server = setupServer(
8 | rest.get('https://api.example.com/users/:userId', (req, res, ctx) => {
9 | return res(ctx.json({ name: 'John Doe', email: 'john@example.com' }))
10 | })
11 | )
12 |
13 | describe('UserProfileAxios tests', () => {
14 | beforeAll(() => server.listen())
15 | afterEach(() => server.resetHandlers())
16 | afterAll(() => server.close())
17 |
18 | test('fetches and displays user data using Axios', async () => {
19 | render( )
20 | expect(screen.getByText('Loading...')).toBeInTheDocument()
21 |
22 | await waitFor(() => {
23 | expect(screen.getByText('John Doe')).toBeInTheDocument()
24 | expect(screen.getByText('john@example.com')).toBeInTheDocument()
25 | })
26 | })
27 |
28 | test('fetches and displays different user data using Axios', async () => {
29 | server.use(
30 | rest.get('https://api.example.com/users/:userId', (req, res, ctx) => {
31 | return res(ctx.json({ name: 'Jane Smith', email: 'jane@example.com' }))
32 | })
33 | )
34 |
35 | render( )
36 | expect(screen.getByText('Loading...')).toBeInTheDocument()
37 |
38 | await waitFor(() => {
39 | expect(screen.getByText('Jane Smith')).toBeInTheDocument()
40 | expect(screen.getByText('jane@example.com')).toBeInTheDocument()
41 | })
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/__snapshots__/Greeting.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Greeting component renders correctly 1`] = `
4 |
5 | Hello,
6 | John Doe
7 | !
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/chapter-4/__tests__/useCounter.test.jsx:
--------------------------------------------------------------------------------
1 | import { renderHook, act } from '@testing-library/react'
2 | import useCounter from '../useCounter'
3 |
4 | describe('useCounter', () => {
5 | test('initializes with a default value of 0', () => {
6 | const { result } = renderHook(() => useCounter())
7 | expect(result.current.count).toBe(0)
8 | })
9 |
10 | test('initializes with a provided initial value', () => {
11 | const { result } = renderHook(() => useCounter(5))
12 | expect(result.current.count).toBe(5)
13 | })
14 |
15 | test('increments the count', () => {
16 | const { result } = renderHook(() => useCounter())
17 | act(() => {
18 | result.current.increment()
19 | })
20 | expect(result.current.count).toBe(1)
21 | })
22 |
23 | test('decrements the count', () => {
24 | const { result } = renderHook(() => useCounter(5))
25 | act(() => {
26 | result.current.decrement()
27 | })
28 | expect(result.current.count).toBe(4)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/chapter-4/formatDate.js:
--------------------------------------------------------------------------------
1 | export const formatDate = (date) => {
2 | return new Date(date).toLocaleDateString()
3 | }
4 |
--------------------------------------------------------------------------------
/src/chapter-4/useCounter.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | const useCounter = (initialValue = 0) => {
4 | const [count, setCount] = useState(initialValue)
5 |
6 | const increment = () => setCount(count + 1)
7 | const decrement = () => setCount(count - 1)
8 |
9 | return { count, increment, decrement }
10 | }
11 |
12 | export default useCounter
13 |
--------------------------------------------------------------------------------
/src/chapter-5/FetchAndDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import axios from 'axios'
3 | import List from './List'
4 |
5 | const FetchAndDisplay = ({ url }) => {
6 | const [data, setData] = useState([])
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | const response = await axios.get(url)
11 | setData(response.data)
12 | }
13 | fetchData()
14 | }, [url])
15 |
16 | return
17 | }
18 |
19 | export default FetchAndDisplay
20 |
--------------------------------------------------------------------------------
/src/chapter-5/Form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const Form = ({ onSubmit }) => {
4 | const [value, setValue] = useState('')
5 |
6 | const handleSubmit = (e) => {
7 | e.preventDefault()
8 | onSubmit(value)
9 | setValue('')
10 | }
11 |
12 | return (
13 |
17 | )
18 | }
19 |
20 | export default Form
21 |
--------------------------------------------------------------------------------
/src/chapter-5/List.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const List = ({ items }) => {
4 | return (
5 |
6 | {items.map((item, index) => (
7 | {item}
8 | ))}
9 |
10 | )
11 | }
12 |
13 | export default List
14 |
--------------------------------------------------------------------------------
/src/chapter-5/Wrapper.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import Form from './Form'
3 | import List from './List'
4 |
5 | const Wrapper = () => {
6 | const [items, setItems] = useState([])
7 |
8 | const handleSubmit = (value) => {
9 | setItems([...items, value])
10 | }
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default Wrapper
21 |
--------------------------------------------------------------------------------
/src/chapter-5/__tests__/FetchAndDisplay.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, waitFor, screen } from '@testing-library/react'
3 | import axios from 'axios'
4 | import FetchAndDisplay from '../FetchAndDisplay'
5 |
6 | jest.mock('axios')
7 |
8 | afterEach(() => {
9 | jest.resetAllMocks()
10 | })
11 |
12 | test('fetches and displays data', async () => {
13 | axios.get.mockResolvedValue({
14 | data: ['Item 1', 'Item 2'],
15 | })
16 |
17 | render( )
18 | await waitFor(() => expect(axios.get).toHaveBeenCalledTimes(1))
19 |
20 | expect(screen.getByText('Item 1')).toBeInTheDocument()
21 | expect(screen.getByText('Item 2')).toBeInTheDocument()
22 | })
23 |
--------------------------------------------------------------------------------
/src/chapter-5/__tests__/Wrapper.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import Wrapper from '../Wrapper'
5 |
6 | test('adding an item updates the list', async () => {
7 | const user = userEvent.setup()
8 |
9 | render( )
10 | const input = screen.getByRole('textbox')
11 | const submitButton = screen.getByRole('button', { name: 'Submit' })
12 |
13 | await user.type(input, 'New item')
14 | await user.click(submitButton)
15 |
16 | expect(screen.getByText('New item')).toBeInTheDocument()
17 | })
18 |
--------------------------------------------------------------------------------
/src/chapter-6/Counter.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const Counter = () => {
4 | const [count, setCount] = useState(0)
5 |
6 | return (
7 |
8 |
Count: {count}
9 |
setCount(count + 1)}>Increment
10 |
setCount(count - 1)}>Decrement
11 |
12 | )
13 | }
14 |
15 | export default Counter
16 |
--------------------------------------------------------------------------------
/src/chapter-6/EmailForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const EmailForm = ({ onSubmit }) => {
4 | const [email, setEmail] = useState('')
5 |
6 | const handleSubmit = (e) => {
7 | e.preventDefault()
8 | onSubmit(email)
9 | setEmail('')
10 | }
11 |
12 | return (
13 |
17 | )
18 | }
19 |
20 | export default EmailForm
21 |
--------------------------------------------------------------------------------
/src/chapter-6/ToggleMessage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const ToggleMessage = () => {
4 | const [showMessage, setShowMessage] = useState(false)
5 |
6 | return (
7 |
8 |
setShowMessage(!showMessage)}>Toggle Message
9 | {showMessage &&
Hello, World!
}
10 |
11 | )
12 | }
13 |
14 | export default ToggleMessage
15 |
--------------------------------------------------------------------------------
/src/chapter-6/__tests__/Counter.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import EmailForm from '../EmailForm'
5 |
6 | test('submits the email and clears the input', async () => {
7 | const user = userEvent.setup()
8 |
9 | const handleSubmit = jest.fn()
10 | render( )
11 | const input = screen.getByRole('textbox')
12 | const submitButton = screen.getByRole('button', { name: 'Submit' })
13 |
14 | await user.type(input, 'test@example.com')
15 | await user.click(submitButton)
16 |
17 | expect(handleSubmit).toHaveBeenCalledWith('test@example.com')
18 | expect(input).toHaveValue('')
19 | })
20 |
--------------------------------------------------------------------------------
/src/chapter-6/__tests__/EmailForm.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import EmailForm from '../EmailForm'
5 |
6 | test('submits the email and clears the input', async () => {
7 | const user = userEvent.setup()
8 |
9 | const handleSubmit = jest.fn()
10 | render( )
11 | const input = screen.getByRole('textbox')
12 | const submitButton = screen.getByRole('button', { name: 'Submit' })
13 |
14 | await user.type(input, 'test@example.com')
15 | await user.click(submitButton)
16 |
17 | expect(handleSubmit).toHaveBeenCalledWith('test@example.com')
18 | expect(input.value).toBe('')
19 | })
20 |
--------------------------------------------------------------------------------
/src/chapter-6/__tests__/ToggleMessage.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import ToggleMessage from '../ToggleMessage'
5 |
6 | describe('ToggleMessage tests', () => {
7 | test('message is not visible by default', () => {
8 | render( )
9 | expect(screen.queryByTestId('message')).toBeNull()
10 | })
11 |
12 | test('message becomes visible when button is clicked', async () => {
13 | const user = userEvent.setup()
14 |
15 | render( )
16 | const toggleButton = screen.getByRole('button', { name: 'Toggle Message' })
17 |
18 | await user.click(toggleButton)
19 | expect(screen.getByTestId('message')).toBeInTheDocument()
20 | })
21 |
22 | test('message becomes hidden again after clicking button twice', async () => {
23 | const user = userEvent.setup()
24 |
25 | render( )
26 | const toggleButton = screen.getByRole('button', { name: 'Toggle Message' })
27 |
28 | await user.click(toggleButton)
29 | await user.click(toggleButton)
30 | expect(screen.queryByTestId('message')).toBeNull()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/chapter-6/__tests__/useFetch.test.jsx:
--------------------------------------------------------------------------------
1 | import { renderHook, waitFor } from '@testing-library/react'
2 | import useFetch from '../useFetch'
3 |
4 | global.fetch = jest.fn(() =>
5 | Promise.resolve({
6 | json: () => Promise.resolve({ data: 'mock data' }),
7 | })
8 | )
9 |
10 | describe('useFetch tests', () => {
11 | test('useFetch fetches data and updates state', async () => {
12 | const { result } = renderHook(() => useFetch('https://api.example.com/data'))
13 |
14 | expect(result.current.isLoading).toBe(true)
15 |
16 | await waitFor(() => {
17 | expect(result.current.isLoading).toBe(false)
18 | expect(result.current.data).toEqual({ data: 'mock data' })
19 | expect(result.current.error).toBe(null)
20 | })
21 | })
22 |
23 | test('useFetch handles error when fetching data', async () => {
24 | global.fetch.mockImplementationOnce(() => Promise.reject(new Error('Fetch error')))
25 |
26 | const { result } = renderHook(() => useFetch('https://api.example.com/data'))
27 |
28 | expect(result.current.isLoading).toBe(true)
29 |
30 | await waitFor(() => {
31 | expect(result.current.isLoading).toBe(false)
32 | expect(result.current.data).toBe(null)
33 | expect(result.current.error).toEqual(new Error('Fetch error'))
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/chapter-6/useFetch.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | const useFetch = (url) => {
4 | const [data, setData] = useState(null)
5 | const [isLoading, setIsLoading] = useState(true)
6 | const [error, setError] = useState(null)
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await fetch(url)
12 | const data = await response.json()
13 | setData(data)
14 | setIsLoading(false)
15 | } catch (err) {
16 | setError(err)
17 | setIsLoading(false)
18 | }
19 | }
20 | fetchData()
21 | }, [url])
22 |
23 | return { data, isLoading, error }
24 | }
25 |
26 | export default useFetch
27 |
--------------------------------------------------------------------------------
/src/chapter-7/ThemeContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState } from 'react'
2 |
3 | const ThemeContext = createContext()
4 |
5 | const ThemeProvider = ({ children }) => {
6 | const [theme, setTheme] = useState('light')
7 |
8 | const toggleTheme = () => {
9 | setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'))
10 | }
11 |
12 | return {children}
13 | }
14 |
15 | export { ThemeContext, ThemeProvider }
16 |
--------------------------------------------------------------------------------
/src/chapter-7/ThemedButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { ThemeContext } from './ThemeContext'
3 |
4 | const ThemedButton = () => {
5 | const { theme, toggleTheme } = useContext(ThemeContext)
6 |
7 | return (
8 |
9 | Themed Button
10 |
11 | )
12 | }
13 |
14 | export default ThemedButton
15 |
--------------------------------------------------------------------------------
/src/chapter-7/__tests__/ThemedButton.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ThemedButton from '../ThemedButton'
3 | import { screen, render } from '../../test-utils/customRender'
4 |
5 | describe('ThemedButton tests', () => {
6 | test('renders button with light theme', () => {
7 | render( )
8 | expect(screen.getByText('Themed Button')).toHaveClass('button')
9 | })
10 |
11 | test('renders button with dark theme', () => {
12 | render( , { theme: 'dark' })
13 | expect(screen.getByText('Themed Button')).toHaveClass('button button-dark')
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/chapter-8/Accordion.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const Accordion = ({ items }) => {
4 | const [activeItem, setActiveItem] = useState(null)
5 |
6 | return (
7 |
8 | {items.map((item, index) => (
9 |
10 |
setActiveItem(activeItem === index ? null : index)}>
11 | {item.title}
12 |
13 | {activeItem === index &&
{item.content}
}
14 |
15 | ))}
16 |
17 | )
18 | }
19 |
20 | export default Accordion
21 |
--------------------------------------------------------------------------------
/src/chapter-8/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 |
3 | const Modal = ({ isOpen, onClose }) => {
4 | const closeButtonRef = useRef(null)
5 |
6 | useEffect(() => {
7 | if (isOpen) {
8 | closeButtonRef.current.focus()
9 | }
10 | }, [isOpen])
11 |
12 | if (!isOpen) return null
13 |
14 | return (
15 |
16 |
Modal title
17 |
Modal content
18 |
19 | Close
20 |
21 |
22 | )
23 | }
24 |
25 | export default Modal
26 |
--------------------------------------------------------------------------------
/src/chapter-8/TabNavigation.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 |
3 | const TabNavigation = ({ tabs }) => {
4 | const [activeTab, setActiveTab] = useState(0)
5 |
6 | return (
7 |
8 |
9 | {tabs.map((tab, index) => (
10 | setActiveTab(index)}
15 | onKeyDown={(event) => {
16 | if (event.key === 'Enter' || event.key === ' ') {
17 | setActiveTab(index)
18 | }
19 | }}
20 | aria-selected={activeTab === index}
21 | >
22 | {tab}
23 |
24 | ))}
25 |
26 |
27 | )
28 | }
29 |
30 | export default TabNavigation
31 |
--------------------------------------------------------------------------------
/src/chapter-8/__tests__/Accordion.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import Accordion from '../Accordion'
5 |
6 | const items = [
7 | { title: 'Item 1', content: 'Content 1' },
8 | { title: 'Item 2', content: 'Content 2' },
9 | { title: 'Item 3', content: 'Content 3' },
10 | ]
11 |
12 | describe('Accordion tests', () => {
13 | test('properly sets ARIA attributes', async () => {
14 | const user = userEvent.setup()
15 |
16 | render( )
17 | const firstItemButton = screen.getByText('Item 1')
18 | const secondItemButton = screen.getByText('Item 2')
19 |
20 | expect(firstItemButton).toHaveAttribute('aria-expanded', 'false')
21 |
22 | expect(secondItemButton).toHaveAttribute('aria-expanded', 'false')
23 |
24 | await user.click(firstItemButton)
25 |
26 | expect(firstItemButton).toHaveAttribute('aria-expanded', 'true')
27 | expect(secondItemButton).toHaveAttribute('aria-expanded', 'false')
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/chapter-8/__tests__/Modal.test.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import Modal from '../Modal'
5 |
6 | const TestComponent = () => {
7 | const [isOpen, setIsOpen] = useState(false)
8 |
9 | return (
10 | <>
11 | setIsOpen(true)}>Open Modal
12 | setIsOpen(false)} />
13 | >
14 | )
15 | }
16 |
17 | describe('Modal tests', () => {
18 | test('focuses the close button when opened', async () => {
19 | const user = userEvent.setup()
20 |
21 | render( )
22 |
23 | const openButton = screen.getByText('Open Modal')
24 | await user.click(openButton)
25 |
26 | const closeButton = screen.getByText('Close')
27 | expect(closeButton).toHaveFocus()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/chapter-8/__tests__/TabNavigation.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, screen } from '@testing-library/react'
3 | import userEvent from '@testing-library/user-event'
4 | import TabNavigation from '../TabNavigation'
5 |
6 | const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
7 |
8 | describe('TabNavigation tests', () => {
9 | test('switches tabs using keyboard navigation', async () => {
10 | const user = userEvent.setup()
11 | render( )
12 |
13 | const firstTab = screen.getByText('Tab 1')
14 | const secondTab = screen.getByText('Tab 2')
15 |
16 | expect(firstTab).toHaveAttribute('aria-selected', 'true')
17 | expect(secondTab).toHaveAttribute('aria-selected', 'false')
18 |
19 | await user.tab() // Moves focus to 'Tab 2'
20 | await user.type(secondTab, '{enter}') // Activates 'Tab 2'
21 |
22 | expect(firstTab).toHaveAttribute('aria-selected', 'false')
23 | expect(secondTab).toHaveAttribute('aria-selected', 'true')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 |
--------------------------------------------------------------------------------
/src/test-utils/customRender.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from '@testing-library/react'
3 | import { ThemeContext } from '../chapter-7/ThemeContext'
4 |
5 | const customRender = (ui, { theme = 'light', ...options } = {}) => {
6 | const Wrapper = ({ children }) => {children}
7 |
8 | return render(ui, { wrapper: Wrapper, ...options })
9 | }
10 |
11 | export * from '@testing-library/react'
12 | export { customRender as render }
13 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | base: '/mastering-react-testing/',
7 | plugins: [react()],
8 | build: {
9 | publicPath: '/mastering-react-testing/',
10 | },
11 | })
12 |
--------------------------------------------------------------------------------