diff --git a/01 Testing components/README.md b/01 Testing components/README.md index 84d3e4a..937c914 100644 --- a/01 Testing components/README.md +++ b/01 Testing components/README.md @@ -205,11 +205,11 @@ Since we want to trigger the `change` event, we simply need to call the `fireEve ## Running asynchronous calls and unit tests -Now let us modify our component so that it performs a call against an API to retrieve some data after initialization. This is a side effect and can be implemented using a `useEffect` hook. We will also import the `getListOfFruit` method from `myApi` folder, which simulates an async call to retrieve a list of pieces of fruit. +Now let us modify our component so that it performs a call against an API to retrieve some data after initialization. This is a side effect and can be implemented using a `useEffect` hook. We will also import the `getListOfFruit` method from `myFruitApi` folder, which simulates an async call to retrieve a list of pieces of fruit. ```diff import * as React from 'react'; -+import { getListOfFruit } from '../myApi'; ++import { getListOfFruit } from '../myFruitApi'; export interface Props { nameFromProps: string; @@ -250,13 +250,13 @@ So now our component will always perform a (fake) fetch call on initialization, import * as React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitForElement } from '@testing-library/react'; -+import * as myApi from '../myApi'; ++import * as myFruitApi from '../myFruitApi'; import { MyComponent, Props } from './myComponent'; ... + it('should display the list of fruits after resolving the api call on initialization', async () => { + // Arrange + const getListOfFruitMock = jest -+ .spyOn(myApi, 'getListOfFruit') ++ .spyOn(myFruitApi, 'getListOfFruit') + .mockResolvedValue(['Melon', 'Apple', 'Pear']); + + // Act @@ -282,13 +282,13 @@ However, if we run the code (and we are using React 16.8.6 or lower), we will se import * as React from 'react'; -import { render, fireEvent, waitForElement } from '@testing-library/react'; +import { render, fireEvent, waitForElement, act, RenderResult } from '@testing-library/react'; -import * as myApi from '../myApi'; +import * as myFruitApi from '../myFruitApi'; import { MyComponent, Props } from './myComponent'; ... it('should display the list of fruits after resolving the api call on initialization', async () => { // Arrange const getListOfFruitMock = jest - .spyOn(myApi, 'getListOfFruit') + .spyOn(myFruitApi, 'getListOfFruit') .mockResolvedValue(['Melon', 'Apple', 'Pear']); // Act @@ -315,7 +315,7 @@ This fixes the issue with this test. Technically, the other tests also share the ```diff import * as React from 'react'; --import { getListOfFruit } from '../myApi'; +-import { getListOfFruit } from '../myFruitApi'; +import { MyFruits } from './myFruits'; export interface Props { @@ -355,7 +355,7 @@ export const MyComponent: React.FunctionComponent = props => { And the code for our new `MyFruits` component can be found below ```javascript import * as React from 'react'; -import { getListOfFruit } from '../myApi'; +import { getListOfFruit } from '../myFruitApi'; export const MyFruits = (props) => { const [fruits, setFruits] = React.useState([]); @@ -384,7 +384,7 @@ The first thing we are going to do is disable the last test we coded as it does - it('should display the list of fruits after resolving the api call on initialization', async () => { // Arrange const getListOfFruitMock = jest - .spyOn(myApi, 'getListOfFruit') + .spyOn(myFruitApi, 'getListOfFruit') .mockResolvedValue(['Melon', 'Apple', 'Pear']); // Act @@ -395,7 +395,7 @@ And now, in order to mock our component, let us just mock the whole module that ```diff import * as React from 'react'; import { render, fireEvent, waitForElement, act, RenderResult } from '@testing-library/react'; -import * as myApi from '../myApi'; +import * as myFruitApi from '../myFruitApi'; import { MyComponent, Props } from './myComponent'; +jest.mock('./myFruits.tsx', () => ({ diff --git a/01 Testing components/config/webpack/base.js b/01 Testing components/config/webpack/base.js index 116fcaf..ae59f05 100644 --- a/01 Testing components/config/webpack/base.js +++ b/01 Testing components/config/webpack/base.js @@ -8,7 +8,7 @@ module.exports = merge( { context: helpers.resolveFromRootPath('src'), resolve: { - extensions: ['.js', '.ts', '.tsx'], + extensions: ['.js', '.ts', '.tsx', '.css', '.scss'], }, entry: { app: ['./index.tsx'], @@ -25,6 +25,21 @@ module.exports = merge( babelCore: '@babel/core', }, }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.s[ac]ss$/i, + use: [ + // Creates `style` nodes from JS strings + 'style-loader', + // Translates CSS into CommonJS + 'css-loader', + // Compiles Sass to CSS + 'sass-loader', + ], + }, ], }, optimization: { diff --git a/01 Testing components/package.json b/01 Testing components/package.json index c70f345..cecd941 100644 --- a/01 Testing components/package.json +++ b/01 Testing components/package.json @@ -7,36 +7,61 @@ "start": "webpack-dev-server --config ./config/webpack/dev.js", "clean": "rimraf dist", "build": "npm run clean && webpack --config ./config/webpack/prod.js", - "test": "jest -c ./config/test/jest.json --verbose", + "test": "jest -c ./config/test/jest.json --verbose --coverage", "test:watch": "jest -c ./config/test/jest.json --verbose --watchAll -i" }, "author": "arp82", "license": "MIT", "dependencies": { + "@babel/runtime": "^7.9.2", "@material-ui/core": "^4.1.3", - "axios": "^0.19.0", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "react-router-dom": "^5.0.1" + "@types/react-redux": "^7.1.7", + "@types/redux": "^3.6.0", + "axios": "^0.19.2", + "install": "^0.13.0", + "npm": "^6.14.4", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-redux": "^7.2.0", + "react-router-dom": "^5.0.1", + "react-spinners": "^0.8.1", + "redux": "^4.0.5", + "sinon": "^9.0.2", + "style-loader": "^1.1.3" }, "devDependencies": { "@babel/cli": "^7.4.4", - "@babel/core": "^7.4.5", - "@babel/preset-env": "^7.4.5", - "@testing-library/react": "^8.0.7", - "@types/jest": "^24.0.13", - "@types/react": "^16.8.19", - "@types/react-dom": "^16.8.4", + "@babel/core": "^7.9.0", + "@babel/plugin-transform-runtime": "^7.9.0", + "@babel/preset-env": "^7.9.5", + "@babel/preset-react": "^7.9.4", + "@testing-library/react": "^8.0.9", + "@testing-library/react-hooks": "^3.2.1", + "@types/enzyme": "^3.10.5", + "@types/jest": "^24.9.1", + "@types/react": "^16.9.33", + "@types/react-dom": "^16.9.6", "@types/react-router-dom": "^4.3.4", "awesome-typescript-loader": "^5.2.1", + "babel-loader": "^8.1.0", + "babel-plugin-transform-runtime": "^6.23.0", + "css-loader": "^3.5.1", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.2", "html-webpack-plugin": "^3.2.0", - "jest": "^24.8.0", + "jest": "^24.9.0", + "node-sass": "^4.13.1", + "react-test-renderer": "^16.13.1", + "redux-mock-store": "^1.5.4", "rimraf": "^2.6.3", - "ts-jest": "^24.0.2", - "typescript": "^3.5.2", - "webpack": "^4.32.2", - "webpack-cli": "^3.3.2", - "webpack-dev-server": "^3.5.0", - "webpack-merge": "^4.2.1" + "sass-loader": "^8.0.2", + "source-map-loader": "^0.2.4", + "ts-jest": "^24.3.0", + "ts-loader": "^6.2.2", + "typescript": "^3.8.3", + "webpack": "^4.42.1", + "webpack-cli": "^3.3.11", + "webpack-dev-server": "^3.10.3", + "webpack-merge": "^4.2.2" } } diff --git a/01 Testing components/src/__mocks__/axios.ts b/01 Testing components/src/__mocks__/axios.ts new file mode 100644 index 0000000..07576e5 --- /dev/null +++ b/01 Testing components/src/__mocks__/axios.ts @@ -0,0 +1,3 @@ +export default { + get: jest.fn().mockResolvedValue({ data: [] }as any), +}; diff --git a/01 Testing components/src/app.tsx b/01 Testing components/src/app.tsx index f201da0..9276af1 100644 --- a/01 Testing components/src/app.tsx +++ b/01 Testing components/src/app.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { MyComponent } from './myComponent'; -export const App: React.FunctionComponent = props => ( +export const App: React.FunctionComponent = () => (
- +
); diff --git a/01 Testing components/src/index.tsx b/01 Testing components/src/index.tsx index ded7936..5f5f27f 100644 --- a/01 Testing components/src/index.tsx +++ b/01 Testing components/src/index.tsx @@ -1,5 +1,19 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { App } from './app'; +import React from "react"; +import ReactDOM from "react-dom"; +import "./styles/style.css"; +import { App } from "./app"; + +import { createStore } from "redux"; +import { Provider } from "react-redux"; + +import { rootReducer } from "./store/contact/index"; + +const store = createStore(rootReducer) + +ReactDOM.render( + + + , + document.getElementById("root") +); -ReactDOM.render(, document.getElementById('root')); diff --git a/01 Testing components/src/myApi/index.ts b/01 Testing components/src/myApi/index.ts index 3e74d70..4b61ed9 100644 --- a/01 Testing components/src/myApi/index.ts +++ b/01 Testing components/src/myApi/index.ts @@ -1 +1,6 @@ -export { getListOfFruit } from './myApi'; \ No newline at end of file +export { + getContactList, + updateContact, + deleteContact, + addContact, +} from './myContactApi'; diff --git a/01 Testing components/src/myApi/myApi.ts b/01 Testing components/src/myApi/myApi.ts deleted file mode 100644 index 988d0c5..0000000 --- a/01 Testing components/src/myApi/myApi.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as BEApi from './myBackEndApiEndpoint'; - -export const getListOfFruit = (): Promise => { - return BEApi.getFruits('http://fruityfruit.com') - .then(resolveFruits) - .catch(handleError); -} - -const resolveFruits = (fruits: string[]) => { - return fruits; -} - -const handleError = () => { - throw new Error('Where is my fruit???'); -} \ No newline at end of file diff --git a/01 Testing components/src/myApi/myBackEndApiEndpoint.ts b/01 Testing components/src/myApi/myBackEndApiEndpoint.ts deleted file mode 100644 index 1457042..0000000 --- a/01 Testing components/src/myApi/myBackEndApiEndpoint.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const getFruits = (_url: string) => { - return Promise.resolve([ - 'grape', - 'pineapple', - 'watermelon', - 'orange', - 'lemon', - 'strawberry', - 'cherry', - 'peach', - ]); -} \ No newline at end of file diff --git a/01 Testing components/src/myApi/myContactApi.spec.ts b/01 Testing components/src/myApi/myContactApi.spec.ts new file mode 100644 index 0000000..bafb570 --- /dev/null +++ b/01 Testing components/src/myApi/myContactApi.spec.ts @@ -0,0 +1,37 @@ +import { + getContactList, + deleteContact, + addContact, + updateContact, +} from './myContactApi'; + +describe('contact Api', () => { + it('update contact', () => { + const contact = { + id: '1', + name: 'Leanne Graham', + email: 'aaa@april.biz', + }; + updateContact(contact).then((u) => expect(Promise.resolve(u.data)).not.toBeNull()); + }); + + it('loads all contacts', () => { + getContactList().then((u) => expect(Promise.resolve(u.data)).not.toBeNull()); + }); + it('add contact', () => { + const contact = { + id: '', + name: 'Ervin Howell', + email: 'Shanna@melissa.tv', + }; + expect(addContact(contact).then((u) => expect(Promise.resolve(u.data)).not.toBeNull())); + }); + it('delete contact', () => { + const contact = { + id: '2', + name: 'Ervin Howell', + email: 'Shanna@melissa.tv', + }; + expect(deleteContact(contact).then((u) => expect(Promise.resolve(u.data)).not.toBeNull())); + }); +}); diff --git a/01 Testing components/src/myApi/myContactApi.ts b/01 Testing components/src/myApi/myContactApi.ts new file mode 100644 index 0000000..c58fe76 --- /dev/null +++ b/01 Testing components/src/myApi/myContactApi.ts @@ -0,0 +1,22 @@ +import axios from 'axios/index'; +import { Contact as ContactType } from '../store/contact/types'; + +export const url = 'https://jsonplaceholder.typicode.com/users'; + +// Get all contacts +export const getContactList = () => + axios.get(url); + +// Delete contact +export const deleteContact = (contact: ContactType) => + axios.delete(`${url}/${contact.id}`); + +// Update existing contact +export const updateContact = (contact: ContactType) => +axios.put(`${url}/${contact.id}`, contact); + + +// Create new contact +export const addContact = (contact: ContactType) => + axios.post(url, contact); + diff --git a/01 Testing components/src/myComponent/index.ts b/01 Testing components/src/myComponent/index.ts index bcc7da0..216c29d 100644 --- a/01 Testing components/src/myComponent/index.ts +++ b/01 Testing components/src/myComponent/index.ts @@ -1 +1,4 @@ -export { MyComponent } from './myComponent'; \ No newline at end of file +export { MyComponent } from './myComponent'; +export { MyAddContact } from './myAddContact'; +export { MyContact } from './myContact'; +export { MyLoading } from './myLoading'; \ No newline at end of file diff --git a/01 Testing components/src/myComponent/myAddContact.spec.tsx b/01 Testing components/src/myComponent/myAddContact.spec.tsx new file mode 100644 index 0000000..d91a157 --- /dev/null +++ b/01 Testing components/src/myComponent/myAddContact.spec.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { MyAddContact, Props } from './myAddContact'; +import Enzyme, { mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() }); + +const baseProps: Props = { + onAdd: jest.fn(), +}; + +describe('MyAddContact', () => { + let props: Props; + let appWrapper; + beforeEach(() => { + props = { ...baseProps }; + appWrapper = mount(); + }); + + it('Should render without errors', () => { + expect(appWrapper.length).toBe(1); + }); + + it('Set the email and name and trigger onAdd "', () => { + const { getByTestId } = render(); + + const inputElementName = getByTestId('add-input-name') as HTMLInputElement; + + const inputElementEmail = getByTestId( + 'add-input-email' + ) as HTMLInputElement; + + const buttonElementAdd = getByTestId('add-button-add') as HTMLButtonElement; + + fireEvent.change(inputElementName, { target: { value: 'John' } }); + fireEvent.change(inputElementEmail, { + target: { value: 'John@gmail.com' }, + }); + buttonElementAdd.click(); + + expect(inputElementName.value).toEqual('John'); + expect(inputElementEmail.value).toEqual('John@gmail.com'); + expect(props.onAdd).toBeCalledTimes(1); + }); + +}); diff --git a/01 Testing components/src/myComponent/myAddContact.tsx b/01 Testing components/src/myComponent/myAddContact.tsx new file mode 100644 index 0000000..87ffb70 --- /dev/null +++ b/01 Testing components/src/myComponent/myAddContact.tsx @@ -0,0 +1,50 @@ +import React, { useState, FC } from 'react'; +import { Contact as ContactType } from '../store/contact/types'; + +export interface Props { + onAdd(contact: ContactType): void; +} + +export const MyAddContact: FC = ({ onAdd }) => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + + // Initial id is an empty string which is gonna be change when api is called + const handleAdd = () => { + const contact = { id: '', name: name, email: email }; + onAdd(contact); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/01 Testing components/src/myComponent/myComponent.spec.tsx b/01 Testing components/src/myComponent/myComponent.spec.tsx index 7fc5f29..5ca5706 100644 --- a/01 Testing components/src/myComponent/myComponent.spec.tsx +++ b/01 Testing components/src/myComponent/myComponent.spec.tsx @@ -1,100 +1,125 @@ import * as React from 'react'; -// import { render, cleanup } from '@testing-library/react'; -import { render, fireEvent, waitForElement, act, RenderResult } from '@testing-library/react'; +import { + render, + fireEvent, + waitForElement, + act, + RenderResult, +} from '@testing-library/react'; +import Enzyme, { mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { MyComponent } from './myComponent'; +import { shallow } from 'enzyme'; +import configureStore from 'redux-mock-store'; import * as myApi from '../myApi'; -import { MyComponent, Props } from './myComponent'; +jest.mock('axios'); -jest.mock('./myFruits.tsx', () => ({ - MyFruits: () =>
, -})); +Enzyme.configure({ adapter: new Adapter() }); -const baseProps: Props = { - nameFromProps: null, -} +jest.mock('react-redux', () => ({ + useDispatch: () => {}, + useSelector: () => [{ id: '1', name: 'aga', email: 'aga@op.com' }], +})); -//afterEach(cleanup); +const setUp = (initialState = {}) => { + const store = configureStore(initialState); + const wrapper = shallow() + .childAt(0) + .dive(); + return wrapper as any; +}; -describe('My component', () => { - let props: Props; +describe('', () => { + let wrapper; beforeEach(() => { - props = {...baseProps}; + const initialState = []; + wrapper = setUp(initialState); + }); + + it('Expect to not log errors in console', () => { + const spy = jest.spyOn(global.console, 'error'); + expect(wrapper.length).not.toBeNull(); + expect(spy).not.toHaveBeenCalled(); }); - it('should display the title provided', () => { - // Arrange - const name = 'Title'; + it('Shows buttons', () => { + const { getByTestId } = render(wrapper); + const deleteElement = getByTestId( + 'contact-button-delete' + ) as HTMLButtonElement; + expect(deleteElement).not.toBeNull(); - // Act - const { getByText } = render(); + const addElement = getByTestId('add-button-add') as HTMLButtonElement; + expect(addElement).not.toBeNull(); - // Assert - const element = getByText('Hello Title!'); - expect(element).not.toBeNull(); - expect(element.tagName).toEqual('H1'); + const changeElement = getByTestId( + 'contact-button-change' + ) as HTMLButtonElement; + expect(changeElement).not.toBeNull(); }); - // it('should display the person name using snapshot testing', () => { - // // Arrange - // const name = 'Fruity'; + it('Renders loading', () => { + const { getByTestId } = render(); + const labelElement = getByTestId('loading-label'); + expect(labelElement.textContent).toEqual('Loading...'); + }); - // // Act - // const { asFragment } = render(); + it('Renders contacts list', () => { + const { getByTestId } = render(wrapper); + const labelElementName = getByTestId('contact-label-name'); + const labelElementEmail = getByTestId('contact-label-email'); + expect(labelElementName.textContent).toEqual('aga'); + expect(labelElementEmail.textContent).toEqual('aga@op.com'); + }); - // // Assert - // expect(asFragment()).toMatchSnapshot(); - // }); + it('Diplay Properly when addContact "', () => { + const { getByTestId } = render(wrapper); + const inputElementName = getByTestId('add-input-name') as HTMLInputElement; - it('should display a label and input elements with empty userName value', () => { - // Arrange + const inputElementEmail = getByTestId( + 'add-input-email' + ) as HTMLInputElement; - // Act - const { getByTestId, getAllByText } = render(); - // const xxlabelElement = getByText(''); // https://testing-library.com/docs/dom-testing-library/api-queries - const elementsWithEmptyText = getAllByText(''); - const labelElement = getByTestId('userName-label'); - const inputElement = getByTestId('userName-input') as HTMLInputElement; + const buttonElementAdd = getByTestId('add-button-add') as HTMLButtonElement; - // Assert - expect(labelElement.textContent).toEqual(''); - expect(inputElement.value).toEqual(''); + buttonElementAdd.click(); + fireEvent.change(inputElementName, { target: { value: 'John' } }); + fireEvent.change(inputElementEmail, { + target: { value: 'John@gmail.com' }, + }); + expect(buttonElementAdd).not.toBeNull(); }); - it('should update username label when the input changes', () => { - // Arrange - // Act - const { getByTestId } = render(); + it('Diplay properly when onDelete', () => { + const { getByTestId } = render(wrapper); + const buttonElement = getByTestId( + 'contact-button-delete' + ) as HTMLButtonElement; + buttonElement.click(); + expect(buttonElement).not.toBeNull(); + }); - const labelElement = getByTestId('userName-label'); - const inputElement = getByTestId('userName-input') as HTMLInputElement; + it('Display properly children "', async () => { + const { getByTestId } = render(wrapper); - fireEvent.change(inputElement, { target: { value: 'John' } }); + const buttonElementChange = getByTestId( + 'contact-button-change' + ) as HTMLButtonElement; + buttonElementChange.click(); - // Assert - expect(labelElement.textContent).toEqual('John'); - expect(inputElement.value).toEqual('John'); - }); + const inputElement = getByTestId( + 'contact-input-change' + ) as HTMLInputElement; - xit('should display the list of fruits after resolving the api call on initialization', async () => { - // Arrange - const getListOfFruitMock = jest - .spyOn(myApi, 'getListOfFruit') - .mockResolvedValue(['Melon', 'Apple', 'Pear']); + const buttonElementSave = getByTestId( + 'contact-button-save' + ) as HTMLButtonElement; - // Act - let wrapper: RenderResult = null; - await act(async() => { - wrapper = render(); - }); - const {getByText} = wrapper; - await waitForElement(() => getByText('Melon')); - const melonElement = getByText('Melon'); - const appleElement = getByText('Apple'); - const pearElement = getByText('Pear'); - - // Assert - expect(getListOfFruitMock).toHaveBeenCalled(); - expect(melonElement).not.toBeUndefined(); - expect(appleElement).not.toBeUndefined(); - expect(pearElement).not.toBeUndefined(); + fireEvent.change(inputElement, { target: { value: 'John@gmail.com' } }); + buttonElementSave.click(); + expect(buttonElementChange).not.toBeNull(); + expect(buttonElementSave).not.toBeNull(); + + expect(inputElement.value).toEqual('John@gmail.com'); }); -}); \ No newline at end of file +}); diff --git a/01 Testing components/src/myComponent/myComponent.tsx b/01 Testing components/src/myComponent/myComponent.tsx index c66da6e..f1fa3e6 100644 --- a/01 Testing components/src/myComponent/myComponent.tsx +++ b/01 Testing components/src/myComponent/myComponent.tsx @@ -1,25 +1,99 @@ -import * as React from 'react'; -import { MyFruits } from './myFruits'; +import React, { useState, FC } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; -export interface Props { - nameFromProps: string; -} +import { + getContactList, + deleteContact, + addContact, + updateContact, +} from '../myApi'; +import { loadAllContacts } from '../store/contact/actions'; +import { MyContact, MyAddContact, MyLoading } from '../myComponent'; +import { Contact as ContactType } from '../store/contact/types'; +import { RootState } from '../store/contact/index'; -export const MyComponent: React.FunctionComponent = props => { - const { nameFromProps } = props; - const [userName, setUserName] = React.useState(''); +export const MyComponent: FC = () => { + const contacts = useSelector((state: RootState) => state.contactReducer); + const dispatch = useDispatch(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + const [loadContacts, setLoadContacts] = useState(true); + + // useEffect loads all contacts. + React.useEffect(() => { + if (loadContacts === true) { + setLoading(true); + getContactList() + .then((u) => dispatch(loadAllContacts(u.data))) + .catch(() => setError(true)) + .finally(() => { + setLoadContacts(false); + setLoading(false); + }); + } + }, [loadContacts]); + + // If there is an error display simple message + if (error === true) { + return

Something went wrong...

; + } + + const handleChange = (contact: ContactType) => { + setLoading(true); + updateContact(contact) + .then(() => setLoadContacts(true)) + .catch(() => setError(true)); + }; + + const handleDelete = (contact: ContactType) => { + setLoading(true); + deleteContact(contact) + .then(() => setLoadContacts(true)) + .catch(() => setError(true)); + }; + + const handleAdd = (contact: ContactType) => { + setLoading(true); + addContact(contact) + .then(() => setLoadContacts(true)) + .catch(() => setError(true)); + }; return ( - <> -

Hello {nameFromProps}!

-

Username

-

{userName}

- setUserName(e.target.value)} - /> - - +
+ +
); -}; \ No newline at end of file +}; diff --git a/01 Testing components/src/myComponent/myContact.spec.tsx b/01 Testing components/src/myComponent/myContact.spec.tsx new file mode 100644 index 0000000..68447fb --- /dev/null +++ b/01 Testing components/src/myComponent/myContact.spec.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { MyContact, Props } from './myContact'; +import Enzyme, { mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +Enzyme.configure({ adapter: new Adapter() }); + +const baseProps: Props = { + contact: { id: '1', name: 'aga', email: 'aga@op.com' }, + onDelete: jest.fn(), + onChange: jest.fn(), +}; + +describe('My Contact', () => { + let props: Props; + let appWrapper; + beforeEach(() => { + props = { ...baseProps }; + appWrapper = mount(); + }); + + it('Should render without errors', () => { + expect(appWrapper.length).toBe(1); + }); + + it('Display the prop Name and Email', () => { + const { getByTestId } = render(); + const labelElementName = getByTestId('contact-label-name'); + const labelElementEmail = getByTestId('contact-label-email'); + expect(labelElementName.textContent).toEqual('aga'); + expect(labelElementEmail.textContent).toEqual('aga@op.com'); + }); + + it('Trigger Event onDelete', () => { + const { getByTestId } = render(); + const buttonElement = getByTestId( + 'contact-button-delete' + ) as HTMLButtonElement; + buttonElement.click(); + expect(props.onDelete).toBeCalledTimes(1); + }); + + it('Showing form when "change email" button is clicked', () => { + const { getByTestId } = render(); + const buttonElement = getByTestId( + 'contact-button-change' + ) as HTMLButtonElement; + buttonElement.click(); + const inputElement = getByTestId( + 'contact-input-change' + ) as HTMLInputElement; + expect(inputElement).not.toBeNull(); + }); + + it('Change the email state and trigger onChange "', () => { + const { getByTestId } = render(); + + const buttonElementChange = getByTestId( + 'contact-button-change' + ) as HTMLButtonElement; + buttonElementChange.click(); + + const inputElement = getByTestId( + 'contact-input-change' + ) as HTMLInputElement; + + const buttonElementSave = getByTestId( + 'contact-button-save' + ) as HTMLButtonElement; + buttonElementSave.click(); + + fireEvent.change(inputElement, { target: { value: 'John@gmail.com' } }); + + expect(inputElement.value).toEqual('John@gmail.com'); + expect(props.onChange).toBeCalledTimes(1); + }); +}); diff --git a/01 Testing components/src/myComponent/myContact.tsx b/01 Testing components/src/myComponent/myContact.tsx new file mode 100644 index 0000000..f2c99da --- /dev/null +++ b/01 Testing components/src/myComponent/myContact.tsx @@ -0,0 +1,92 @@ +import React, { useState, FC } from 'react'; +import { Contact as ContactType } from '../store/contact/types'; + +export interface Props { + contact: ContactType; + onDelete(contact: ContactType): void; + onChange(contact: ContactType): void; +} + +export const MyContact: FC = ({ contact, onDelete, onChange }) => { + const [showEdit, setShowEdit] = useState(false); + const [email, setEmail] = useState(''); + + const handleChange = (e: React.MouseEvent) => { + e.preventDefault(); + setShowEdit(true); + }; + + const handleSave = ( + e: React.MouseEvent, + contact: ContactType + ) => { + e.preventDefault(); + setShowEdit(false); + const updatedContact = { ...contact, email: email }; + onChange(updatedContact); + }; + + const handleDelete = ( + e: React.MouseEvent, + contact: ContactType + ) => { + e.preventDefault(); + onDelete(contact); + }; + + // Edit form let change email. + // By default edit form is hidden. + // It appers when "change email" button is clicked. + let editForm; + if (showEdit === true) { + editForm = ( + + + setEmail(e.target.value)} + /> + + + + + + ); + } + return ( + + + {contact.name} + {contact.email} + + + + + + + + {editForm} + + ); +}; diff --git a/01 Testing components/src/myComponent/myFruits.tsx b/01 Testing components/src/myComponent/myFruits.tsx deleted file mode 100644 index 591b96f..0000000 --- a/01 Testing components/src/myComponent/myFruits.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { getListOfFruit } from '../myApi'; - -export const MyFruits = (props) => { - const [fruits, setFruits] = React.useState([]); - React.useEffect(() => { - getListOfFruit().then(setFruits); - }, []); - - return ( - <> -

Fruits gallore

- {fruits.map((fruit, index) => ( -
  • {fruit}
  • - ))} - - ) -} \ No newline at end of file diff --git a/01 Testing components/src/myComponent/myLoading.spec.tsx b/01 Testing components/src/myComponent/myLoading.spec.tsx new file mode 100644 index 0000000..2452786 --- /dev/null +++ b/01 Testing components/src/myComponent/myLoading.spec.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { MyLoading, Props } from './myLoading'; +import Enzyme, { mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +Enzyme.configure({ adapter: new Adapter() }); + +const baseProps: Props = { + hidden: false, + children: null, +}; +describe('My Loading', () => { + let props: Props; + let appWrapper; + beforeEach(() => { + props = { ...baseProps }; + appWrapper = mount(); + }); + + it('Should render without errors', () => { + expect(appWrapper.length).toBe(1); + }); + + it('if hidden is false show loading', () => { + const { getByTestId } = render(); + const labelElement = getByTestId('loading-label'); + expect(labelElement.textContent).toEqual('Loading...'); + }); + it('if hidden is true show children', () => { + const children =
    Hello
    ; + const { getByTestId } = render( +