Mariana Valencia Gallego
Wilmer Garzon Cabezas
Team PlatziJose Santiago Urrego Olaya
Juan Jose Bernal Villamarin
Wilmer Garzon Cabezas
Team PlatziCristian Contreras
Wilmer Garzon Cabezas
Team PlatziElioenai Garcia
Wilmer Garzon Cabezas
Team PlatziUlqernesh Karvenae
Ulqernesh Karvenae
Salome Arboleda
Relacionado al setState, siempre que se tenga en cuenta el estado original para sumarle, restarle, etc, es buena práctica hacerlo de esta manera
setContador((prev) => prev + 1)}
De lo contrario, podría dependiendo de la función y cómo esta sea llamada, generar errores en el resultado final y no dar el resultado esperado
Por ejemplo el siguiente bloque de código, escrito como está en el curso, se esperaría que sumara 3 veces pero solo suma 1 vez ya que en el momento en que la función es llamada el valor del estado es 0 y nunca se suma
const handleClick = () => {
setContador(contador + 1);
setContador(contador + 1);
setContador(contador + 1);
};
Gran aporte, muchas gracias Mariana! 💚
Correcto, esto se debe a que las actualizaciones del estado en React no son inmediatas, sino React agrupa actualizaciones (batching), por lo que puede tomar el valor actual del estado.
Usando TDD lo cree así:
import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, act } from '@testing-library/react'; import TodoList from './TodoList'; describe('<TodoList />', () => { it('should display input to enter Todo activity ', () => { render(<TodoList />); const input = screen.getByPlaceholderText('Enter Todo Activity'); expect(input).toBeInTheDocument(); }); it('should display button to add Todo', () => { render(<TodoList />); const button = screen.getByText('Add Todo'); expect(button).toBeInTheDocument(); }); it('should display ul empty at the begin', () => { render(<TodoList />); const ul = screen.getByRole('list'); expect(ul).toBeEmptyDOMElement(); }); it('should add todo to the list', async () => { render(<TodoList />); const input = screen.getByPlaceholderText('Enter Todo Activity'); fireEvent.change(input, { target: { value: 'Learn Vitest' } }); const button = screen.getByText('Add Todo'); await act(async () => { fireEvent.click(button); }); const ul = screen.getByRole('list'); expect(ul).not.toBeEmptyDOMElement(); expect(ul).toContainHTML('<li>Learn Vitest</li>'); expect(screen.getByText('Learn Vitest')).toBeInTheDocument(); }); }); ```import { describe, it, expect } from 'vitest';import { render, screen, fireEvent, act } from '@testing-library/react';import TodoList from './TodoList';describe('\<TodoList />', () => { it('should display input to enter Todo activity ', () => { render(*\<TodoList* */>*); const input = screen.getByPlaceholderText('Enter Todo Activity'); expect(input).toBeInTheDocument();   });   it('should display button to add Todo', () => { render(*\<TodoList* */>*); const button = screen.getByText('Add Todo'); expect(button).toBeInTheDocument();   });   it('should display ul empty at the begin', () => { render(*\<TodoList* */>*); const ul = screen.getByRole('list'); expect(ul).toBeEmptyDOMElement(); });   it('should add todo to the list', async () => { render(*\<TodoList* */>*); const input = screen.getByPlaceholderText('Enter Todo Activity'); fireEvent.change(input, { target: { value: 'Learn Vitest' } });   const button = screen.getByText('Add Todo'); await act(async () => { fireEvent.click(button); }); const ul = screen.getByRole('list'); expect(ul).not.toBeEmptyDOMElement(); expect(ul).toContainHTML('\<li>Learn Vitest\</li>'); expect(screen.getByText('Learn Vitest')).toBeInTheDocument(); }); }); 1. src/TodoList/TodoList.tsx ```js import React, {useState} from 'react'; const TodoList = () => { const [todoValue, setTodoValue] = useState(""); const [list, setList] = useState<string[]>([]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setTodoValue(e.target.value); } const addTodo = () => { setList([...list, "Learn Vitest"]); } return ( <div> <input type="text" placeholder="Enter Todo Activity" value={todoValue} onChange={handleChange}/> <button onClick={addTodo}>Add Todo</button> <ul> { list.map((item, index) => ( <li key={index}>{item}</li> )) } </ul> </div> ); } export default TodoList; ```import React, {useState} from 'react'; const TodoList = () => { const \[todoValue, setTodoValue] = useState("");   const \[list, setList] = useState\<string\[]>(\[]);   const handleChange = (e: React.ChangeEvent\<HTMLInputElement>) => { setTodoValue(e.target.value); }   const addTodo = () => { setList(\[...list, "Learn Vitest"]); }   return ( *<*div*>* *<*input type="text" placeholder="Enter Todo Activity" value={todoValue} onChange={handleChange}*/>* *<*button onClick={addTodo}*>*Add Todo*\</*button*>* *<*ul*>* { list.map((item, index) => ( *<*li key={index}*>*{item}*\</*li*>* )) } *\</*ul*>* *\</*div*>* );} export default TodoList;
Gran aporte Juan! 💚
Los comportamientos no deterministas se refieren a situaciones en las que el resultado de una operación puede variar en diferentes ejecuciones, incluso con los mismos datos de entrada. Esto puede ocurrir, por ejemplo, en sistemas que dependen de la fecha y hora actual o en conexiones a servicios externos que pueden fallar o comportarse de manera inconsistente. Estos comportamientos son problemáticos en testing, ya que pueden llevar a resultados inesperados. Por eso, se recomienda usar mocks para simular estos comportamientos y garantizar la fiabilidad de las pruebas.
Gran aporte Cristian! 💚
Otra refactorización que podría ser en el componente <u>Contador </u>es reutilizar el Button que se creó en la clase 4 😎
Great! gran aporte Elioenai! 💚
Reto interesante
import { describe, it, expect, vi, afterEach } from "vitest"; import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Timer } from "./Timer"; describe("<Timer />", () => { afterEach(() => { vi.useRealTimers(); }); it("should show Start when hovered", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer", { name: /timer/i }); await user.hover(timer); expect(timer).toHaveTextContent("Start"); }); it("should display a valid time format", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer"); await user.click(timer); expect(timer).toHaveTextContent(/^\d{2}:\d{2}$/); }); it("should decrement every second", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await act(async () => { await vi.advanceTimersByTimeAsync(1000); }); expect(timer).toHaveTextContent("00:59"); }); it("should stop when clicked again", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(3000); expect(timer).toHaveTextContent("00:59"); }); it("should show Continue when paused and hovered", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer"); await user.click(timer); await user.click(timer); await user.unhover(timer); await user.hover(timer); expect(timer).toHaveTextContent(/continue/i); }); it("should continue when clicked after being paused", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(3000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:58"); }); it("should restart when clicked again", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(60000); expect(timer).toHaveTextContent("00:00"); await user.click(timer); expect(timer).toHaveTextContent("01:00"); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); }); });
import { describe, it, expect, vi, afterEach } from "vitest"; import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Timer } from "./Timer"; describe("<Timer />", () => { afterEach(() => { vi.useRealTimers(); }); it("should show Start when hovered", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer", { name: /timer/i }); await user.hover(timer); expect(timer).toHaveTextContent("Start"); }); it("should display a valid time format", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer"); await user.click(timer); expect(timer).toHaveTextContent(/^\d{2}:\d{2}$/); }); it("should decrement every second", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await act(async () => { await vi.advanceTimersByTimeAsync(1000); }); expect(timer).toHaveTextContent("00:59"); }); it("should stop when clicked again", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(3000); expect(timer).toHaveTextContent("00:59"); }); it("should show Continue when paused and hovered", async () => { const user = userEvent.setup(); render(<Timer />); const timer = screen.getByRole("timer"); await user.click(timer); await user.click(timer); await user.unhover(timer); await user.hover(timer); expect(timer).toHaveTextContent(/continue/i); }); it("should continue when clicked after being paused", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(3000); expect(timer).toHaveTextContent("00:59"); await user.click(timer); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:58"); }); it("should restart when clicked again", async () => { vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] }); const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime, }); render(<Timer time={60} />); const timer = screen.getByRole("timer"); await user.click(timer); await vi.advanceTimersByTimeAsync(60000); expect(timer).toHaveTextContent("00:00"); await user.click(timer); expect(timer).toHaveTextContent("01:00"); await vi.advanceTimersByTimeAsync(1000); expect(timer).toHaveTextContent("00:59"); }); });
Así cree mi TDD y también aplique Table Driven Testing:
import { it, expect, describe } from 'vitest'; import { render, screen, fireEvent, act } from '@testing-library/react'; import { Timer } from './Timer'; const actionButtons = [{ name: 'Start' }, { name: 'Stop' }, { name: 'Reset' }]; describe('⏰ <Timer /> ⏰', () => { it('should have a title', () => { render(<Timer />); const title = screen.getByRole('heading', { level: 2, name: 'Timer' }); expect(title).toBeInTheDocument(); }); it('should show the initial time as 00:00:00', () => { render(<Timer />); const time = screen.getByText('00:00:00'); expect(time).toBeInTheDocument(); }); it.each(actionButtons)('should have a $name button', ({ name }) => { render(<Timer />); const button = screen.getByRole('button', { name }); expect(button).toBeInTheDocument(); }); it('start and stop the timer when clicking', async () => { render(<Timer />); const startButton = screen.getByRole('button', { name: 'Start' }); const stopButton = screen.getByRole('button', { name: 'Stop' }); await act(() => { fireEvent.click(startButton); }); const time = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(time).toBeInTheDocument(); await act(() => { fireEvent.click(stopButton); }); const stoppedTime = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(stoppedTime).toBeInTheDocument(); }); it('reset the timer when clicking reset button', async () => { render(<Timer />); const startButton = screen.getByRole('button', { name: 'Start' }); const resetButton = screen.getByRole('button', { name: 'Reset' }); await act(() => { fireEvent.click(startButton); }); const time = await screen.findByText('00:00:01', {}, { timeout: 2000 }); expect(time).toBeInTheDocument(); await act(() => { fireEvent.click(resetButton); }); const resetTime = await screen.findByText('00:00:00', {}, { timeout: 2000 }); expect(resetTime).toBeInTheDocument(); }); });
Mi componente:
import { useState, useRef, useEffect } from 'react'; const Timer = () => { const [time, setTime] = useState('00:00:00'); const timerRef = useRef<NodeJS.Timeout | null>(null); const start = () => { timerRef.current = setInterval(() => { setTime(prevTime => { const [hours, minutes, seconds] = prevTime.split(':').map(Number); let totalSeconds = hours * 3600 + minutes * 60 + seconds + 1; const newHours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0'); totalSeconds %= 3600; const newMinutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0'); const newSeconds = String(totalSeconds % 60).padStart(2, '0'); return `${newHours}:${newMinutes}:${newSeconds}`; }); }, 1000); }; const stop = () => cleanTimer(); const reset = () => { cleanTimer(); timerRef.current = null; setTime('00:00:00'); }; const cleanTimer = () => { if (timerRef.current) clearInterval(timerRef.current); }; useEffect(() => { return () => { cleanTimer(); }; }, []); return ( <div> <h2>Timer</h2> <strong>{time}</strong> <div> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> <button onClick={reset}>Reset</button> </div> </div> ); }; export { Timer };