diff --git a/.gitignore b/.gitignore index 7d48688..41f5179 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,5 @@ src/api/apiUtils.ts *.iml junit.xml /reports/ -/.awcache +.awcache /.vscode diff --git a/00 Base/config/webpack/base.js b/00 Base/config/webpack/base.js index 116fcaf..3726108 100644 --- a/00 Base/config/webpack/base.js +++ b/00 Base/config/webpack/base.js @@ -2,6 +2,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CheckerPlugin } = require('awesome-typescript-loader'); const merge = require('webpack-merge'); const helpers = require('./helpers'); +var MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = merge( {}, @@ -11,7 +12,9 @@ module.exports = merge( extensions: ['.js', '.ts', '.tsx'], }, entry: { - app: ['./index.tsx'], + app: ['./index.tsx', + './content/styles.css', + ], }, module: { rules: [ @@ -25,6 +28,10 @@ module.exports = merge( babelCore: '@babel/core', }, }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + }, ], }, optimization: { @@ -45,6 +52,10 @@ module.exports = merge( template: 'index.html', }), new CheckerPlugin(), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }), ], } ); diff --git a/00 Base/package.json b/00 Base/package.json index c70f345..88807cf 100644 --- a/00 Base/package.json +++ b/00 Base/package.json @@ -8,16 +8,22 @@ "clean": "rimraf dist", "build": "npm run clean && webpack --config ./config/webpack/prod.js", "test": "jest -c ./config/test/jest.json --verbose", - "test:watch": "jest -c ./config/test/jest.json --verbose --watchAll -i" + "test:watch": "jest -c ./config/test/jest.json --verbose --watchAll -i", + "test:coverage": "jest -c ./config/test/jest.json --verbose --coverage" }, "author": "arp82", "license": "MIT", "dependencies": { "@material-ui/core": "^4.1.3", "axios": "^0.19.0", + "deep-freeze": "0.0.1", "react": "^16.8.6", "react-dom": "^16.8.6", - "react-router-dom": "^5.0.1" + "react-redux": "^7.2.0", + "react-router-dom": "^5.0.1", + "redux": "^4.0.5", + "redux-mock-store": "^1.5.4", + "redux-thunk": "^2.3.0" }, "devDependencies": { "@babel/cli": "^7.4.4", @@ -29,8 +35,12 @@ "@types/react-dom": "^16.8.4", "@types/react-router-dom": "^4.3.4", "awesome-typescript-loader": "^5.2.1", + "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", + "mini-css-extract-plugin": "^0.9.0", "rimraf": "^2.6.3", "ts-jest": "^24.0.2", "typescript": "^3.5.2", diff --git a/00 Base/src/app.tsx b/00 Base/src/app.tsx index f201da0..bef0ed5 100644 --- a/00 Base/src/app.tsx +++ b/00 Base/src/app.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { MyComponent } from './myComponent'; +import { TodosContainer } from './components'; -export const App: React.FunctionComponent = props => ( +export const App: React.FunctionComponent = () => (
- +
); diff --git a/00 Base/src/components/index.ts b/00 Base/src/components/index.ts new file mode 100644 index 0000000..0ebcc5a --- /dev/null +++ b/00 Base/src/components/index.ts @@ -0,0 +1 @@ +export { TodosContainer } from './todoList/todosContainer'; \ No newline at end of file diff --git a/00 Base/src/components/todoList/components/todoRow.spec.tsx b/00 Base/src/components/todoList/components/todoRow.spec.tsx new file mode 100644 index 0000000..99964dc --- /dev/null +++ b/00 Base/src/components/todoList/components/todoRow.spec.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { TodoEntity } from '../../../model/todo'; +import { TodoRowComponent } from './todoRow'; + +Enzyme.configure({ adapter: new Adapter() }) + +function setup(todo) { + const props = { + key: '1', + todo, + deleteTodo: jest.fn(), + } + + const enzymeWrapper = shallow() + + return { + props, + enzymeWrapper + } +} + +describe('TodoRow', () => { + it('should render self', () => { + const todo: TodoEntity = { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + } + + const { enzymeWrapper } = setup(todo) + + expect(enzymeWrapper.find('tr')).toHaveLength(1) + expect(enzymeWrapper.find('td')).toHaveLength(4) + expect(enzymeWrapper.find('span')).toHaveLength(3) + expect(enzymeWrapper.find('button').text()).toBe('Delete') + }) + + it('should call deleteTodo when the button is clicked', () => { + const event = jest.fn() + const { enzymeWrapper, props } = setup([]) + const deleteTodoButton = enzymeWrapper.find('button') + + deleteTodoButton.simulate('click', event) + + expect(props.deleteTodo).toHaveBeenCalledTimes(1) + }) +}); \ No newline at end of file diff --git a/00 Base/src/components/todoList/components/todoRow.tsx b/00 Base/src/components/todoList/components/todoRow.tsx new file mode 100644 index 0000000..5df01ff --- /dev/null +++ b/00 Base/src/components/todoList/components/todoRow.tsx @@ -0,0 +1,42 @@ +import { connect } from 'react-redux'; +import { deleteTodo } from '../../../redux/actions/todosActions'; + +import * as React from 'react'; +import {TodoEntity} from '../../../model/todo'; + +interface Props { + todo: TodoEntity; + deleteTodo: (id: number) => void +} + +export const TodoRowComponent = (props: Props) => { + const { deleteTodo } = props + const { id, title, completed } = props.todo + return ( + + + {id} + + + {title} + + + {completed ? 'Completed' : 'Pending'} + + + + + + ); +} + +const mapDispatchToProps = dispatch => { + return { + deleteTodo: (id: number) => {return dispatch(deleteTodo(id))}, + }; +} + +export const TodoRow = connect( + null, + mapDispatchToProps +)(TodoRowComponent); \ No newline at end of file diff --git a/00 Base/src/components/todoList/components/todoTable.spec.tsx b/00 Base/src/components/todoList/components/todoTable.spec.tsx new file mode 100644 index 0000000..12442a3 --- /dev/null +++ b/00 Base/src/components/todoList/components/todoTable.spec.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { TodoTableComponent } from './todoTable'; +import { TodoEntity } from '../../../model/todo'; + +Enzyme.configure({ adapter: new Adapter() }) + +function setup(todos) { + const props = { + todos, + } + + const enzymeWrapper = shallow() + + return { + props, + enzymeWrapper + } +} + +describe('TodoTable', () => { + it('should render self', () => { + const todos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ] + + const { enzymeWrapper } = setup(todos) + + expect(enzymeWrapper.find('div').hasClass('row')).toBe(true) + expect(enzymeWrapper.find('table').hasClass('table')) + expect(enzymeWrapper.find('table')).toHaveLength(1) + expect(enzymeWrapper.find('thead')).toHaveLength(1) + expect(enzymeWrapper.find('tr')).toHaveLength(1) + expect(enzymeWrapper.find('th')).toHaveLength(4) + expect(enzymeWrapper.find('Id')).toBeDefined() + expect(enzymeWrapper.find('Title')).toBeDefined() + expect(enzymeWrapper.find('Completed')).toBeDefined() + expect(enzymeWrapper.find('Action')).toBeDefined() + expect(enzymeWrapper.find('tbody')).toHaveLength(1) + }) +}); diff --git a/00 Base/src/components/todoList/components/todoTable.tsx b/00 Base/src/components/todoList/components/todoTable.tsx new file mode 100644 index 0000000..385068b --- /dev/null +++ b/00 Base/src/components/todoList/components/todoTable.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import {TodoEntity} from '../../../model/todo'; +import { TodoRow } from './todoRow'; + +interface Props { + todos: TodoEntity[]; +} + +export const TodoTableComponent = (props: Props) => { + const {todos } = props + return ( +
+ + + + + + + + + + + { + todos && todos.map((todo: TodoEntity) => + + ) + } + +
+ Id + + Title + + Completed + + Action +
+
+ ); +} \ No newline at end of file diff --git a/00 Base/src/components/todoList/todosContainer.spec.tsx b/00 Base/src/components/todoList/todosContainer.spec.tsx new file mode 100644 index 0000000..fdf3b24 --- /dev/null +++ b/00 Base/src/components/todoList/todosContainer.spec.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import Enzyme, { shallow } from 'enzyme'; +import { TodoEntity } from '../../model/todo'; +import { TodoAreaComponent } from './todosContainer'; +import Adapter from 'enzyme-adapter-react-16'; + +import { mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import ReduxThunk from 'redux-thunk'; +const middlewares = [ReduxThunk]; +const mockStore = configureStore(middlewares); + +Enzyme.configure({ adapter: new Adapter() }) + +function setup(todos) { + const props = { + todos, + loadTodos: jest.fn(), + addTodo: jest.fn(), + } + + const enzymeWrapper = shallow() + + return { + props, + enzymeWrapper + } +} + +describe('TodoAreaComponent', () => { + it('should render self', () => { + const todos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ] + + const { enzymeWrapper } = setup(todos) + + expect(enzymeWrapper.find('h1').text()).toBe('Todos') + expect(enzymeWrapper.find('div').hasClass('addTodoContainer')).toBe(true) + expect(enzymeWrapper.find('input').hasClass('todoInput')) + expect(enzymeWrapper.find('span').text()).toBe('New todo:') + expect(enzymeWrapper.find('button').text()).toBe('Add todo') + }) + + it('should call loadTodos when componentDidMount', () => { + const todos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ] + + const { props } = setup(todos) + expect(props.loadTodos).toBeCalled() + expect(props.addTodo).not.toBeCalled() + }) + + it('should call addTodo when the button is clicked', () => { + const event = jest.fn() + const { enzymeWrapper, props } = setup([]) + const newTodoButton = enzymeWrapper.find('button') + + newTodoButton.simulate('click', event) + + expect(props.addTodo).toHaveBeenCalledTimes(1) + }) + + it('if we dont have todos, should show de text ', () => { + const { enzymeWrapper } = setup([]) + + expect(enzymeWrapper.find('p').text()).toBe('No todos yet') + }) +}); \ No newline at end of file diff --git a/00 Base/src/components/todoList/todosContainer.tsx b/00 Base/src/components/todoList/todosContainer.tsx new file mode 100644 index 0000000..77f8421 --- /dev/null +++ b/00 Base/src/components/todoList/todosContainer.tsx @@ -0,0 +1,70 @@ +import { connect } from 'react-redux'; +import { TodoState } from '../../redux/reducers'; +import { getTodos, addTodo } from '../../redux/actions/todosActions'; + +import * as React from 'react'; +import {TodoTableComponent} from './components/todoTable'; +import { TodoEntity } from '../../model/todo'; + +interface State { + newTodo: string +} + +interface Props { + todos: Array; + loadTodos: () => void; + addTodo: (newTodo: string) => void; +} + +export class TodoAreaComponent extends React.Component { + + state = { + newTodo: "" + } + + componentDidMount() { + this.props.loadTodos() + } + + onAddSubmit = () => { + this.props.addTodo(this.state.newTodo) + this.setState({ newTodo: ''}) + } + + render() { + const { todos } = this.props + const { newTodo } = this.state + + return ( + <> +

Todos

+
+ New todo: + this.setState({ newTodo: event.target.value })} /> + +
+ {todos && todos.length > 0 ? + : +

No todos yet

} + + ) + } +} + +const mapStateToProps = (state: TodoState) => { + return{ + todos: state.todoReducer + }; +} + +const mapDispatchToProps = dispatch => { + return { + loadTodos: () => {return dispatch(getTodos())}, + addTodo: (newTodo: string) => {return dispatch(addTodo(newTodo))}, + }; +} + +export const TodosContainer = connect( + mapStateToProps, + mapDispatchToProps +)(TodoAreaComponent); \ No newline at end of file diff --git a/00 Base/src/content/styles.css b/00 Base/src/content/styles.css new file mode 100644 index 0000000..0a7e94d --- /dev/null +++ b/00 Base/src/content/styles.css @@ -0,0 +1,28 @@ +/* Default Table Style */ +.table { + color: #333; + background: white; + border: 1px solid grey; + font-size: 12pt; + border-collapse: collapse; +} + +table thead th, +table tfoot th { + color: #777; + background: rgba(0,0,0,.1); +} + +table caption { + padding:.5em; +} + +table th, +table td { + padding: .5em; + border: 1px solid lightgrey; +} + +.row { + padding-top: 20px; +} \ No newline at end of file diff --git a/00 Base/src/index.html b/00 Base/src/index.html index 2c5018a..196fd86 100644 --- a/00 Base/src/index.html +++ b/00 Base/src/index.html @@ -6,7 +6,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> - React testing by sample + Todos List
diff --git a/00 Base/src/index.tsx b/00 Base/src/index.tsx index ded7936..5a18166 100644 --- a/00 Base/src/index.tsx +++ b/00 Base/src/index.tsx @@ -1,5 +1,19 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { App } from './app'; +import { createStore, applyMiddleware, compose } from 'redux'; +import reduxThunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import { reducers } from './redux/reducers'; -ReactDOM.render(, document.getElementById('root')); +const composeEnhancers = window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose; + +const store = createStore(reducers, /* preloadedState, */ composeEnhancers( + applyMiddleware(reduxThunk) + )); + +ReactDOM.render( + + + + , document.getElementById('root')); diff --git a/00 Base/src/model/todo.ts b/00 Base/src/model/todo.ts new file mode 100644 index 0000000..7d4c836 --- /dev/null +++ b/00 Base/src/model/todo.ts @@ -0,0 +1,13 @@ +export interface TodoEntity { + userId: number; + id: number; + title: string; + completed: boolean; +} + +export const createDefaultTodoEntity = () => ({ + userId: -1, + id: -1, + title: '', + completed: false, +}) \ No newline at end of file diff --git a/00 Base/src/myApi/index.ts b/00 Base/src/myApi/index.ts index 3e74d70..fdec20b 100644 --- a/00 Base/src/myApi/index.ts +++ b/00 Base/src/myApi/index.ts @@ -1 +1 @@ -export { getListOfFruit } from './myApi'; \ No newline at end of file +export { getListOfTodos, addToTodoList, deleteFromTodoList } from './myApi'; \ No newline at end of file diff --git a/00 Base/src/myApi/myApi.spec.ts b/00 Base/src/myApi/myApi.spec.ts new file mode 100644 index 0000000..c9034b8 --- /dev/null +++ b/00 Base/src/myApi/myApi.spec.ts @@ -0,0 +1,47 @@ +import * as BEApi from './myBackEndApiEndpoint'; +import { todoState } from '../redux/reducers/todoReducer'; +import { getListOfTodos } from './myApi'; + +describe('myApi', () => { + const todos = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ] as todoState; + + it('getListOfTodos should call getTodos and return the todos', async () => { + jest.spyOn(BEApi, "getTodos").mockResolvedValue(todos) + + const result = await getListOfTodos() + + expect(BEApi.getTodos).toBeCalled() + expect(result).toEqual(todos); + }); + + it('addToTodoList should call addToTodoList and return the todos', async () => { + jest.spyOn(BEApi, "addToTodoList").mockResolvedValue(todos) + + const result = await BEApi.addToTodoList('new todo') + + expect(BEApi.addToTodoList).toBeCalled() + expect(result).toEqual(todos); + }); + + it('deleteFromTodoList should call deleteFromTodoList and return the todos', async () => { + jest.spyOn(BEApi, "deleteFromTodoList").mockResolvedValue(todos) + + const result = await BEApi.deleteFromTodoList(2) + + expect(BEApi.deleteFromTodoList).toBeCalled() + expect(result).toEqual(todos); + }); +}); diff --git a/00 Base/src/myApi/myApi.ts b/00 Base/src/myApi/myApi.ts index 988d0c5..9bdb338 100644 --- a/00 Base/src/myApi/myApi.ts +++ b/00 Base/src/myApi/myApi.ts @@ -1,15 +1,26 @@ import * as BEApi from './myBackEndApiEndpoint'; +import { TodoEntity } from '../model/todo'; -export const getListOfFruit = (): Promise => { - return BEApi.getFruits('http://fruityfruit.com') - .then(resolveFruits) +export const getListOfTodos = (): Promise => { + return BEApi.getTodos() + .then(resolveTodos) .catch(handleError); } -const resolveFruits = (fruits: string[]) => { - return fruits; +export const addToTodoList = (todo: string): Promise => { + return BEApi.addToTodoList(todo) + .then(resolveTodos) + .catch(handleError); } +export const deleteFromTodoList = (id: number): Promise => { + return BEApi.deleteFromTodoList(id) + .then(resolveTodos) + .catch(handleError); +} + +const resolveTodos = (todos: TodoEntity[]): TodoEntity[] => todos + const handleError = () => { - throw new Error('Where is my fruit???'); + throw new Error('Where is my todos???'); } \ No newline at end of file diff --git a/00 Base/src/myApi/myBackEndApiEndpoint.spec.ts b/00 Base/src/myApi/myBackEndApiEndpoint.spec.ts new file mode 100644 index 0000000..32124a2 --- /dev/null +++ b/00 Base/src/myApi/myBackEndApiEndpoint.spec.ts @@ -0,0 +1,84 @@ +import * as BEApi from './myBackEndApiEndpoint'; +import Axios from 'axios' +import { TodoEntity } from '../model/todo'; + +describe('myBackEndApiEndpoint', () => { + const todos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ]; + + it('getTodos should return the todos', async () => { + jest.spyOn(Axios, "get").mockResolvedValue(todos) + + await BEApi.getTodos() + + expect(Axios.get).toBeCalled() + }); + + it('addToTodoList should add todo the todos', async () => { + const newTodo: string = 'Do the test' + const newTodos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + }, + { + userId: 1, + id: 3, + title: 'Do the test', + completed: false, + } + ] + + await BEApi.setTodos(todos) + expect(BEApi.getTodosValue()).toEqual(todos) + + const result = await BEApi.addToTodoList(newTodo) + expect(result).toEqual(newTodos) + expect(BEApi.getTodosValue()).toEqual(newTodos) + }); + + it('deleteFromTodoList should delete todo from todos', async () => { + const todoToDelete: number = 1 + const newTodos: TodoEntity[] = [ + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + }, + { + userId: 1, + id: 3, + title: 'Do the test', + completed: false, + } + ] + + await BEApi.setTodos(todos) + expect(BEApi.getTodosValue()).toEqual(todos) + + const result = await BEApi.deleteFromTodoList(todoToDelete) + expect(result).toEqual(newTodos) + expect(BEApi.getTodosValue()).toEqual(newTodos) + }); +}); diff --git a/00 Base/src/myApi/myBackEndApiEndpoint.ts b/00 Base/src/myApi/myBackEndApiEndpoint.ts index 1457042..5f0e0fe 100644 --- a/00 Base/src/myApi/myBackEndApiEndpoint.ts +++ b/00 Base/src/myApi/myBackEndApiEndpoint.ts @@ -1,12 +1,35 @@ -export const getFruits = (_url: string) => { - return Promise.resolve([ - 'grape', - 'pineapple', - 'watermelon', - 'orange', - 'lemon', - 'strawberry', - 'cherry', - 'peach', - ]); -} \ No newline at end of file +import Axios from 'axios' +import { TodoEntity } from '../model/todo' + +let todos: TodoEntity[] = [] + +// Funnctions to test the other functions +export const setTodos = (newTodos: TodoEntity[]) => { + todos = newTodos +} +export const getTodosValue = () => todos + +export const getTodos = () => + Axios.get('https://jsonplaceholder.typicode.com/todos?userId=1') + .then( response => { + todos = response.data + return Promise.resolve(todos) + } + ) + +export const addToTodoList = (title: string) => { + const id = todos.length > 0 ? todos[todos.length - 1].id + 1 : 1 + const newTodo: TodoEntity = { + userId: 1, + id, + title, + completed: false + } + todos.push(newTodo) + return Promise.resolve(todos) +} + +export const deleteFromTodoList = (id: number) => { + todos = todos.filter(todo => todo.id !== id) + return Promise.resolve(todos) +} diff --git a/00 Base/src/myComponent/index.ts b/00 Base/src/myComponent/index.ts deleted file mode 100644 index bcc7da0..0000000 --- a/00 Base/src/myComponent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MyComponent } from './myComponent'; \ No newline at end of file diff --git a/00 Base/src/myComponent/myComponent.tsx b/00 Base/src/myComponent/myComponent.tsx deleted file mode 100644 index ea44068..0000000 --- a/00 Base/src/myComponent/myComponent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; - -export interface Props { - nameFromProps: string; -} - -export const MyComponent: React.FunctionComponent = props => { - const { nameFromProps } = props; - - return ( - <> -

Hello {nameFromProps}!

- - ); -}; \ No newline at end of file diff --git a/00 Base/src/redux/actions/todosActions.spec.ts b/00 Base/src/redux/actions/todosActions.spec.ts new file mode 100644 index 0000000..3aaaeb1 --- /dev/null +++ b/00 Base/src/redux/actions/todosActions.spec.ts @@ -0,0 +1,50 @@ +import { actionsEnums } from "../common/actionsEnums"; +import { completedAction } from "./todosActions"; +import { TodoEntity } from "../../model/todo"; + +describe('todosActions', () => { + it('should create an action to get todos', () => { + const todos: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false, + } + ] + const action = actionsEnums.ADD_REQUEST_COMPLETED + const expectedAction = { + type: action, + payload: todos, + } + expect(completedAction(todos, action)).toEqual(expectedAction) + }) + + it('should return action when is called', () => { + const todoResponse: TodoEntity[] = [ + { + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false + } + ] + + const result = completedAction(todoResponse, actionsEnums.TODO_REQUEST_COMPLETED); + + expect(result.type).toBe(actionsEnums.TODO_REQUEST_COMPLETED); + expect(result.payload).toBe(todoResponse); + }); +}); \ No newline at end of file diff --git a/00 Base/src/redux/actions/todosActions.ts b/00 Base/src/redux/actions/todosActions.ts new file mode 100644 index 0000000..69e8ea6 --- /dev/null +++ b/00 Base/src/redux/actions/todosActions.ts @@ -0,0 +1,40 @@ +import { getListOfTodos, addToTodoList, deleteFromTodoList } from "../../myApi"; +import { TodoEntity } from "../../model/todo"; +import { actionsEnums } from "../common/actionsEnums"; + +export const completedAction = (todos: TodoEntity[], action) => { + return { + type: action, + payload: todos + } +} + +export const getTodos = () => (dispatcher) => { + const promise = getListOfTodos(); + + promise.then( + (data) => dispatcher(completedAction(data, actionsEnums.TODO_REQUEST_COMPLETED)) + ) + + return promise; +} + +export const addTodo = (todo: string) => (dispatcher) => { + const promise = addToTodoList(todo); + + promise.then( + (data) => dispatcher(completedAction(data, actionsEnums.ADD_REQUEST_COMPLETED)) + ) + + return promise; +} + +export const deleteTodo = (id: number) => (dispatcher) => { + const promise = deleteFromTodoList(id); + + promise.then( + (data) => dispatcher(completedAction(data, actionsEnums.DELETE_REQUEST_COMPLETED)) + ) + + return promise; +} \ No newline at end of file diff --git a/00 Base/src/redux/common/actionsEnums.ts b/00 Base/src/redux/common/actionsEnums.ts new file mode 100644 index 0000000..53c85ae --- /dev/null +++ b/00 Base/src/redux/common/actionsEnums.ts @@ -0,0 +1,5 @@ +export const actionsEnums = { + TODO_REQUEST_COMPLETED: 'TODO_REQUEST_COMPLETED', + ADD_REQUEST_COMPLETED: 'ADD_REQUEST_COMPLETED', + DELETE_REQUEST_COMPLETED: 'DELETE_REQUEST_COMPLETED', + } \ No newline at end of file diff --git a/00 Base/src/redux/reducers/index.ts b/00 Base/src/redux/reducers/index.ts new file mode 100644 index 0000000..4a87506 --- /dev/null +++ b/00 Base/src/redux/reducers/index.ts @@ -0,0 +1,10 @@ +import { combineReducers} from 'redux'; +import { todoReducer, todoState } from './todoReducer'; + +export interface TodoState { + todoReducer : todoState; +}; + +export const reducers = combineReducers({ + todoReducer, +}); \ No newline at end of file diff --git a/00 Base/src/redux/reducers/todoReducer.spec.ts b/00 Base/src/redux/reducers/todoReducer.spec.ts new file mode 100644 index 0000000..646583b --- /dev/null +++ b/00 Base/src/redux/reducers/todoReducer.spec.ts @@ -0,0 +1,28 @@ +import { todoReducer, todoState } from "./todoReducer"; +import { actionsEnums } from "../common/actionsEnums"; + +describe('todoReducer', () => { + it('should update values when send correct type', () => { + const initialState = [] as todoState; + + const action = { + type: actionsEnums.TODO_REQUEST_COMPLETED, + payload: [{ + userId: 1, + id: 1, + title: 'Buy', + completed: true, + }, + { + userId: 1, + id: 2, + title: 'Go to the gym', + completed: false + }] as todoState, + }; + + const result = todoReducer(initialState, action); + + expect(result).toBe(action.payload); + }); +}); diff --git a/00 Base/src/redux/reducers/todoReducer.ts b/00 Base/src/redux/reducers/todoReducer.ts new file mode 100644 index 0000000..dc2d60f --- /dev/null +++ b/00 Base/src/redux/reducers/todoReducer.ts @@ -0,0 +1,15 @@ +import {actionsEnums} from '../common/actionsEnums'; +import {TodoEntity} from '../../model/todo'; + +export type todoState = TodoEntity[]; + +export const todoReducer = (state : todoState = [], action) => { + switch (action.type) { + case actionsEnums.TODO_REQUEST_COMPLETED: + case actionsEnums.ADD_REQUEST_COMPLETED: + case actionsEnums.DELETE_REQUEST_COMPLETED: + return action.payload; + default: + return state + } +}; \ No newline at end of file diff --git a/03 Test Hooks PablorRC/.babelrc b/03 Test Hooks PablorRC/.babelrc new file mode 100644 index 0000000..957cae3 --- /dev/null +++ b/03 Test Hooks PablorRC/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/03 Test Hooks PablorRC/.editorconfig b/03 Test Hooks PablorRC/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/03 Test Hooks PablorRC/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/03 Test Hooks PablorRC/.prettierrc b/03 Test Hooks PablorRC/.prettierrc new file mode 100644 index 0000000..c1a6f66 --- /dev/null +++ b/03 Test Hooks PablorRC/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/03 Test Hooks PablorRC/README.md b/03 Test Hooks PablorRC/README.md new file mode 100644 index 0000000..aa1b941 --- /dev/null +++ b/03 Test Hooks PablorRC/README.md @@ -0,0 +1,8 @@ +# Base project for the training + +React Testing Library is a library built on DOM Testing Library to provide support for React component testing. DOM Testing Library is a light-weight solution to write maintainable tests for UI applications. The main idea behind this library is to focus on the actual functionality behind a given application, so that the tests are more concerned with that the application does and what the user sees rather than implementation details and the abstractions behind them. This approach also favors maintainability in the long term, as the tests themselves should not break when refactoring or performing implementation changes as long as the same outwards functionality is kept. + +This repository covers some guided examples to introduce briefly React Testing Library and React Hooks Testing Library to write tests using Jest. For further examples, you may check some of the resources below +- https://react-hooks-testing-library.com/ +- https://testing-library.com/docs/intro +- https://github.com/Lemoncode/react-testing-by-example diff --git a/03 Test Hooks PablorRC/config/test/jest.json b/03 Test Hooks PablorRC/config/test/jest.json new file mode 100644 index 0000000..7cef52d --- /dev/null +++ b/03 Test Hooks PablorRC/config/test/jest.json @@ -0,0 +1,6 @@ +{ + "rootDir": "../../", + "preset": "ts-jest", + "restoreMocks": true, + "setupFilesAfterEnv": ["@testing-library/react/cleanup-after-each"] +} diff --git a/03 Test Hooks PablorRC/config/webpack/base.js b/03 Test Hooks PablorRC/config/webpack/base.js new file mode 100644 index 0000000..5922317 --- /dev/null +++ b/03 Test Hooks PablorRC/config/webpack/base.js @@ -0,0 +1,59 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { CheckerPlugin } = require('awesome-typescript-loader'); +const merge = require('webpack-merge'); +const helpers = require('./helpers'); +var MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = merge( + {}, + { + context: helpers.resolveFromRootPath('src'), + resolve: { + extensions: ['.js', '.ts', '.tsx'], + }, + entry: { + app: ['./index.tsx', './content/styles.css'], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'awesome-typescript-loader', + options: { + useBabel: true, + useCache: true, + babelCore: '@babel/core', + }, + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + }, + ], + }, + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + chunks: 'all', + name: 'vendor', + test: /[\\/]node_modules[\\/]/, + enforce: true, + }, + }, + }, + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + }), + new CheckerPlugin(), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }), + ], + } +); diff --git a/03 Test Hooks PablorRC/config/webpack/dev.js b/03 Test Hooks PablorRC/config/webpack/dev.js new file mode 100644 index 0000000..99acebc --- /dev/null +++ b/03 Test Hooks PablorRC/config/webpack/dev.js @@ -0,0 +1,19 @@ +const merge = require('webpack-merge'); +const base = require('./base'); +const helpers = require('./helpers'); + +module.exports = merge(base, { + mode: 'development', + devtool: 'inline-source-map', + output: { + path: helpers.resolveFromRootPath('dist'), + filename: '[name].js', + }, + devServer: { + inline: true, + host: 'localhost', + port: 8080, + stats: 'minimal', + hot: true, + }, +}); diff --git a/03 Test Hooks PablorRC/config/webpack/helpers.js b/03 Test Hooks PablorRC/config/webpack/helpers.js new file mode 100644 index 0000000..3a187bd --- /dev/null +++ b/03 Test Hooks PablorRC/config/webpack/helpers.js @@ -0,0 +1,5 @@ +const path = require('path'); + +const rootPath = path.resolve(__dirname, '../../'); + +exports.resolveFromRootPath = (...args) => path.join(rootPath, ...args); diff --git a/03 Test Hooks PablorRC/config/webpack/prod.js b/03 Test Hooks PablorRC/config/webpack/prod.js new file mode 100644 index 0000000..7435bd3 --- /dev/null +++ b/03 Test Hooks PablorRC/config/webpack/prod.js @@ -0,0 +1,11 @@ +const merge = require('webpack-merge'); +const base = require('./base'); +const helpers = require('./helpers'); + +module.exports = merge(base, { + mode: 'production', + output: { + path: helpers.resolveFromRootPath('dist'), + filename: './js/[name].[chunkhash].js', + }, +}); diff --git a/03 Test Hooks PablorRC/package.json b/03 Test Hooks PablorRC/package.json new file mode 100644 index 0000000..cd98a4a --- /dev/null +++ b/03 Test Hooks PablorRC/package.json @@ -0,0 +1,47 @@ +{ + "name": "react-testing-by-example", + "version": "1.0.0", + "description": "Learn testing by sample using jest + react-testing-library, each of the samples contains a readme.md file that indicates the purpose of the sample plus an step by step guide to reproduce it.", + "main": "index.js", + "scripts": { + "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:watch": "jest -c ./config/test/jest.json --verbose --watchAll -i --no-cache", + "test:coverage": "jest -c ./config/test/jest.json --verbose --coverage" + }, + "author": "arp82", + "license": "MIT", + "dependencies": { + "@material-ui/core": "^4.1.3", + "axios": "^0.19.2", + "css-loader": "^3.4.2", + "mini-css-extract-plugin": "^0.9.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-router-dom": "^5.0.1" + }, + "devDependencies": { + "@babel/cli": "^7.4.4", + "@babel/core": "^7.4.5", + "@babel/preset-env": "^7.4.5", + "@testing-library/react": "^8.0.7", + "@testing-library/react-hooks": "^3.2.1", + "@types/jest": "^24.0.13", + "@types/react": "^16.8.19", + "@types/react-dom": "^16.8.4", + "@types/react-router-dom": "^4.3.4", + "awesome-typescript-loader": "^5.2.1", + "html-webpack-plugin": "^3.2.0", + "jest": "^24.8.0", + "react-test-renderer": "^16.13.1", + "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" + } +} diff --git a/03 Test Hooks PablorRC/src/app.spec.tsx b/03 Test Hooks PablorRC/src/app.spec.tsx new file mode 100644 index 0000000..557f0ed --- /dev/null +++ b/03 Test Hooks PablorRC/src/app.spec.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { App } from './app'; + +describe('App', () => { + + it('debe cargar el título', () => { + const { getByText } = render(); + + const element = getByText('Tareas'); + expect(element).not.toBeNull(); + expect(element.tagName).toEqual('H1'); + }); + + it('debe cargar la tabla', () => { + const { getByText, getByTestId } = render(); + + const headerId = getByText('Id'); + expect(headerId).not.toBeNull(); + expect(headerId.tagName).toEqual('TH'); + + const headerTitle = getByText('Título'); + expect(headerTitle).not.toBeNull(); + expect(headerTitle.tagName).toEqual('TH'); + + const headerCompleted = getByText('Completada'); + expect(headerCompleted).not.toBeNull(); + expect(headerCompleted.tagName).toEqual('TH'); + + const headerActions = getByText('Acciones'); + expect(headerActions).not.toBeNull(); + expect(headerActions.tagName).toEqual('TH'); + + const newTodo = getByText('Nueva tarea:'); + expect(newTodo).not.toBeNull(); + expect(newTodo.tagName).toEqual('TD'); + + const inputElement = getByTestId('todo-input') as HTMLInputElement; + expect(inputElement.value).toEqual('') + + const newTodoButton = getByText('Agregar tarea'); + expect(newTodoButton).not.toBeNull(); + expect(newTodoButton.tagName).toEqual('BUTTON'); + }); +}); \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/app.tsx b/03 Test Hooks PablorRC/src/app.tsx new file mode 100644 index 0000000..5d068a6 --- /dev/null +++ b/03 Test Hooks PablorRC/src/app.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { TodosTable } from './components'; +import { useTodos } from './components/useTodos'; + +export const App = () => { + // // Data *-*-*- ELIMINAR + // const todosData = [ + // { userId: 1, id: 1, title: 'Comprar', completed: true }, + // { userId: 1, id: 2, title: 'Tirar la basura', completed: false }, + // { userId: 1, id: 3, title: 'Ir al gimnasio', completed: false }, + // ] + + // const todosData = [ + // { userId: 1, id: 1, title: 'Comprar', completed: true }, + // ] + + const {todos, deleteTodo, addTodo, updateTodo} = useTodos() + + return ( +
+

Tareas

+ +
+ ) +} \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/TodoRow.spec.tsx b/03 Test Hooks PablorRC/src/components/TodoRow.spec.tsx new file mode 100644 index 0000000..f9b384f --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/TodoRow.spec.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import { render, fireEvent, waitForElement } from '@testing-library/react'; +import { TodoRow } from '.'; + +describe('TodoRow', () => { + + it('si se le pasa una tarea completada, se debe de pintar Completada', () => { + const todo = { userId: 1, id: 1, title: 'Comprar', completed: true }; + + const { getByText } = render(); + + const textCompleted = getByText('Completada'); + expect(textCompleted).not.toBeNull(); + expect(textCompleted.tagName).toEqual('TD'); + }); + + it('si se le pasa una tarea no completada, se debe de pintar Pendiente', () => { + const todo = { userId: 1, id: 1, title: 'Comprar', completed: false }; + + const { getByText } = render(); + + const textCompleted = getByText('Pendiente'); + expect(textCompleted).not.toBeNull(); + expect(textCompleted.tagName).toEqual('TD'); + }); + + it('si no se está editando, la tarea tiene que mostrarse como texto', () => { + const todo = { userId: 1, id: 1, title: 'Comprar', completed: false }; + + const { getByText } = render(); + + const textCompleted = getByText('Pendiente'); + expect(textCompleted).not.toBeNull(); + expect(textCompleted.tagName).toEqual('TD'); + + const textTitle = getByText('Comprar'); + expect(textTitle).not.toBeNull(); + expect(textTitle.tagName).toEqual('TD'); + }); + + it('la función deleteTodo es llamada correctamente', async () => { + const deleteTodo = jest.fn(); + const todo = { userId: 1, id: 1, title: 'Comprar', completed: true }; + + const { getByTestId } = render(); + + const deleteTodoButton = await waitForElement(() => getByTestId('deleteTodo-button')) as HTMLButtonElement; + + fireEvent.click(deleteTodoButton) + + expect(deleteTodo).toHaveBeenCalledTimes(1) + }); + + // it('la función updateTodo es llamada correctamente', async () => { + // const updateTodo = jest.fn(); + // const todo = { userId: 1, id: 1, title: 'Comprar', completed: true }; + + // const { getByTestId } = render(); + + // const updateTodoButton = await waitForElement(() => getByTestId('updateTodo-button')) as HTMLButtonElement; + + // fireEvent.click(updateTodoButton) + + // expect(updateTodo).toHaveBeenCalledTimes(1) + // }); + +}); \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/TodoRow.tsx b/03 Test Hooks PablorRC/src/components/TodoRow.tsx new file mode 100644 index 0000000..c1545fb --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/TodoRow.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' + +export const TodoRow = props => { + const [ editing, setEditing ] = useState(false) + const [ currentTodo, setCurrentTodo ] = useState(props.todo) + + const onChangeInput = event => { + const { value } = event.target + + setCurrentTodo({ ...currentTodo, title: value}) + } + + const onCancel = () => { + setEditing(false) + setCurrentTodo({ ...currentTodo, title: props.todo.title}) + } + + const onAcept = () => { + setEditing(false) + props.updateTodo(id, currentTodo) + } + + const { id, title, completed } = props.todo + + return ( + + {id} + {editing ? ( + + ) : title} + {completed ? 'Completada' : 'Pendiente'} + {editing ? ( + + + + + ) : ( + + + + + )} + + ) +} diff --git a/03 Test Hooks PablorRC/src/components/TodosTable.spec.tsx b/03 Test Hooks PablorRC/src/components/TodosTable.spec.tsx new file mode 100644 index 0000000..e4d8914 --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/TodosTable.spec.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { render, fireEvent, waitForElement } from '@testing-library/react'; +import { TodosTable } from './TodosTable'; + +describe('TodosTable', () => { + + it('si no tenemos tareas, debe de mostrar el texto indicándolo', () => { + const todos = []; + + const { getByText } = render(); + + const element = getByText('No existen tareas'); + expect(element).not.toBeNull(); + expect(element.tagName).toEqual('TD'); + }); + + it('si se le pasa una tarea, se debe de pintar en la tabla correctamente', () => { + const todos = [{ userId: 1, id: 1, title: 'Comprar', completed: true }]; + + const { getByText } = render(); + + const elementTitle = getByText('Comprar'); + expect(elementTitle).not.toBeNull(); + expect(elementTitle.tagName).toEqual('TD'); + + const editButton = getByText('Editar'); + expect(editButton).not.toBeNull(); + expect(editButton.tagName).toEqual('BUTTON'); + + const deleteButton = getByText('Borrar'); + expect(deleteButton).not.toBeNull(); + expect(deleteButton.tagName).toEqual('BUTTON'); + }); + + it('al agregar una nueva tarea la función addTodo es llamada correctamente y se limpia el input', async () => { + const addTodo = jest.fn(); + const todos = [{ userId: 1, id: 1, title: 'Comprar', completed: true }]; + + const { getByTestId } = render(); + + const inputElement = getByTestId('todo-input') as HTMLInputElement; + const newTodoButton = await waitForElement(() => getByTestId('addTodo-button')) as HTMLButtonElement; + + fireEvent.change(inputElement, { target: { value: 'Tirar la basura' }}) + expect(inputElement.value).toEqual('Tirar la basura') + + fireEvent.click(newTodoButton) + + expect(addTodo).toHaveBeenCalledTimes(1) + expect(inputElement.value).toEqual('') + }); +}); \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/TodosTable.tsx b/03 Test Hooks PablorRC/src/components/TodosTable.tsx new file mode 100644 index 0000000..f0a59f8 --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/TodosTable.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react' +import { TodoRow } from '.' + +export const TodosTable = props => { + const initialFormState = { userId: 1, id: null, title: '', completed: false } + + const [ todo, setTodo ] = useState(initialFormState) + + const handleInputChange = event => { + const { name, value } = event.target + + setTodo({ ...todo, [name]: value }) + } + + const onAddSubmit = () => { + if (!todo.title) { + alert('Rellene la tarea para poder agregarla') + return + } + + props.addTodo(todo) + setTodo(initialFormState) + } + + return ( + + + + + + + + + + + {props.todos.length > 0 ? ( + props.todos.map(todo => ) + ) : ( + + + + )} + + + + + +
IdTítuloCompletadaAcciones
No existen tareas
+ Nueva tarea: + + + +
+ ) +} \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/index.ts b/03 Test Hooks PablorRC/src/components/index.ts new file mode 100644 index 0000000..89b242c --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/index.ts @@ -0,0 +1,2 @@ +export { TodosTable } from './TodosTable'; +export { TodoRow } from './TodoRow'; \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/useTodos.spec.tsx b/03 Test Hooks PablorRC/src/components/useTodos.spec.tsx new file mode 100644 index 0000000..d0efe3c --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/useTodos.spec.tsx @@ -0,0 +1,107 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useTodos, getTodos } from './useTodos' +import Axios from 'axios' + +describe('useTodos', () => { + it('al inicializar, las tareas están vacías y add, delete y update son funciones', () => { + const { result } = renderHook(() => useTodos()) + + expect(result.current.todos).toEqual([]) + expect(result.current.addTodo).toEqual(expect.any(Function)) + expect(result.current.deleteTodo).toEqual(expect.any(Function)) + expect(result.current.updateTodo).toEqual(expect.any(Function)) + }) + + // it('Se realiza la llamada correctamente a la api', async () => { + // const setTodos = jest.fn() + // const todosData = [ + // { userId: 1, id: 1, title: 'Comprar', completed: true }, + // { userId: 1, id: 2, title: 'Tirar la basura', completed: false }, + // { userId: 1, id: 3, title: 'Ir al gimnasio', completed: false }, + // ] + + // const getTodosData = jest.spyOn(Axios, 'get').mockResolvedValue({ + // data: todosData, + // }) + + // await getTodos(setTodos) + + // expect(getTodosData).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/todos?userId=1') + // expect(setTodos).toHaveBeenCalled() + // }) + + it('debe actualizarse cuando llamamos a setTodos', () => { + const todosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + { userId: 1, id: 2, title: 'Tirar la basura', completed: false }, + { userId: 1, id: 3, title: 'Ir al gimnasio', completed: false }, + ] + + const { result } = renderHook(() => useTodos()) + + act(() => { + result.current.setTodos(todosData) + }) + + expect(result.current.todos).toEqual(todosData) + }) + + it('debe agregarse correctamente el nuevo elemento cuando llamamos a addTodo', () => { + const todosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + ] + const newTodo = { userId: 1, id: null, title: 'Tirar la basura', completed: false } + const newTodosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + { userId: 1, id: 2, title: 'Tirar la basura', completed: false } + ] + + const { result } = renderHook(() => useTodos(todosData)) + + act(() => { + result.current.addTodo(newTodo) + }) + + expect(result.current.todos).toEqual(newTodosData) + }) + + it('debe eliminarse correctamente el elemento indicado cuando llamamos a deleteTodo', () => { + const todosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + { userId: 1, id: 2, title: 'Tirar la basura', completed: false } + ] + const idToDelete = 1 + const newTodosData = [ + { userId: 1, id: 2, title: 'Tirar la basura', completed: false } + ] + + const { result } = renderHook(() => useTodos(todosData)) + + act(() => { + result.current.deleteTodo(idToDelete) + }) + + expect(result.current.todos).toEqual(newTodosData) + }) + + it('debe actualizarse correctamente el elemento indicado cuando llamamos a updateTodo', () => { + const todosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + { userId: 1, id: 2, title: 'Tirar la basura', completed: false } + ] + const idToUpdate = 2 + const newTodo = { userId: 1, id: 2, title: 'Tirar la basura a las 20.00', completed: false } + const newTodosData = [ + { userId: 1, id: 1, title: 'Comprar', completed: true }, + { userId: 1, id: 2, title: 'Tirar la basura a las 20.00', completed: false } + ] + + const { result } = renderHook(() => useTodos(todosData)) + + act(() => { + result.current.updateTodo(idToUpdate, newTodo) + }) + + expect(result.current.todos).toEqual(newTodosData) + }) +}) \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/components/useTodos.tsx b/03 Test Hooks PablorRC/src/components/useTodos.tsx new file mode 100644 index 0000000..05ca01e --- /dev/null +++ b/03 Test Hooks PablorRC/src/components/useTodos.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import Axios from 'axios' + +export const getTodos = (setTodos) => + React.useEffect(() => { + Axios.get('https://jsonplaceholder.typicode.com/todos?userId=1') + .then(response => setTodos(response.data)); + }, []); + +export const useTodos = (todosData = []) => { + const [ todos, setTodos] = React.useState(todosData); + + getTodos(setTodos) + + const addTodo = (todo) => { + todo.id = todos[todos.length - 1].id + 1 + setTodos([ ...todos, todo ]) + } + + const deleteTodo = id => { + setTodos(todos.filter(todo => todo.id !== id)) + } + + const updateTodo = (id, updatedTodo) => { + setTodos(todos.map(todo => (todo.id === id ? updatedTodo : todo))) + } + + return { + todos, + setTodos, + getTodos, + addTodo, + deleteTodo, + updateTodo, + } +} \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/content/styles.css b/03 Test Hooks PablorRC/src/content/styles.css new file mode 100644 index 0000000..630e085 --- /dev/null +++ b/03 Test Hooks PablorRC/src/content/styles.css @@ -0,0 +1,23 @@ +table { + color: #333; + background: white; + border: 1px solid grey; + font-size: 12pt; + border-collapse: collapse; +} + +table thead th, +table tfoot th { + color: #777; + background: rgba(0,0,0,.1); +} + +table caption { + padding:.5em; +} + +table th, +table td { + padding: .5em; + border: 1px solid lightgrey; +} \ No newline at end of file diff --git a/03 Test Hooks PablorRC/src/index.html b/03 Test Hooks PablorRC/src/index.html new file mode 100644 index 0000000..64a436c --- /dev/null +++ b/03 Test Hooks PablorRC/src/index.html @@ -0,0 +1,14 @@ + + + + + + Tareas + + +
+ + diff --git a/03 Test Hooks PablorRC/src/index.tsx b/03 Test Hooks PablorRC/src/index.tsx new file mode 100644 index 0000000..ded7936 --- /dev/null +++ b/03 Test Hooks PablorRC/src/index.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { App } from './app'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/03 Test Hooks PablorRC/tsconfig.json b/03 Test Hooks PablorRC/tsconfig.json new file mode 100644 index 0000000..86c8569 --- /dev/null +++ b/03 Test Hooks PablorRC/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "declaration": false, + "noImplicitAny": false, + "sourceMap": true, + "jsx": "react", + "noLib": false, + "allowJs": true, + "suppressImplicitAnyIndexErrors": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["./src/**/*"], + "exclude": [ + "node_modules", + ] +}