Skip to content

Commit

Permalink
Add demo + coverage badge
Browse files Browse the repository at this point in the history
  • Loading branch information
fdodino committed Aug 4, 2024
1 parent 083bc2e commit 5717cee
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 76 deletions.
118 changes: 44 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@

# Contador en React Context

[![Build React App](https://github.com/uqbar-project/eg-contador-react-context/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/uqbar-project/eg-contador-react-context/actions/workflows/build.yml) ![coverage](./badges/coverage/coverage.svg)
[![Build React App](https://github.com/uqbar-project/eg-contador-react-context/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/uqbar-project/eg-contador-react-context/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/uqbar-project/eg-contador-react-context/graph/badge.svg?token=42JI8APW8Q)](https://codecov.io/gh/uqbar-project/eg-contador-react-context)

![video](images/demo2.gif)
![video](video/demo2024.gif)

## La aplicación

El ejemplo consiste en un simple contador numérico, al que le podemos incrementar o decrementar su valor de uno en uno. Para ayudar a entender el funcionamiento de React Context, incorporamos un _log_ que mostrará cada operación de suma o resta que haya pedido el usuario, con la opción de poder borrarlo.

## Framework de UI CSS

Antes de comenzar con los conceptos más salientes, la UI fue hecha con los componentes que provee [Semantic UI React](https://react.semantic-ui.com/).

## React Context

El API de React Context permite unificar el estado entre los componentes de una aplicación.
Expand Down Expand Up @@ -42,10 +38,10 @@ Tenemos tres componentes en nuestra aplicación:

En el context vamos a definir como estado compartido el valor numérico actual y la lista de logs:

archivo _src/context/Context.js_
archivo _src/context/Context.tsx_

```js
export const Context = createContext()
export const Context = createContext<LogContext | null>(null)
```

## Definiendo nuestro propio Provider
Expand All @@ -55,12 +51,12 @@ Tendremos tres acciones: subir un valor, bajar un valor (ambas generan un nuevo
- el valor actual
- los logs

archivo _src/context/Context.js_
archivo _src/context/Context.tsx_

```js
export const Provider = ({ children }) => {
```ts
export const Provider = ({ children }: { children: ReactNode }) => {
const [count, setCount] = useState(0)
const [logs, setLogs] = useState([])
const [logs, setLogs] = useState<Log[]>([])
```
El contexto publica en una referencia value lo que podemos luego utilizar en los componentes que trabajen con ese contexto:
Expand All @@ -70,8 +66,8 @@ El contexto publica en una referencia value lo que podemos luego utilizar en los
- y funciones para subir y bajar el valor
- y una función que elimine el log
```js
const addLog = (action) => {
```ts
const addLog = (action: ActionLog) => {
const newLogs = logs.concat(new Log(action))
setLogs(newLogs)
}
Expand All @@ -82,14 +78,14 @@ El contexto publica en una referencia value lo que podemos luego utilizar en los
logs,
// Funciones que afectan al estado
decrement: () => {
addLog('DECREMENT')
addLog(ActionLog.DECREMENT)
setCount(count - 1)
},
increment: () => {
addLog('INCREMENT')
addLog(ActionLog.INCREMENT)
setCount(count + 1)
},
deleteLog: (logToDelete) => {
deleteLog: (logToDelete: Log) => {
// fíjense que el context está tomando una responsabilidad
const newLogs = logs.filter((log) => logToDelete.id !== log.id)
setLogs(newLogs)
Expand All @@ -99,7 +95,7 @@ El contexto publica en una referencia value lo que podemos luego utilizar en los
Por último devuelve un componente React que trabaja con la `prop children` porque el Context va a decorar (envolver, wrappear) los componentes que lo utilicen:
```js
```tsx
return (
<Context.Provider value={value}>
{children}
Expand All @@ -110,7 +106,7 @@ Por último devuelve un componente React que trabaja con la `prop children` porq

Recordemos que `this.props.children` permite definir componentes React hijos asociados a nuestro Provider, que simplemente los muestra (es una especie de **template method**).

```js
```tsx
const App = () => (
<Provider >
<Contador />
Expand All @@ -121,8 +117,8 @@ const App = () => (

Un detalle respecto a los cambios de estado: es importante que todo cambio de estado modifique el objeto con el que se trabaja. En el agregado de un log, podemos utilizar el método push como alternativa, siempre que generemos una copia:

```js
const addLog = (action) => {
```ts
const addLog = (action: ActionLog) => {
const newLogs = [...logs]
newLogs.push(new Log(action))
setLogs(newLogs)
Expand All @@ -133,7 +129,7 @@ Si no hacemos la copia superficial `[...logs]`, los cambios no se distribuirán

## Enlazando las acciones con cada componente

Para mapear las acciones y estado a los componentes deberiamos usar un **Consumer**. Al igual que para los casos de `useState` y `useEffect`, React Context trae una función que se llama `useContext` a la cual le debemos pasar por argumento el **Context** que queremos usar.
Para mapear las acciones y estado a los componentes deberiamos usar un **Consumer**. Al igual que para los casos de `useState` y `useOnInit/useEffect`, React Context trae una función que se llama `useContext` a la cual le debemos pasar por argumento el **Context** que queremos usar.

### Componente Contador

Expand All @@ -142,22 +138,22 @@ Mapearemos entonces en el componente contador:
- como **state del context** la propiedad count (no nos interesan los logs)
- como **acciones**, las acciones para subir o bajar el contador (increment y decrement)

```js
const { count, decrement, increment } = useContext(Context)
```tsx
const { count, decrement, increment } = useContext(Context)!
```

Entonces podemos usar libremente en nuestra función render las referencias count, increment y decrement:

```js
```tsx
return (
<Container textAlign="center">
<div className="container">
...
<div>
<Button primary data-testid="button_minus" onClick={decrement}>-</Button>
<Label data-testid="contador" circular color={color(count)} size="huge">{count}</Label>
<Button secondary data-testid="button_plus" onClick={increment}>+</Button>
<div className="contador">
<button data-testid="button_minus" className="secondary" onClick={decrement}>-</button>
<label data-testid="contador">{count}</label>
<button data-testid="button_plus" className="primary" onClick={increment}>+</button>
</div>
</Container>
</div>
)
```

Expand All @@ -168,22 +164,18 @@ En el componente LogContador mapearemos:
- como **state del context** la propiedad logs
- como **acción**, la funcion de borrar un log que recibe por parametro el log..

```js
const { deleteLog, logs } = useContext(Context)
```

El lector puede ver cómo el botón Eliminar llama a la función `deleteLog` y cómo se reciben los logs para renderizar cada LogRow.

## Testing

### Tests sobre el contador

Hablaremos de los tests más interesantes, el resto pueden consultarse en [App.test.js](./src/App.test.js). Veamos por ejemplo, cómo simulamos que al presionar el botón **+**
Hablaremos de los tests más interesantes, el resto pueden consultarse en [App.test.tsx](./src/App.test.tsx). Veamos por ejemplo, cómo simulamos que al presionar el botón **+**

- por un lado sube el valor del contador
- por otro lado se genera una nueva fila en el log

```js
```tsx
test('si se presiona el botón +, se agrega un log', () => {
render(
<App />
Expand All @@ -197,55 +189,33 @@ test('si se presiona el botón +, el contador pasa a estar en 1', () => {
<App />
)
fireEvent.click(screen.getByTestId('button_plus'))
expect(screen.getByTestId('contador')).toHaveTextContent('1')
expect(screen.getByTestId('contador').textContent).toBe('1')
})
```

Estos tests no son tan unitarios: prueba que se presiona el botón +, eso dispara la función increment(), lo que devuelve un nuevo estado y eso termina generando el render de la vista, por lo tanto esperamos que en el Label ahora esté el valor 1 y que se muestre un nuevo log.

Fíjense que elegimos repetir las dos líneas de código que forman parte del Arrange / Act en ambos tests.

- por un lado, queremos mantener ambos tests separados, porque están probando distintos componentes (no es una buena idea tener un solo test con los dos expect porque pertenecen a casos de prueba diferentes)
- por otra parte, envolver esas líneas en una función la "alejaría" lo que luego necesitamos preguntar: `getAllByTestId`, `getByTestId` tendrían que ser devueltas por esa función, oscureciendo un poco el entendimiento:

```js
describe('si se presiona el botón -', () => {
beforeEach(() => {
render(
<App />
)
fireEvent.click(resultApp.getByTestId('button_minus'))
})

test('se agrega un log', () => {
expect(screen.getAllByTestId('LogRow')).toHaveLength(1)
})

test('el contador pasa a estar en -1', () => {
expect(screen.getByTestId('contador')).toHaveTextContent('-1')
})
})
```

y la razón para no hacerlo es que **el linter explícitamente se queja si intentamos llamar a la función render dentro del beforeEach (es una práctica desaconsejada)**.
Fíjense que elegimos repetir las dos líneas de código que forman parte del Arrange / Act en ambos tests. Queremos mantener ambos tests separados, porque están probando distintos componentes (no es una buena idea tener un solo test con los dos expect porque pertenecen a casos de prueba diferentes)

### Delete del log

Y el último test es una prueba end-to-end bastante exhaustiva: el usuario presiona el botón +, eso además de modificar el valor agrega un log. Entonces podemos presionar el botón "Eliminar log" para luego chequear que la lista de logs queda vacía:

```js
```tsx
test('cuando el usuario presiona el botón Delete Log se elimina un log', () => {
render(
<App />
)
const actualIndex = Log.getLastIndex()
fireEvent.click(screen.getByTestId('button_plus'))
expect(screen.queryAllByTestId('LogRow')).toHaveLength(1)
fireEvent.click(screen.getByTestId('button_deleteLog_' + actualIndex))
expect(screen.queryAllByTestId('LogRow')).toHaveLength(0)
})
render(
<App />
)
const actualIndex = Log.getLastIndex()
fireEvent.click(screen.getByTestId('button_plus'))
expect(screen.queryAllByTestId('LogRow')).toHaveLength(1)
fireEvent.click(screen.getByTestId('button_deleteLog_' + actualIndex))
expect(screen.queryAllByTestId('LogRow')).toHaveLength(0)
})
```

## Material relacionado

- [Context - documentación oficial de React](https://es.reactjs.org/docs/context.html)
- [Context - documentación oficial de React](https://react.dev/learn/passing-data-deeply-with-context)
- [useContext](https://react.dev/reference/react/useContext)
- [createContext](https://react.dev/reference/react/createContext)
Binary file removed images/demo2.gif
Binary file not shown.
3 changes: 1 addition & 2 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ describe('tests del contador', () => {
render(
<App />
)
fireEvent
.click(screen.getByTestId('button_plus'))
fireEvent.click(screen.getByTestId('button_plus'))
expect(screen.getAllByTestId('LogRow')).toHaveLength(1)
})

Expand Down
Binary file removed src/assets/delete.png
Binary file not shown.
Binary file added video/demo2024.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 5717cee

Please sign in to comment.