├── .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 | Mastering React Testing 7 |

Mastering React Testing

8 |

Level up your React skills with comprehensive testing techniques - The ultimate guide to mastering React testing!

9 |
10 | 11 |
12 | GitHub 13 | GitHub Repository 14 |
15 |
16 | 17 |
18 | Gumroad 19 | Purchase on Gumroad 20 |
21 |
22 |
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 | 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 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 |
14 | setValue(e.target.value)} /> 15 | 16 |
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 | 10 | 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 | 14 | setEmail(e.target.value)} /> 15 | 16 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------