I wanted to test that a count increases whenever I clicked on a button, but it seems not to work. Please Help! Here is the code...
describe('checkbtn', () => {
it('onClick', () => {
const { queryByTitle } = render(<Counter />);
const { queryByTitle } = render(<Counter />);
const btn = queryByTitle('button1');
const count = queryByTitle('count');
expect(count.innerHTML).toBe(count.innerHTML);
fireEvent.click(btn);
expect(count.innerHTML).toBe(count.innerHTML + 1);
})
})
First of all you expect some state to equal the same state + 1 here:
expect(count.innerHTML).toBe(count.innerHTML + 1);
It's the same as to write
const x = 2;
expect(x).toBe(x+2)
Second is that you try to add number to string which will result in not what you expect.
What you should do is to write explicit values in your test:
describe('checkbtn', () => {
it('onClick', () => {
const { queryByTitle } = render(<Counter />);
const btn = queryByTitle('button1');
const count = queryByTitle('count');
expect(count.innerHTML).toBe('1');
fireEvent.click(btn);
expect(count.innerHTML).toBe('2');
})
})
Related
In #testing-library/react-native I have a method provided by render called toJSON which I use to compare the resulting output
const { toJSON } = render(<HocText>simple string</HocText>);
const { toJSON: expectedToJSON } = render(<Text>simple string</Text>);
expect(toJSON()).toStrictEqual(expectedToJSON());
I am trying to find the equivalent of it in #testing-library/react.
I tried
const { asFragment } = render(<HocMyComponent text="should be as is" />);
const { asFragment: expectedAsFragment } = render(<span>should be as is</span>);
expect(asFragment()).toStrictEqual(expectedAsFragment());
But the result was a failure because it is missing my data.
asFragment was the correct one.
It as actually a bug on the test. But it was a bit hard to see so I had to do a serialization first.
type MyComponentProps = { text: string }
function MyComponent({ text }: MyComponentProps) {
return <span data-testid="my-element">{text}</span>
}
const MyComponentWithRef = forwardRef<HTMLSpanElement, MyComponentProps>(({ text }, ref) => <span data-testid="my-ref-element" ref={ref}>{text}</span>)
describe("hoc", () => {
it("simple component should work as expected", async () => {
render(<MyComponent text="bar" />);
expect(screen.getByTestId("my-element")).toHaveTextContent("bar");
})
it("should work with simple component", () => {
const serializer = new XMLSerializer();
const HocMyComponent = withHoc<MyComponentProps, MyComponentProps, HTMLSpanElement>(MyComponent);
const { asFragment } = render(<HocMyComponent text="should be as is" />);
expect(screen.getByTestId("my-element")).toHaveTextContent("should be as is");
const renderedValue = serializer.serializeToString(asFragment());
const { asFragment: expectedAsFragment } = render(<span data-testid="my-element">should be as is</span>);
expect(renderedValue).toStrictEqual(serializer.serializeToString(expectedAsFragment()));
});
But once I figured out what was wrong in the test. The resulting test is
it("should work with simple component", () => {
const HocMyComponent = withHoc<MyComponentProps, MyComponentProps, HTMLSpanElement>(MyComponent);
const { asFragment } = render(<HocMyComponent text="should be as is" />);
expect(screen.getByTestId("my-element")).toHaveTextContent("should be as is");
const { asFragment: expectedAsFragment } = render(<span data-testid="my-element">should be as is</span>);
expect(asFragment()).toStrictEqual(expectedAsFragment());
});
I need to make background change every second. if i use setinterval. the background changes too fast.
here's my code:
const { url, id, isButtonPrev, isButtonNext } = useOwnSelector(state => state.sliderReducer);
const img = useRef<HTMLImageElement>(null);
const dispatch = useOwnDispatch();
Here's function which chang background
const setBackGround = (index: number | null = null) => {
console.log(index)
if(img.current) {
img.current.src = `${url}${id < 10 ? `0${id}` : `${id}`}.jpg`;
img.current.onload = () => {
document.body.style.backgroundImage = `url(${img.current?.src})`;
if (index) dispatch(setId(index));
dispatch(isButton(''));
}
}
}
then I call this function:
setBackGround();
setInterval(() => {
setBackGround(id + 1);
}, 1000);
but background change very fast
I also tried to use the useEffect hook. But it didn’t help either
useEffect( () => {
const intervalID = setInterval(() => {
setBackGround(id + 1);
}, 1000);
return clearInterval(intervalID);
}, []);
useRef returns an object like {current: "value"}.Therefore, you need to use it as follows.
const imgRef = useRef<HTMLImageElement>(null);
if(imgRef.current){
imgRef.current.src = url;
}
codesandbox.io/s/github/Tmcerlean/battleship
I am developing a simple board game and need to increment a state variable when a player clicks on a cell with a valid move.
The functionality for validating the move and making the move is all in place, however, I am having difficulty updating the state within the event listener.
I can see that the state is being updated when observed from a useEffect hook, but not when viewed from within the function (even following successive calls).
I have done some reading and believe it could have something to do with having a stale closure, but I am not certain.
My approach to solve this issue was to remove and then re-add the click event listener following every click by the user.
My assumption was that this would cause the correct (newly incremented) state variable to be picked up. Unfortunately, this does not appear to be the case and within the event listener function, the variable is never incremented from 0.
I initialise the state variable here:
const [placedShips, setPlacedShips] = useState(0);
Next, a click event listener is applied to each cell within the gameboard:
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
You will see that the setPlacedships state variable is incremented here and there is a console log to report its value.
I am aware that the useState hook is asynchronous and so console.log will show 0 for the first time it is called. Consequently, I have a useEffect hook deployed outside of the function which also contains a console.log to report the changed value of setPlacedShips:
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips)
}, [placedShips])
After every click the placedShips variable is incremented by 1 and then two functions are run:
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
which is immediately followed by the original setEventListeners function:
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
As mentioned above, the issue is that the console log within the setEventListeners function constantly remains at 0, while the console log within the useEffect hook increments as expected.
For reference, here is the full component I am working on currently:
import React, { useEffect, useState, useLayoutEffect } from "react";
import gameboardFactory from "../../factories/gameboardFactory";
import Table from "../Reusable/Table";
import "./GameboardSetup.css";
// -----------------------------------------------
//
// Desc: Gameboard setup phase of game
//
// -----------------------------------------------
let playerGameboard = gameboardFactory();
const GameboardSetup = () => {
const [humanSetupGrid, setHumanSetupGrid] = useState([]);
const [ships, _setShips] = useState([
{
name: "carrier",
length: 5,
direction: "horizontal",
},
{
name: "battleship",
length: 4,
direction: "horizontal",
},
{
name: "cruiser",
length: 3,
direction: "horizontal",
},
{
name: "submarine",
length: 3,
direction: "horizontal",
},
{
name: "destroyer",
length: 2,
direction: "horizontal",
},
]);
const [placedShips, setPlacedShips] = useState(0);
const createGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(0);
}
};
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} />;
});
setHumanSetupGrid(result);
};
const setUpPlayerGrid = () => {
// createGrid('grid');
createUiGrid();
};
const currentShip = () => {
return ships[placedShips];
};
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
cell.addEventListener("mouseover", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
}
});
cell.addEventListener("mouseleave", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
}
});
});
};
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
useEffect(() => {
setUpPlayerGrid();
// setUpComputerGrid();
}, []);
useEffect(() => {
console.log(humanSetupGrid);
}, [humanSetupGrid]);
// Re-render the component to enable event listeners to be added to generated grid
useLayoutEffect(() => {
setEventListeners();
});
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips);
}, [placedShips]);
return (
<div className="setup-container">
<div className="setup-information">
<p className="setup-information__p">Add your ships!</p>
<button
className="setup-information__btn"
onClick={() => console.log(placedShips)}
>
Rotate
</button>
</div>
<div className="setup-grid">
<Table grid={humanSetupGrid} />
</div>
</div>
);
};
export default GameboardSetup;
I am quite confused what is happening here and have been stuck on this problem for a couple of days now - if anybody has any suggestions then they would be highly appreciated!
Thank you.
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
The above code does not remove any event listeners, which is probably the reason why 0 is still being logged. You pass a new anonymous function to removeEventListener. Since the function is just created it will never remove any event listeners, because it is not registered as an event listener.
Two different functions that do the same are not equal, which is why the event listener is not removed.
const a = (e) => clickListener(e); // passed to addEventListener
const b = (e) => clickListener(e); // passed to removeEventListener
console.log(a == b); //=> false
To add and remove events you cannot use anonymous functions. You either have to use named functions, or store the function in a variable. Then register and remove the event listener using the function name or variable.
Since you only forward the event to the clickListener you can simply replace your event handler registration with:
cell.addEventListener("click", clickListener);
Then remove it using:
cell.removeEventListener("click", clickListener);
Note that this scenario could've been avoided if you passed your event handlers using a more React approach. Instead of using cell.addEventHandler(...) you could've passed the event on creation of this element. eg. <div className='cell' id={counter} onClick={clickListener} />
When working with React you should try to not manipulate the DOM manually. React Components have Synthetic Events, which means that you don't need to add event listeners the vanilla way.
Just add each synthetic event to the cell component with its corresponding handler.
You can do it in the createUiGrid function:
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} onClick={onClickHandler} onMouseOut={onMouseOutHandler} onMouseOver={onMouseOverHandler} />;
});
setHumanSetupGrid(result);
};
And then just move the code you did on vanilla to each corresponding handler (be sure to remove all listener manipulation before testing).
I'm trying to get each alert dismissed after some time passes but when I try calling setTimeout/Interval in useEffect, my alerts dont display or dismiss properly. As it stands now, theres always one alert that doesnt get dismissed because allAlerts isnt consistently updated. How can I add setTimeout or setInterval to run my checkAlertTimes function after useEffect has stopped updating?
const [filteredAlerts, setFilteredAlerts] = useState()
const checkAlertTimes = () => {
const currentTime = new Date().getTime()
setFilteredAlerts(alertsToday.filter(alert => {
const alertTime = alert.date.getTime()
const secondsPassed = (currentTime - alertTime) /1000
if (secondsPassed < 30){return alert}
}))
}
useEffect(() => {
checkAlertTimes()
},[allAlerts])
return (
<div>{//map through and show alerts}</div>
)
From where I see you need something like this:
const [filteredAlerts, setFilteredAlerts] = useState();
const checkAlertTimes = () => {
const currentTime = new Date().getTime();
setFilteredAlerts(
alertsToday.filter((alert) => {
const alertTime = alert.date.getTime();
const secondsPassed = (currentTime - alertTime) / 1000;
if (secondsPassed < 30) {
return alert;
}
})
);
};
useEffect(() => {
const interval = setInterval(() => {
checkAlertTimes();
}, 1000);
return () => clearInterval(interval);
}, [allAlerts, alertsToday]);
return (
<div>
{
//map through and show alerts
}
</div>
);
I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
import React, { useState } from 'react';
const Countdown = ({ countDownTimerOpt }) => {
const [getObj, setObj] = useState({
formatTimer: null,
countDownTimer: 0,
intervalObj: null,
});
const { formatTimer, countDownTimer, intervalObj } = getObj;
if (countDownTimerOpt > 0 && intervalObj === null) {
startTimer();
}
function startTimer() {
const x = setInterval(() => {
timer();
}, 1000);
setObj((prev) => ({
...prev,
countDownTimer: countDownTimerOpt,
intervalObj: x,
}));
}
function timer() {
var days = Math.floor(countDownTimer / 24 / 60 / 60);
var hoursLeft = Math.floor(countDownTimer - days * 86400);
var hours = Math.floor(hoursLeft / 3600);
var minutesLeft = Math.floor(hoursLeft - hours * 3600);
var minutes = Math.floor(minutesLeft / 60);
var remainingSeconds = countDownTimer % 60;
const formatTimer1 =
pad(days) +
':' +
pad(hours) +
':' +
pad(minutes) +
':' +
pad(remainingSeconds);
if (countDownTimer === 0) {
clearInterval(intervalObj);
} else {
setObj((prev) => ({
...prev,
formatTimer: formatTimer1,
countDownTimer: prev['countDownTimer'] - 1,
}));
}
}
function pad(n) {
return n < 10 ? '0' + n : n;
}
return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;
import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';
const Training = () => {
const [getValue, setValue] = useState(0);
useEffect(() => {
const x = setTimeout(() => {
console.log('setTimeout');
setValue(10000);
}, 5000);
return () => clearInterval(x);
}, []);
return <Timer countDownTimerOpt={getValue} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
const useTimer = (startTime) => {
const [time, setTime] = useState(startTime)
const [intervalID, setIntervalID] = useState(null)
const hasTimerEnded = time <= 0
const isTimerRunning = intervalID != null
const update = () => {
setTime(time => time - 1)
}
const startTimer = () => {
if (!hasTimerEnded && !isTimerRunning) {
setIntervalID(setInterval(update, 1000))
}
}
const stopTimer = () => {
clearInterval(intervalID)
setIntervalID(null)
}
// clear interval when the timer ends
useEffect(() => {
if (hasTimerEnded) {
clearInterval(intervalID)
setIntervalID(null)
}
}, [hasTimerEnded])
// clear interval when component unmounts
useEffect(() => () => {
clearInterval(intervalID)
}, [])
return {
time,
startTimer,
stopTimer,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
import { useEffect, useRef, useState } from 'react';
export const useCountDown: (
total: number,
ms?: number,
) => [number, () => void, () => void, () => void] = (
total: number,
ms: number = 1000,
) => {
const [counter, setCountDown] = useState(total);
const [startCountDown, setStartCountDown] = useState(false);
// Store the created interval
const intervalId = useRef<number>();
const start: () => void = () => setStartCountDown(true);
const pause: () => void = () => setStartCountDown(false);
const reset: () => void = () => {
clearInterval(intervalId.current);
setStartCountDown(false);
setCountDown(total);
};
useEffect(() => {
intervalId.current = setInterval(() => {
startCountDown && counter > 0 && setCountDown(counter => counter - 1);
}, ms);
// Clear interval when count to zero
if (counter === 0) clearInterval(intervalId.current);
// Clear interval when unmount
return () => clearInterval(intervalId.current);
}, [startCountDown, counter, ms]);
return [counter, start, pause, reset];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv