Testing/mocking a service inside functional container in React/Jest - reactjs

I am stuck in a weird scenario. I wrote a functional component a few days back. It had state hooks in it and also context hooks. I am calling an API in a function based on a click. Here's the function
const gameService = new Game();
const Toolbar = () => {
const [name, setName] = useState("");
const [addVisible, setAddVisible] = useState(false);
const [loading, setLoading] = useState(false);
const errorContext = useContext(ErrorContext);
const appContext = useContext(AppContext);
const addGame = async () => {
setLoading(true);
try {
const updatedGames = await gameService.add({
name
});
appContext.setGames(updatedGames);
setLoading(false);
setName('');
setAddVisible(false);
errorContext.setError(undefined);
} catch (e) {
errorContext.setError(e);
setLoading(false);
}
};
return (
<StyledToolbar>
Games
<IconButton
style={{ float: "right" }}
onClick={() => setAddVisible(true)}
>
<Plus size={12} />
</IconButton>
<br />
{addVisible && (
<div className="form">
<form onSubmit={() => addGame()}>
<label htmlFor="name">Board Name</label>
<input
disabled={loading}
id="name"
name="name"
placeholder="My Board 1"
value={name}
onChange={e => setName(e.target.value)}
/>
<div style={{ float: "right" }}>
<IconButton type="submit" disabled={loading}>
<ArrowRight size={15} />
</IconButton>
<IconButton
disabled={loading}
onClick={() => {
setAddVisible(false);
setName("");
}}
>
<X size={15} />
</IconButton>
</div>
</form>
</div>
)}
</StyledToolbar>
);
};
This is game Service which is handling the API calls
class Game {
constructor() {
this.instance = axios.create({
baseURL: API_URL,
timeout: TIMEOUT
});
}
getAll() {
return new Promise(async (resolve, reject) => {
try {
const game = await this.instance.get('/games');
resolve(game.data);
} catch (e) {
reject(e.message);
}
});
}
getById(id) {
return new Promise(async (resolve, reject) => {
try {
const game = await this.instance.get(`/games/${id}`);
resolve(game.data);
} catch (e) {
reject(e.message);
}
});
}
update(id, body) {
return new Promise(async (resolve, reject) => {
try {
const game = await this.instance.put(`/games/${id}`, body);
resolve(game.data);
} catch (e) {
reject(e.message);
}
});
}
add(body) {
return new Promise(async (resolve, reject) => {
try {
const game = await this.instance.post('/games', body);
resolve(game.data);
} catch (e) {
reject(e.message);
}
});
}
delete(id) {
return new Promise(async (resolve, reject) => {
try {
const game = await this.instance.delete(`/games/${id}`);
resolve(game.data);
} catch (e) {
reject(e.message);
}
});
}
}
and this is so far test I've written and I am not able to figure out how I can test "Add" method of this class. I am trying to simulate the click and trying to mock/spy on add method.
describe("Toolbar Add functionality", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<Toolbar />);
wrapper.find("IconButton").simulate("click");
});
it("Should make an API call when clicked add button", () => {
jest.mock("../services/game");
const gameService = new Game();
console.log(gameService);
const getSpy = jest.spyOn(gameService, "add");
const inputField = wrapper.find("form").find("input");
inputField.simulate("change", { target: { value: "name" } });
wrapper
.find("form")
.find("IconButton[type='submit']")
.simulate("click");
expect(getSpy).toBeCalled();
});
});
I need help in finding out how I can mock service in a similar scenario because I have the same issue in multiple containers where I am fetching data and updating the calls.

You need to wait for the promise to resolve setting your test as async and using await / act, this is the method I use but you can find other options by googling jest asynchronous testing
import { act } from 'react-dom/test-utils';
...
describe("Toolbar Add functionality", async () => {
let wrapper;
let gameService;
beforeEach(() => {
// I'm not sure about this arragement for gameService creation though
jest.mock("../services/game");
const gameService = new Game();
wrapper = mount(<Toolbar gameService={gameService} />);
wrapper.find("IconButton").simulate("click");
});
it("Should make an API call when clicked add button", () => {
console.log(gameService);
const getSpy = jest.spyOn(gameService, "add");
const inputField = wrapper.find("form").find("input");
inputField.simulate("change", { target: { value: "name" } });
wrapper
.find("form")
.find("IconButton[type='submit']")
.simulate("click");
await act(async () => await Promise.resolve());
expect(getSpy).toBeCalled();
});
});
UPDATE
Because your Game service is a class and you are creating new objects to use it, it will be necessary to pass it as a prop so you will be able to spy on the actual object the component is using
const gameService = new Game();
const Toolbar = ({gameService}) => {
const [name, setName] = useState("");
...
}

Related

Database not updating - React.js, Moralis

I'm having issues updating my database when saving edit. I am trying to retrieve Github data via API and with that data, add it to the Moralis database. I added another button outside of the form because I presume the saveEdit function will have to run after the form has been submitted. Code is as follows:
export const Dashboard = () => {
const [userSearch, setUserSearch] = useState<string>("");
const [foundUser, setFoundUser] = useState<IGitHubUser>();
const performSearchRequest = async () => {
try {
const response = await axios.get<IGitHubUser>(
`https://api.github.com/users/${userSearch}`
);
setFoundUser(response.data);
} catch (error) {
console.log(error);
}
};
const searchForUser = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
performSearchRequest();
};
const { Moralis, isInitialized } = useMoralis();
const user = isInitialized ? Moralis.User.current() : undefined;
const saveEdits = async () => {
const User = Moralis.Object.extend("_User");
const query = new Moralis.Query(User);
const myDetails = await query.first();
if (foundUser) {
myDetails?.set("github", foundUser.name);
console.log("details saved");
}
try {
await myDetails?.save();
} catch (err) {
console.log(err);
}
window.location.reload();
};
return (
<>
<h2>Search for a user</h2>
<form className="search-user" onSubmit={searchForUser}>
<input
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
placeholder="Enter a username..."
/>
<button>Search</button>
</form>
<button onClick={saveEdits}>Search</button>
</>
);
};

React - useEffect being called too many times while using it for "scroll to bottom" of messages list

I'm trying to implement "scroll to bottom" function. Here is my code:
const messagesEndRef = useRef();
const scrollToBottom = () => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
console.log("scroll");
}
};
And I use "useEffect" to trigger it:
useEffect(() => {
scrollToBottom();
}, [messages]);
Here is the place where I implement the view:
<div className="scroll">
<ListMessages listMessages={messages} />
<div ref={messagesEndRef} />
</div>
And here is the result on browser:
You guys can see that the word "scroll" is printed duplicated (or more) after every message. It makes the app very slow
Could you guys have any idea to help me to solve this case?
Thank you in advance!
PS: Here is my full component
const ws = new WebSocket("ws://localhost:8080/chat");
const Chat = () => {
const createWebSocket = () => {
ws.addEventListener("open", () => {
console.log("We are connected!");
});
ws.addEventListener("message", (e) => {
receiveMessage(e.data);
});
};
createWebSocket();
const [messages, setMessages] = useState([]);
const messagesEndRef = useRef();
const pushMessageToList = async (message, sentBy) => {
let inputMessage = {
id: Date.now(),
message: message,
sentBy: sentBy,
};
let listMessages = [...messages];
listMessages.push(inputMessage);
setMessages(listMessages);
};
const receiveMessage = (message) => {
pushMessageToList(message, "chatbot");
};
const sendInputMessage = (message) => {
pushMessageToList(message, "user");
ws.send(message);
};
const scrollToBottom = () => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
console.log("scroll");
}
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
// component did mount
}, []);
return (
<div className="bg-white">
<div className="scroll">
<ListMessages listMessages={messages} />
<div ref={messagesEndRef} />
</div>
<Input sendInputMessage={sendInputMessage} />
</div>
);
};
export default Chat;
Your code is adding new listeners to the websocket on every render, that's why you are getting incremental logs.
You have to setup handlers inside a useEffect hook, and remove them in the cleanup function of the hook itself (see docs), like:
useEffect(() => {
function onOpen() {
console.log("We are connected!");
}
function onMessage({ data }) {
setMessages([
...messages,
{
id: Date.now(),
message: data.message,
sentBy: 'chatbot'
}
]);
}
ws.addEventListener("open", onOpen);
ws.addEventListener("message",onMessage);
return () => {
ws.removeEventListener("open", onOpen);
ws.removeEventListener("message", onMessage);
}
}, [messages]);
(This hook needs the messages dependency, because you want to update the messages based on the previous state - I think you can do with the callback as well, without any dependency:
setMessages(prevMessages => ([
...prevMessages,
{
id: Date.now(),
message: data.message,
sentBy: 'chatbot'
}
]));
Now you can use the scroll hook in the same hook if you are listing messages as dependency, or in separate one like you have now in the other case.
Your full component will look something like:
const ws = new WebSocket("ws://localhost:8080/chat");
const Chat = () => {
const [messages, setMessages] = useState([]);
const messagesEndRef = useRef();
useEffect(() => {
function onOpen() {
console.log("We are connected!");
}
function onMessage({ data }) {
setMessages((prevMessages) => [
...prevMessages,
{
id: Date.now(),
message: data.message,
sentBy: "chatbot",
},
]);
}
ws.addEventListener("open", onOpen);
ws.addEventListener("message",onMessage);
return () => {
ws.removeEventListener("open", onOpen);
ws.removeEventListener("message", onMessage);
}
}, []);
useEffect(() => {
messagesEndRef?.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendInputMessage = (message) => {
ws.send(message);
setMessages([
...messages,
{
id: Date.now(),
message: data.message,
sentBy: 'user'
}
]);
};
return (
<div className="bg-white">
<div className="scroll">
<ListMessages listMessages={messages} />
<div ref={messagesEndRef} />
</div>
<Input sendInputMessage={sendInputMessage} />
</div>
);
};
export default Chat;

Trigger react useEffect

I'm fetching data from a firebase db it works when the component renders, but I can't make it to fetch again when there is a new entry in my db.
What I've tried
I've tried passing a state to the dependency array of useEffect and I changed that state every time my form was submitted (That's the time when there's a new entry in my db)
App
function App() {
const [showForm, setShowForm] = useState(true);
const [tasks, setTasks] = useState([]);
const [isSubmitted, setIsSubmitted] = useState(true);
//Fetch tasks from server
const fetchData = () => {
fetch(
"https://react-task-tracker-8e519-default-rtdb.firebaseio.com/tasks.json"
)
.then((response) => {
return response.json();
})
.then((data) => {
const tasks = [];
//Convert the data to an array so i can map over it
for (const key in data) {
const task = {
id: key,
...data[key],
};
tasks.push(task);
}
setTasks(tasks);
});
};
useEffect(() => {
fetchData();
}, [isSubmitted]);
//Show/Hide form
const onAddHandler = () => {
setShowForm(!showForm);
};
const formSubmitted = () => {
setIsSubmitted(!isSubmitted);
console.log(isSubmitted);
};
return (
<Container>
<Header click={onAddHandler} isShown={showForm}></Header>
{showForm ? <Form fs={formSubmitted}></Form> : ""}
<Tasks tasks={tasks}></Tasks>
</Container>
);
}
export default App;
Form
function Form(props) {
const [task, setTask] = useState();
const [dayTime, setDayTime] = useState();
const [reminder, setReminder] = useState();
//Posting Form data to firebase (DUMMY API)
const postFormData = (fullTask) => {
fetch(
"https://react-task-tracker-8e519-default-rtdb.firebaseio.com/tasks.json",
{
method: "POST",
body: JSON.stringify(fullTask),
headers: {
"Content-Type": "application/json",
},
}
);
};
//Make an object of form data
const onSubmit = (e) => {
e.preventDefault();
const fullTask = {
task: task,
dayTime: dayTime,
reminder: reminder,
};
//Post func call
postFormData(fullTask);
props.fs();
//Field clearing
setTask("");
setDayTime("");
setReminder("");
};
return (
<AddForm onSubmit={onSubmit}>
<FormControl>
<Label>Task</Label>
<Input
type="text"
placeholder="Add Task"
onChange={(e) => setTask(e.target.value)}
value={task}
required
></Input>
</FormControl>
<FormControl>
<Label>Day & Time</Label>
<Input
type="text"
placeholder="Add Task"
onChange={(e) => setDayTime(e.target.value)}
value={dayTime}
required
></Input>
</FormControl>
<FromControlCheck>
<CheckLabel>Set Reminder</CheckLabel>
<CheckInput
type="checkbox"
onChange={(e) => setReminder(e.currentTarget.checked)}
value={reminder}
></CheckInput>
</FromControlCheck>
<Submit type="submit" value="Save Task"></Submit>
</AddForm>
);
}
export default Form;
I would pass fetchData as a props to <Form>. When submitted, I would call it.
Form
const onSubmit = async (e) => {
e.preventDefault();
const fullTask = {
task: task,
dayTime: dayTime,
reminder: reminder,
};
//Post func call
await postFormData(fullTask);
await props.fetchData();
//Field clearing
setTask("");
setDayTime("");
setReminder("");
};
Then remove the isSubmitted state.
Try change the "Id" value to "id". Try make it the same name as the key for the id in "fecthData" function.
I think this solve your problem
function App() {
const [showForm, setShowForm] = useState(true);
const [tasks, setTasks] = useState([]);
const [isSubmitted, setIsSubmitted] = useState(false);
//Fetch tasks from server
const fetchData = () => {
fetch(
"https://react-task-tracker-8e519-default-rtdb.firebaseio.com/tasks.json"
)
.then((response) => {
return response.json();
})
.then((data) => {
const tasks = [];
//Convert the data to an array so i can map over it
for (const key in data) {
const task = {
id: key,
...data[key],
};
tasks.push(task);
}
setTasks(tasks);
});
};
useEffect(() => {
if (isSubmitted) {
fetchData();
setIsSubmitted(false);
}
}, [isSubmitted]);
//Show/Hide form
const onAddHandler = () => {
setShowForm(!showForm);
};
const formSubmitted = () => {
setIsSubmitted(true);
console.log(isSubmitted);
};
return (
<Container>
<Header click={onAddHandler} isShown={showForm}></Header>
{showForm ? <Form fs={formSubmitted}></Form> : ""}
<Tasks tasks={tasks}></Tasks>
</Container>
);
}
export default App;

WebRTC using React: Why the remote video is not playing?

I have two services, one is sending video service and the other is receiving video service.
The server only forwards the information and forwards the information to the other party.
The service that receives the video can receive the video at some times, but most of them cannot.
I don't know where the problem occurred, the service that sends the video or the service that receives the video.
caller
import { useEffect, useRef, useState, useCallback } from "react";
import io from "socket.io-client";
function createPeerConnection() {
const myPeerConnection = new RTCPeerConnection();
return myPeerConnection;
}
async function setLocalStream(myPeerConnection, target) {
console.log("setLocalStream");
const webcamStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});
target.srcObject = webcamStream;
webcamStream
.getTracks()
.forEach((track) =>
myPeerConnection.addTransceiver(track, { streams: [webcamStream] })
);
}
const App = () => {
const ref = useRef();
const [socket] = useState(io());
useEffect(() => {
socket.emit("init", "hello");
socket.on("init", (res) => {
console.log(res);
});
}, []);
const handleNegotiationNeededEvent = useCallback(
(myPeerConnection) => async () => {
try {
const offer = await myPeerConnection.createOffer();
await myPeerConnection.setLocalDescription(offer);
console.log(
"send video-offer",
myPeerConnection.localDescription
);
socket.emit("video-offer", myPeerConnection.localDescription);
} catch (error) {
console.error(error);
}
},
[socket]
);
const handleICECandidateEvent = useCallback(
(event) => {
if (event.candidate) {
console.log("send new-ice-candidate", event.candidate);
socket.emit("new-ice-candidate", event.candidate);
}
},
[socket]
);
useEffect(() => {
const myPeerConnection = createPeerConnection();
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent(
myPeerConnection
);
setLocalStream(myPeerConnection, ref.current);
socket.on("video-answer", async (sdp) => {
console.log("received video-answer", sdp);
const desc = new RTCSessionDescription(sdp);
try {
await myPeerConnection.setRemoteDescription(desc);
} catch (error) {
console.error(error);
}
});
socket.on("new-ice-candidate", async (c) => {
console.log("received new-ice-candidate", c);
const candidate = new RTCIceCandidate(c);
try {
await myPeerConnection.addIceCandidate(candidate);
} catch (error) {
console.error(error);
}
});
}, []);
return (
<>
<video style={{ width: "10px" }} ref={ref} autoPlay muted />
</>
);
};
export default App;
callee
import { useEffect, useRef, useState, useCallback } from "react";
import io from "socket.io-client";
function createPeerConnection() {
const myPeerConnection = new RTCPeerConnection();
return myPeerConnection;
}
const App = () => {
const ref = useRef();
const [socket] = useState(io());
useEffect(() => {
socket.emit("init", "hello");
socket.on("init", (res) => {
console.log(res);
});
}, []);
const handleICECandidateEvent = useCallback(
(event) => {
if (event.candidate) {
console.log("send new-ice-candidate", event.candidate);
socket.emit("new-ice-candidate", event.candidate);
}
},
[socket]
);
const handleTrackEvent = useCallback((event) => {
ref.current.srcObject = event.streams[0];
}, []);
useEffect(() => {
const myPeerConnection = createPeerConnection();
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.ontrack = handleTrackEvent;
socket.on("video-offer", async (sdp) => {
console.log("received offer", sdp);
const desc = new RTCSessionDescription(sdp);
await myPeerConnection.setRemoteDescription(desc);
await myPeerConnection.setLocalDescription(
await myPeerConnection.createAnswer()
);
console.log("send answer", myPeerConnection.localDescription);
socket.emit("video-answer", myPeerConnection.localDescription);
});
socket.on("new-ice-candidate", async (c) => {
console.log("received new-ice-candidate", c);
const candidate = new RTCIceCandidate(c);
try {
await myPeerConnection.addIceCandidate(candidate);
} catch (error) {
console.error(error);
}
});
}, []);
return (
<>
<video style={{ width: "20px" }} ref={ref} autoPlay />
</>
);
};
export default App;

Hook is not called more than 1 time

I am building a Simple ToDoList App.
I fetch List Using React Hook.
When I add a new Todo or delete an existing one the request sends and works but the component doesn`t rerender.
I tried 2 ways to solve the problem to create
1.async functions(delete and add). Took getToDoList outside the hook and call it after requests(post/delete)
useEffect(() => {
getToDoList();
}, []);
const getToDoList = async () => {
const result = await axios.get('http://localhost:1200/');
setToDoList(result.data);
};
const addNewTodo = async () => {
await axios.post('http://localhost:1200/create', {
item: newToDo.current.value
});
getToDoList();
};
const deleteToDo = async (id) => {
await axios.delete(`http://localhost:1200/delete?id=${id}`);
getToDoList();
};
2.Took getToDoList inside the hook and gave it 2 dependecies which change in deleteToDo/addNewToDo
const [add, setAdd] = useState(false);
const [remove, setRemove] = useState(false);
useEffect(() => {
const getToDoList = async () => {
const result = await axios.get('http://localhost:1200/');
setToDoList(result.data);
};
getToDoList();
}, [add, remove]);
const addNewTodo = async () => {
await axios.post('http://localhost:1200/create', {
item: newToDo.current.value
});
setAdd(!add);
};
const deleteToDo = async (id) => {
await axios.delete(`http://localhost:1200/delete?id=${id}`);
setRemove(!remove);
};
Both don`t work. Tell me please where Im wrong
JSX
return (
<div>
<Jumbotron>
<Container>
<h1>Hello!</h1>
<p>This is a simple ToDoList created by Vadik</p>
</Container>
</Jumbotron>
<Container>
<Container>
<InputGroup>
<FormControl placeholder="What needs to be done" ref={newToDo}/>
</InputGroup>
<Button onClick={addNewTodo} variant="primary" size="sm">
Add new Todo
</Button>
</Container>
<Container className="cards">
<Card>
<ListGroup>
{toDoList.todos.map((elem) => <ListGroup.Item
key={elem._id}>{elem.item}<CloseButton onClick={() => deleteToDo(elem._id)}/> </ListGroup.Item>)}
</ListGroup>
</Card>
</Container>
</Container>
</div>
);
Try the below code.This should work.In addTodo there are 2 ways either update the resultList after post is successful or use the getList to get the latest data and update the resultList. Both of them are shown.
function Example() {
const [resultList, setResult] = useState({todos: []});
useEffect(() => {
const todoList = getToDoList();
setResult(prev => todoList);
}, []);
const addTodo = async () => {
await axios.post('http://localhost:1200/create', {
item: newToDo.current.value
});
//Addition successful, hence update our resultList.
return setResult(prev => {
return {todos: [...prev.todos, {item: newToDo.current.value, id: prev.todos.length+1}]}
});
//Now since add api doesn't return a
//value call Get api again and update the todoList to render
//const todoList = getToDoList();
//setResult(prev => todoList);
};
const getToDoList = async () => {
const result = await axios.get('http://localhost:1200/');
return result.data;
};
return (
<div id='container'>
{resultList.todos.map(todo => <div>{todo.item}</div>
)}
<button onClick={addTodo}>Add</button>
</div>
)
}
The problem was that I didn`t send any response to those requests.enter image description here

Resources