├── .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 |
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 |
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 |
--------------------------------------------------------------------------------