Testing a ResizeObserver inside an Effect Fn - javascript

I'm struggling a bit with unit testing the following:
const { element, state } = props;
const { theme } = state;
const { computedHeight, computedWidth } = theme;
const elementToSize = document.querySelector(element);
const observer = new ResizeObserver(element => {
const { contentRect } = element[0];
// set the viewport size in the state
dispatch(setViewportSize(state, contentRect.width, contentRect.height));
// perform the task function
wrapper(600, computedWidth, computedHeight, contentRect.width, contentRect.height).
then(() => {
elementToSize.style.height = `${contentRect.height <= computedHeight ? contentRect.height : computedHeight}px`;
elementToSize.style.width = `${contentRect.width <= computedWidth ? contentRect.width : computedWidth}px`;
}).catch( e => new Error(e));
});
observer.observe(document.documentElement);
const wrapper = async (ms, ...callbackArgs) => {
try {
await asyncInterval(checkSize, ms, ...callbackArgs);
} catch {
new Error('async Interval has failed...');
}
return await asyncInterval(checkSize, ms, ...callbackArgs);
};
};
export const SizeableEffect = (element, state) => [effectFn, { element, state } ];
In the code above, I have difficulties with unit testing the code inside the ResizeObserver.
I have the following in my test.
import ResizeObserver from 'resize-observer-polyfill';
import { SizeableEffect } from 'effects';
import { domMock } from '../mocks/dom';
jest.mock('resize-observer-polyfill');
describe('SizeableEffect', () => {
let stateMock, dispatch, effect, effectFn;
beforeEach(() => {
stateMock = {
viewportWidth: null,
viewportHeight: null,
theme: {
chatFrameSize: {
width: 300,
height: 500
}
}
};
dispatch = jest.fn();
effect = SizeableEffect('elementMock', stateMock);
effectFn = effect[0];
Object.defineProperties(window, {
document: {
value: domMock()
}
});
});
it('the effect should be called with the element', () => {
expect(effect[1]).toEqual(
expect.objectContaining({
element: 'elementMock',
state: stateMock
})
);
});
it('the effect function should perform the operations', () => {
effectFn(dispatch, { element: 'some', state: stateMock });
expect(document.querySelector).toHaveBeenCalledWith('some');
expect(dispatch).not.toHaveBeenCalled();
expect(ResizeObserver).toHaveBeenCalled();
});
});
And as you can see from the screenshot, I have uncovered lines where the ResizeObserver is doing the login by measuring the width' and height's against the viewport.
How can I cover those lines in the best way?

Related

Trying to figure out how to use socket.io the correct way in a useEffect that is using an axios get request to fetch messages

So far i'm stuck on my useEffect that fetches all the current messages and renders the state accordingly. as of right now it doesn't render the new state until page is refreshed.
const Home = ({ user, logout }) => {
const history = useHistory();
const socket = useContext(SocketContext);
const [conversations, setConversations] = useState([]);
const [activeConversation, setActiveConversation] = useState(null);
const classes = useStyles();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const addSearchedUsers = (users) => {
const currentUsers = {};
// make table of current users so we can lookup faster
conversations.forEach((convo) => {
currentUsers[convo.otherUser.id] = true;
});
const newState = [...conversations];
users.forEach((user) => {
// only create a fake convo if we don't already have a convo with this user
if (!currentUsers[user.id]) {
let fakeConvo = { otherUser: user, messages: [] };
newState.push(fakeConvo);
}
});
setConversations(newState);
};
const clearSearchedUsers = () => {
setConversations((prev) => prev.filter((convo) => convo.id));
};
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
const sendMessage = (data, body) => {
socket.emit("new-message", {
message: data.message,
recipientId: body.recipientId,
sender: data.sender,
});
};
const postMessage = async (body) => {
try {
const data = await saveMessage(body);
if (!body.conversationId) {
addNewConvo(body.recipientId, data.message);
} else {
addMessageToConversation(data);
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
const addNewConvo = useCallback(
(recipientId, message) => {
conversations.forEach((convo) => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message);
convo.latestMessageText = message.text;
convo.id = message.conversationId;
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
const addMessageToConversation = useCallback(
(data) => {
// if sender isn't null, that means the message needs to be put in a brand new convo
const { message, sender = null } = data;
if (sender !== null) {
const newConvo = {
id: message.conversationId,
otherUser: sender,
messages: [message],
};
newConvo.latestMessageText = message.text;
setConversations((prev) => [newConvo, ...prev]);
}
conversations.forEach((convo) => {
console.log('hi', message.conversationId)
if (convo.id === message.conversationId) {
const convoCopy = { ...convo }
convoCopy.messages.push(message);
convoCopy.latestMessageText = message.text;
console.log('convo', convoCopy)
} else {
return convo
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
const setActiveChat = useCallback((username) => {
setActiveConversation(username);
}, []);
const addOnlineUser = useCallback((id) => {
setConversations((prev) =>
prev.map((convo) => {
if (convo.otherUser.id === id) {
const convoCopy = { ...convo };
convoCopy.otherUser = { ...convoCopy.otherUser, online: true };
return convoCopy;
} else {
return convo;
}
}),
);
}, []);
const removeOfflineUser = useCallback((id) => {
setConversations((prev) =>
prev.map((convo) => {
if (convo.otherUser.id === id) {
const convoCopy = { ...convo };
convoCopy.otherUser = { ...convoCopy.otherUser, online: false };
return convoCopy;
} else {
return convo;
}
}),
);
}, []);
// Lifecycle
useEffect(() => {
// Socket init
socket.on("add-online-user", addOnlineUser);
socket.on("remove-offline-user", removeOfflineUser);
socket.on("new-message", addMessageToConversation);
return () => {
// before the component is destroyed
// unbind all event handlers used in this component
socket.off("add-online-user", addOnlineUser);
socket.off("remove-offline-user", removeOfflineUser);
socket.off("new-message", addMessageToConversation);
};
}, [addMessageToConversation, addOnlineUser, removeOfflineUser, socket]);
useEffect(() => {
// when fetching, prevent redirect
if (user?.isFetching) return;
if (user && user.id) {
setIsLoggedIn(true);
} else {
// If we were previously logged in, redirect to login instead of register
if (isLoggedIn) history.push("/login");
else history.push("/register");
}
}, [user, history, isLoggedIn]);
useEffect(() => {
const fetchConversations = async () => {
try {
const { data } = await axios.get("/api/conversations");
setConversations(data);
} catch (error) {
console.error(error);
}
};
if (!user.isFetching) {
fetchConversations();
}
}, [user]);
const handleLogout = async () => {
if (user && user.id) {
await logout(user.id);
}
};
return (
<>
<Button onClick={handleLogout}>Logout</Button>
<Grid container component="main" className={classes.root}>
<CssBaseline />
<SidebarContainer
conversations={conversations}
user={user}
clearSearchedUsers={clearSearchedUsers}
addSearchedUsers={addSearchedUsers}
setActiveChat={setActiveChat}
/>
<ActiveChat
activeConversation={activeConversation}
conversations={conversations}
user={user}
postMessage={postMessage}
/>
</Grid>
</>
);
};
this is the main part im working on, the project had starter code when i began and was told not to touch the backend so i know its something wrong with the front end code. i feel like im missing something important for the socket.io
import { io } from 'socket.io-client';
import React from 'react';
export const socket = io(window.location.origin);
socket.on('connect', () => {
console.log('connected to server');
});
export const SocketContext = React.createContext();
this is how i have the socket.io setup, if anyone could point me in the right direction that would be cool. I have been reading up on socket.io as much as I can but am still pretty lost on it.
Based on the assumption the backend is working properly...
const addNewConvo = useCallback(
(recipientId, message) => {
conversations.forEach((convo) => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message);
convo.latestMessageText = message.text;
convo.id = message.conversationId;
}
});
setConversations(conversations);
},
[setConversations, conversations],
);
setConversations(conversations);
This is an incorrect way to set a state using the state's variable, and such it wont do anything. Likely why your code wont change until refresh.
Suggested fix:
const addNewConvo = useCallback(
(recipientId, message) => {
setConversations(previousState => previousState.map(convo => {
if (convo.otherUser.id === recipientId) {
convo.messages.push(message)
convo.latestMessageText = message.text;
convo.id = message.conversationId;
return convo
}
return convo
}))
},
[setConversations, conversations],
);
note: even above could be done more efficiently since I made a deep copy of messages

VueJS/VueX Maximum call stack size exceeded when processing Websocket (Best Practice advice)

Just trying to find the right approach, "good practice" on how to manage a websocket connection with Vue and Vuex. This was my first original approach for the websocket module but as you can see I'm really close to deal with async data inside the SET_REMOTE_DATA mutation, which I recogn is an antipattern.
Here's the first module approach, as you can see the mutation is dealing with two state properties and at the same time with the resolve data coming from the WebSocket promise coming from an action:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChats: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://xyz')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
const chatSession = wsData.chat
const wsDataType = wsData.type
if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
state.connectionId = wsData.connectionId
} else {
if (wsDataType==="incoming-chats-updated") {
state.incomingChats = wsData.incomingChats
} else {
console.log(JSON.stringify(chatSession))
}
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
export const getters = {
getIncomingChats: (state) => {
return state.incomingChats
},
}
And here's my second approach, a more modular one where all the stress now lives on the processWebsocket action. Instead of having one mutation setting the "connectionId" and the "incomingChats" state now I'm trying to create two mutations, one for each property. The problem with this approach is that I'm not sure where to resolve the websocket promise and I'm getting an "Error in Login Maximum call stack size exceeded" JS error in the console.
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChats: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://xyz')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
const wsData = JSON.parse(event.data)
const chatSession = wsData.chat
const wsDataType = wsData.type
const incomingChats = wsData.incomingChats
const connectionId = wsData.connectionId
if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
commit('SET_CONNECTION_ID', connectionId)
} else{
if (wsDataType==="incoming-chats-updated") {
commit('SET_INCOMING_CHATS', incomingChats)
} else {
console.log(JSON.stringify(chatSession))
}
}
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
export const mutations = {
SET_CONNECTION_ID(remoteConnectionId) {
state.connectionId = remoteConnectionId
},
SET_INCOMING_CHATS(state, incomingChats) {
state.incomingChats = incomingChats
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
export const getters = {
getIncomingChats: (state) => {
return state.incomingChats
},
}
Any advice will be really appreciate.

why componentdidmount called two times

I have React Component in componentDidMount fetch data from the server. The issue is componentDidMount called twice also the API called twice. I have a view increment API like youtube video views increment twice in the database because of twice API calling.
class SingleVideoPlay extends React.Component {
constructor(props) {
super(props);
this.player = React.createRef();
}
state = {
autoPlay: true,
relatedVideos: [],
video: null,
user: null,
comments: [],
commentInput: {
value: '',
touch: false,
error: false
},
following: false,
tab: 'comments'
};
_Mounted = false;
componentDidMount() {
this._Mounted = true;
if (this._Mounted) {
const videoId = this.props.match.params.id;
this.getVideoDetails(videoId);
}
}
componentWillUnmount() {
this._Mounted = false;
try {
clearInterval(this.state.videoInterval);
this.props.videoEditUrl('');
} catch (error) {}
}
captureVideoTime = async () => {
const { video } = this.state;
const result = await updateWatchTime({
id: video._id,
time: 1
});
if (result.status === 200) {
const updateVideo = {
...video,
secondsWatched: video.secondsWatched + 1
};
this.setState({ video: updateVideo });
}
};
videoEnded = () => {
clearInterval(this.state.videoInterval);
};
videoPause = () => {
clearInterval(this.state.videoInterval);
};
loadVideo = () => {
clearInterval(this.state.videoInterval);
};
playingVideo = () => {
const interval = setInterval(this.captureVideoTime, 1000);
this.setState({ videoInterval: interval });
};
getVideoDetails = async (videoId) => {
const video = await getVideo(videoId);
if (video.status === 200) {
let response = video.data;
if (this.props.userId)
if (response.user._id === this.props.userId._id)
this.props.videoEditUrl(`/video/edit/${response.media._id}`);
this.setState({
relatedVideos: response.videos.docs,
video: response.media,
user: response.user
});
this.checkIsFollowing();
this.updateVideoStat(response.media._id);
}
};
updateVideoStat = async (id) => videoView(id);
checkIsFollowing = async () => {
const { userId } = this.props;
const { video } = this.state;
if (userId && video) {
const response = await isFollow({
follower: userId._id,
following: video._id
});
if (response) {
this.setState({ following: response.following });
}
}
};
addOrRemoveFollowing = async () => {
this.checkIsFollowing();
const { following, video } = this.state;
const { userId } = this.props;
if (userId) {
if (following) {
const response = await removeFollow({
follower: userId._id,
following: video._id
});
this.setState({ following: false });
} else {
const response = await addFollow({
follower: userId._id,
following: video._id
});
this.setState({ following: true });
}
}
};
submitCommentHandler = async (event) => {
const { userId } = this.props;
event.preventDefault();
if (userId) {
const result = await saveComment({
mediaId: this.state.video._id,
parentId: '0',
userID: userId._id,
userName: userId.username,
comment: this.state.commentInput.value
});
console.log(result);
if (result.status === 200) {
this.getVideoComments();
this.setState({ commentInput: { value: '', touch: false, error: false } });
}
}
};
render() {
const { autoPlay, relatedVideos, video, user, comments, commentInput, following, tab } = this.state;
const { userId } = this.props;
return (
<div className="container-fluid">
some coponents
</div>
);
}
}
const mapStateToProps = (state) => ({
userId: state.auth.user
});
export default connect(mapStateToProps, { videoEditUrl })(SingleVideoPlay);
I don't know why componentDidMount called two times alse it shows memmory lecage issue.
How to Fix it.
Multiple componentDidMount calls may be caused by using <React.StrictMode> around your component. After removing it double calls are gone.
This is intended behavior to help detect unexpected side effects. You can read more about it in the docs. It happens only in development environment, while in production componentDidMount is called only once even with <React.StrictMode>.
This was tested with React 18.1.0
I think the issue exists on the parent component that used SingleVideoPlay component. Probably that parent component caused SingleVideoPlay component rendered more than once.
Also, there is an issue on your code.
componentDidMount() {
this._Mounted = true;
if (this._Mounted) {
const videoId = this.props.match.params.id;
this.getVideoDetails(videoId);
}
}
Here, no need to check if this._Mounted, because it will always be true.
1.Install jQuery by
npm i jquery
import $ from 'jquery'
create your function or jwuery code after the export command or put at the end of the file

Rewriting flow / class component to custom hook

I am trying to rewrite the following code here which was written in flow to a custom hook. I got stuck and the hook doesn't work as it should. Do you see any mistake I made?
The error I get is TypeError: elementRefs.ref is not a function
Here is how I call it:
parent.js
import MultiRef from "./MultiRef";
const [elementRefs] = useState(() => new MultiRef());
Here is my version:
function useMultiRef() {
const map = new Map();
const _refFns = new Map();
const ref = (key) => {
let refFn = _refFns.get(key);
if (!refFn) {
refFn = (value) => {
if (value == null) {
_refFns.delete(key);
map.delete(key);
} else {
map.set(key, value);
}
};
_refFns.set(key, refFn);
}
return refFn;
};
}
export default useMultiRef;
Here is the original code:
/* #flow */
type RefFn<V> = (value: V|null) => mixed;
export default class MultiRef<K,V> {
map: Map<K,V> = new Map();
_refFns: Map<K,RefFn<V>> = new Map();
ref(key: K): RefFn<V> {
let refFn: ?RefFn<V> = this._refFns.get(key);
if (!refFn) {
refFn = value => {
if (value == null) {
this._refFns.delete(key);
this.map.delete(key);
} else {
this.map.set(key, value);
}
};
this._refFns.set(key, refFn);
}
return refFn;
}
}
Update (parent.js):
/**
* Scrolls to element if var goToElement is set
*/
useEffect(() => {
const ref = elementRefs.map.get(goToElement);
// If no ref exists, scroll to top of the floor instead
if (!ref) {
floorRef.current.scrollTop = 0;
return;
}
// Scroll to element: scrollTo(x-coord, y-coord)
floorRef.current.scrollTo(0, ref.offsetTop);
// Wait for a short time to perform scrolling
const timer = setTimeout(() => {
// currentElementRef is needed for highlighting the element
setCurrentElementRef(ref);
}, 500);
return () => clearTimeout(timer);
}, [elementRefs, goToElement]);
You need to return ref value for elementRefs.ref to work, currently, you just run a void function.
Also, that's not a custom hook you implemented a simple function.
function multiRef() {
//...
const ref = (key) => {...};
return { ref };
}
Custom hook should use hooks, to get similar properties like a class, it should look something like:
function useMultiRef() {
const map = useRef(new Map());
const _refFns = useRef(new Map());
const ref = useCallback((key) => {
let refFn = _refFns.current.get(key);
if (!refFn) {
refFn = (value) => {
if (value == null) {
_refFns.current.delete(key);
map.current.delete(key);
} else {
map.current.set(key, value);
}
};
_refFns.current.set(key, refFn);
}
return refFn.current;
}, []);
return { ref, map };
}
export default useMultiRef;

How to use mockDOMSource to test a stream of actions in Cycle.js?

I realize there is probably a better way using cycle/time, but I'm just trying to understand the basics. Somehow, my action$ stream doesn't seem to be running; I've tried to construct multiple mock doms using xs.periodic. The test framework is mocha.
import 'mocha';
import {expect} from 'chai';
import xs from 'xstream';
import Stream from 'xstream';
import {mockDOMSource, DOMSource} from '#cycle/dom';
import {HTTPSource} from '#cycle/http';
import XStreamAdapter from '#cycle/xstream-adapter';
export interface Props {
displayAbs: boolean
}
export interface ISources {
DOM: DOMSource;
http: HTTPSource;
}
function testIntent(sources: ISources):Stream<Props> {
return xs.merge<Props>(
sources.DOM
.select('.absShow').events('click')
.mapTo( { displayAbs: true } ),
sources.DOM
.select('.absHide').events('click')
.mapTo( { displayAbs: false } )
).startWith( {displayAbs: false } );
}
describe( 'Test', ()=>{
describe( 'intent()', ()=>{
it('should change on click to shows and hides', () => {
let listenerGotEnd = false;
const mDOM$: Stream<DOMSource> = xs.periodic(1000).take(6).map(ii => {
if (ii % 2 == 0) {
return mockDOMSource(XStreamAdapter, {
'.absShow': {'click': xs.of({target: {}})}
})
}
else {
return mockDOMSource(XStreamAdapter, {
'.absHide': {'click': xs.of({target: {}})}
})
}
});
const action$ = mDOM$.map(mDOM => testIntent({
DOM: mDOM,
http: {} as HTTPSource,
})).flatten();
action$.addListener({
next: (x) => {
console.log("x is " + x.displayAbs);
},
error: (err) => {
console.log("error is:" + err);
throw err;
},
complete: () => { listenerGotEnd = true; }
});
expect(listenerGotEnd).to.equal(true);
});
});/* end of describe intent */
});
The primary reason the test is not running is because it's asynchronous, so in mocha we need to take in the done callback and then call it when our test is done.
Without using #cycle/time, this is how I would write this test:
import 'mocha';
import {expect} from 'chai';
import xs, {Stream} from 'xstream';
import {mockDOMSource, DOMSource} from '#cycle/dom';
import XStreamAdapter from '#cycle/xstream-adapter';
export interface Props {
displayAbs: boolean
}
export interface ISources {
DOM: DOMSource;
}
function testIntent(sources: ISources):Stream<Props> {
return xs.merge<Props>(
sources.DOM
.select('.absShow').events('click')
.mapTo( { displayAbs: true } ),
sources.DOM
.select('.absHide').events('click')
.mapTo( { displayAbs: false } )
).startWith( {displayAbs: false } );
}
describe('Test', () => {
describe('intent()', () => {
it('should change on click to shows and hides', (done) => {
const show$ = xs.create();
const hide$ = xs.create();
const DOM = mockDOMSource(XStreamAdapter, {
'.absShow': {
'click': show$
},
'.absHide': {
'click': hide$
}
});
const intent$ = testIntent({DOM});
const expectedValues = [
{displayAbs: false},
{displayAbs: true},
{displayAbs: false},
]
intent$.take(expectedValues.length).addListener({
next: (x) => {
expect(x).to.deep.equal(expectedValues.shift());
},
error: done,
complete: done
});
show$.shamefullySendNext({});
hide$.shamefullySendNext({});
});
});
});
This test runs in 11ms, which is a fair bit faster than using xs.periodic(1000).take(6)
For comparison, here is how I would write it with #cycle/time:
import {mockTimeSource} from '#cycle/time'
describe('Test', () => {
describe('intent()', () => {
it('should change on click to shows and hides', (done) => {
const Time = mockTimeSource();
const show$ = Time.diagram('---x-----');
const hide$ = Time.diagram('------x--');
const expected$ = Time.diagram('f--t--f--', {f: false, t: true});
const DOM = mockDOMSource({
'.absShow': {
'click': show$
},
'.absHide': {
'click': hide$
}
});
const intent$ = testIntent({DOM}).map(intent => intent.displayAbs);
Time.assertEqual(intent$, expected$);
Time.run(done);
});
});
});
The first version is effectively what #cycle/time is doing for you under the hood, this is just a slightly nicer way of writing it. It's also nice to have better error messages.

Categories

Resources