├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── robots.txt ├── src ├── LoginModule │ ├── __snapshots__ │ │ └── index.unit.test.js.snap │ ├── components │ │ ├── Login.js │ │ ├── Login.unit.test.js │ │ ├── LoginForm.js │ │ └── LoginForm.unit.test.js │ ├── hooks │ │ ├── useLogin.js │ │ └── useLogin.unit.test.js │ ├── index.integration.test.js │ ├── index.js │ └── index.unit.test.js ├── index.js └── setupTests.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error", { "singleQuote": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # integration-tests 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | **Run in development mode:** 6 | 7 | ``` 8 | yarn start 9 | ``` 10 | 11 | To log in, use the email **tobias.funke@reqres.in** and any password. 12 | 13 | **Run tests in watch mode:** 14 | 15 | ``` 16 | yarn test 17 | ``` 18 | 19 | **Run all tests and generate coverage report** 20 | 21 | Integration tests: 22 | 23 | ``` 24 | yarn test:coverage:integration 25 | ``` 26 | 27 | Unit tests: 28 | 29 | ``` 30 | yarn test:coverage:unit 31 | ``` 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fun-with-tests", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.8", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^10.0.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-scripts": "3.4.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jest-environment-jsdom-sixteen", 18 | "test:coverage:integration": "react-scripts test --watchAll=false --env=jest-environment-jsdom-sixteen --coverage --testPathPattern=\"integration.test.js\" --verbose", 19 | "test:coverage:unit": "react-scripts test --watchAll=false --env=jest-environment-jsdom-sixteen --coverage --testPathPattern=\"unit.test.js\" --verbose", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | }, 37 | "jest": { 38 | "restoreMocks": true 39 | }, 40 | "devDependencies": { 41 | "@testing-library/react-hooks": "^3.2.1", 42 | "eslint-plugin-prettier": "^3.1.2", 43 | "jest-environment-jsdom-sixteen": "^1.0.3", 44 | "prettier": "^2.0.2", 45 | "react-test-renderer": "^16.13.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarahatwork/integration-tests/9e2cd17b3f7495cbb7edee3ad10faeec33933132/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Integration Tests 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/LoginModule/__snapshots__/index.unit.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LoginModule matches snapshot 1`] = ` 4 |
5 |
9 |
12 |
15 | 23 |
26 | 34 | 46 |
47 |
48 |
49 |
52 |
55 | 63 |
66 | 74 | 86 |
87 |
88 |
89 |
92 | 106 |
107 |
108 |
109 | `; 110 | -------------------------------------------------------------------------------- /src/LoginModule/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Typography } from '@material-ui/core'; 3 | import { LoginForm } from './LoginForm'; 4 | 5 | export function Login({ state, onSubmit }) { 6 | if (state.user) { 7 | return ( 8 | 9 | 10 | Logged in as {state.user.email} 11 | 12 | 13 | ); 14 | } 15 | 16 | const isLoading = state.status === 'pending'; 17 | const isError = state.status === 'rejected'; 18 | 19 | return ( 20 | <> 21 | 22 | {isError && ( 23 | 24 | 25 | Error: {state.error} 26 | 27 | 28 | )} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/LoginModule/components/Login.unit.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Login } from './Login'; 4 | 5 | describe('Login', () => { 6 | it('renders default state', () => { 7 | render(); 8 | 9 | const submitButton = screen.getByRole('button'); 10 | expect(submitButton).toBeInTheDocument(); 11 | }); 12 | 13 | it('renders signed in state', () => { 14 | render(); 15 | 16 | const loggedInText = screen.getByText('Logged in as'); 17 | expect(loggedInText).toBeInTheDocument(); 18 | const emailAddressText = screen.getByText('test@email.com'); 19 | expect(emailAddressText).toBeInTheDocument(); 20 | 21 | // form is not rendered 22 | const submitButton = screen.queryByRole('button'); 23 | expect(submitButton).toBeNull(); 24 | }); 25 | 26 | it('renders error state', () => { 27 | render(); 28 | 29 | const errorText = screen.getByText('Error:'); 30 | expect(errorText).toBeInTheDocument(); 31 | const errorMessageText = screen.getByText('invalid password'); 32 | expect(errorMessageText).toBeInTheDocument(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/LoginModule/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Button, TextField, Box } from '@material-ui/core'; 3 | 4 | export function LoginForm({ onSubmit, isLoading }) { 5 | const [form, setForm] = useState({ 6 | email: '', 7 | password: '', 8 | }); 9 | const handleInputChange = useCallback( 10 | (event) => { 11 | const target = event.target; 12 | 13 | setForm({ 14 | ...form, 15 | [target.name]: target.value, 16 | }); 17 | }, 18 | [form] 19 | ); 20 | const handleSubmit = useCallback( 21 | (e) => { 22 | e.preventDefault(); 23 | onSubmit(form); 24 | }, 25 | [form, onSubmit] 26 | ); 27 | 28 | return ( 29 |
30 | 31 | 39 | 40 | 41 | 50 | 51 | 52 | 60 | 61 |
62 | ); 63 | } -------------------------------------------------------------------------------- /src/LoginModule/components/LoginForm.unit.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, screen } from '@testing-library/react'; 3 | import { LoginForm } from './LoginForm'; 4 | 5 | describe('LoginForm', () => { 6 | test('initial state', () => { 7 | render(); 8 | 9 | // it renders empty email and passsword fields 10 | const emailField = screen.getByRole('textbox', { name: 'Email' }); 11 | expect(emailField).toHaveValue(''); 12 | const passwordField = screen.getByLabelText('Password'); 13 | expect(passwordField).toHaveValue(''); 14 | 15 | // it renders enabled submit button 16 | const button = screen.getByRole('button'); 17 | expect(button).not.toBeDisabled(); 18 | expect(button).toHaveTextContent('Submit'); 19 | }); 20 | 21 | it('calls onSubmit with form data on submit button click', () => { 22 | const onSubmitSpy = jest.fn(); 23 | render(); 24 | 25 | const emailField = screen.getByRole('textbox', { name: 'Email' }); 26 | const passwordField = screen.getByLabelText('Password'); 27 | const button = screen.getByRole('button'); 28 | 29 | // fill out and submit form 30 | fireEvent.change(emailField, { target: { value: 'test@email.com' } }); 31 | fireEvent.change(passwordField, { target: { value: 'password' } }); 32 | fireEvent.click(button); 33 | 34 | expect(onSubmitSpy).toHaveBeenCalledWith({ 35 | email: 'test@email.com', 36 | password: 'password', 37 | }); 38 | }); 39 | 40 | it('updates button on loading state', () => { 41 | render(); 42 | 43 | const button = screen.getByRole('button'); 44 | 45 | expect(button).toBeDisabled(); 46 | expect(button).toHaveTextContent('Loading...'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/LoginModule/hooks/useLogin.js: -------------------------------------------------------------------------------- 1 | import { useReducer, useCallback } from 'react'; 2 | 3 | function loginReducer(state, action) { 4 | switch (action.type) { 5 | case 'error': { 6 | return { 7 | ...state, 8 | status: 'rejected', 9 | error: action.error, 10 | }; 11 | } 12 | case 'success': { 13 | return { 14 | ...state, 15 | status: 'resolved', 16 | user: action.user, 17 | error: null, 18 | }; 19 | } 20 | case 'start': { 21 | return { 22 | ...state, 23 | status: 'pending', 24 | error: null, 25 | }; 26 | } 27 | /* istanbul ignore next */ 28 | default: { 29 | throw new Error(`Unhandled action type: ${action.type}`); 30 | } 31 | } 32 | } 33 | 34 | export function useLogin() { 35 | const [state, dispatch] = useReducer(loginReducer, { 36 | status: 'idle', 37 | user: null, 38 | error: null, 39 | }); 40 | 41 | const onSubmit = useCallback( 42 | async ({ email, password }) => { 43 | dispatch({ type: 'start' }); 44 | const response = await fetch('https://reqres.in/api/login', { 45 | method: 'POST', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify({ 50 | email, 51 | password, 52 | }), 53 | }); 54 | const { token, error } = await response.json(); 55 | if (token) { 56 | dispatch({ 57 | type: 'success', 58 | user: { email }, 59 | }); 60 | } else { 61 | dispatch({ 62 | type: 'error', 63 | error, 64 | }); 65 | } 66 | }, 67 | [dispatch] 68 | ); 69 | 70 | return { 71 | onSubmit, 72 | state, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/LoginModule/hooks/useLogin.unit.test.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | 3 | import { useLogin } from './useLogin'; 4 | 5 | describe('useLogin', () => { 6 | test('initial state', () => { 7 | const { result } = renderHook(() => useLogin()); 8 | expect(result.current.state).toEqual({ 9 | status: 'idle', 10 | user: null, 11 | error: null, 12 | }); 13 | }); 14 | 15 | test('successful login flow', async () => { 16 | jest 17 | .spyOn(window, 'fetch') 18 | .mockResolvedValue({ json: () => ({ token: '123' }) }); 19 | 20 | const { result, waitForNextUpdate } = renderHook(() => useLogin()); 21 | 22 | act(() => { 23 | result.current.onSubmit({ 24 | email: 'test@email.com', 25 | password: 'password', 26 | }); 27 | }); 28 | 29 | // sets state to pending 30 | expect(result.current.state).toEqual({ 31 | status: 'pending', 32 | user: null, 33 | error: null, 34 | }); 35 | 36 | await waitForNextUpdate(); 37 | 38 | // sets state to resolved, stores email address 39 | expect(result.current.state).toEqual({ 40 | status: 'resolved', 41 | user: { 42 | email: 'test@email.com', 43 | }, 44 | error: null, 45 | }); 46 | }); 47 | 48 | test('error login flow', async () => { 49 | jest 50 | .spyOn(window, 'fetch') 51 | .mockResolvedValue({ json: () => ({ error: 'invalid password' }) }); 52 | 53 | const { result, waitForNextUpdate } = renderHook(() => useLogin()); 54 | 55 | act(() => { 56 | result.current.onSubmit({ 57 | email: 'test@email.com', 58 | password: 'invalid', 59 | }); 60 | }); 61 | 62 | // sets state to pending 63 | expect(result.current.state).toEqual({ 64 | status: 'pending', 65 | user: null, 66 | error: null, 67 | }); 68 | 69 | await waitForNextUpdate(); 70 | 71 | // sets state to rejected, stores error 72 | expect(result.current.state).toEqual({ 73 | status: 'rejected', 74 | user: null, 75 | error: 'invalid password', 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/LoginModule/index.integration.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, waitFor, screen } from '@testing-library/react'; 3 | import { LoginModule } from './'; 4 | 5 | describe('LoginModule', () => { 6 | test('initial state', () => { 7 | render(); 8 | 9 | // it renders empty email and passsword fields 10 | const emailField = screen.getByRole('textbox', { name: 'Email' }); 11 | expect(emailField).toHaveValue(''); 12 | const passwordField = screen.getByLabelText('Password'); 13 | expect(passwordField).toHaveValue(''); 14 | 15 | // it renders enabled submit button 16 | const button = screen.getByRole('button'); 17 | expect(button).not.toBeDisabled(); 18 | expect(button).toHaveTextContent('Submit'); 19 | }); 20 | 21 | test('successful login', async () => { 22 | jest 23 | .spyOn(window, 'fetch') 24 | .mockResolvedValue({ json: () => ({ token: '123' }) }); 25 | 26 | render(); 27 | 28 | const emailField = screen.getByRole('textbox', { name: 'Email' }); 29 | const passwordField = screen.getByLabelText('Password'); 30 | const button = screen.getByRole('button'); 31 | 32 | // fill out and submit form 33 | fireEvent.change(emailField, { target: { value: 'test@email.com' } }); 34 | fireEvent.change(passwordField, { target: { value: 'password' } }); 35 | fireEvent.click(button); 36 | 37 | // it sets loading state 38 | expect(button).toBeDisabled(); 39 | expect(button).toHaveTextContent('Loading...'); 40 | 41 | await waitFor(() => { 42 | // it hides form elements 43 | expect(button).not.toBeInTheDocument(); 44 | expect(emailField).not.toBeInTheDocument(); 45 | expect(passwordField).not.toBeInTheDocument(); 46 | 47 | // it displays success text and email address 48 | const loggedInText = screen.getByText('Logged in as'); 49 | expect(loggedInText).toBeInTheDocument(); 50 | const emailAddressText = screen.getByText('test@email.com'); 51 | expect(emailAddressText).toBeInTheDocument(); 52 | }); 53 | }); 54 | 55 | test('error login', async () => { 56 | jest 57 | .spyOn(window, 'fetch') 58 | .mockResolvedValue({ json: () => ({ error: 'invalid password' }) }); 59 | 60 | render(); 61 | 62 | const emailField = screen.getByRole('textbox', { name: 'Email' }); 63 | const passwordField = screen.getByLabelText('Password'); 64 | const button = screen.getByRole('button'); 65 | 66 | // fill out and submit form 67 | fireEvent.change(emailField, { target: { value: 'test@email.com' } }); 68 | fireEvent.change(passwordField, { target: { value: 'password' } }); 69 | fireEvent.click(button); 70 | 71 | // it sets loading state 72 | expect(button).toBeDisabled(); 73 | expect(button).toHaveTextContent('Loading...'); 74 | 75 | await waitFor(() => { 76 | // it resets button 77 | expect(button).not.toBeDisabled(); 78 | expect(button).toHaveTextContent('Submit'); 79 | 80 | // it displays error text 81 | const errorText = screen.getByText('Error:'); 82 | expect(errorText).toBeInTheDocument(); 83 | const errorMessageText = screen.getByText('invalid password'); 84 | expect(errorMessageText).toBeInTheDocument(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/LoginModule/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLogin } from './hooks/useLogin'; 3 | import { Login } from './components/Login'; 4 | 5 | export function LoginModule() { 6 | const { state, onSubmit } = useLogin(); 7 | 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/LoginModule/index.unit.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { LoginModule } from './'; 4 | 5 | describe('LoginModule', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | expect(container.cloneNode(true)).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Container, Box } from '@material-ui/core'; 5 | 6 | import { LoginModule } from './LoginModule'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | --------------------------------------------------------------------------------