React/Jest function component testing - reactjs

How can I write a test case for my function component which is having API call to return data on load? For example consider below code:
//List of tasKs and process tasK information
const TaskList = props => {
/**Function to load data on component initialization */
useEffect(() => {
getData(true);
}, []);
/**
* Function to pull tasks from database
* #param {filters} applyfilter
*/
const getData = async applyfilter => {
try {
let taskData = await service.getTasks()
taskData.tasks = taskData.tasks.sort(GFG_sortFunction)
let status_data = taskData.tasks.filter(values => {
if (statuses.includes(values.status_id)) {
return true
}
})
setData(status_data);
setT_Data(taskData.tasks);
setshowGenerateDPF(true);
builtFilter(taskData.tasks);
if ((selectedPlatforms.length !== 0 || selectedAssets.length !== 0 || selectedCategories.length !== 0 || selectedStatus.length !== 0) && applyfilter)
FilterTasksData(selectedPlatforms, selectedAssets, selectedCategories, selectedStatus, taskData.tasks);
}
catch (err) {
throw err;
}
};
return (
<>
<Row>
<div data-testid="tasklist" style={{ float: "left", padding: "0" }} className={[classes.taskList, "col-md-8"].join(' ')}>
<span style={{ float: "left" }}>
{tasks.length} Tasks </span>
</div>
</Row>
</>
}
I want to write unit test case for getData method. How can I do that?

You would want to mock the response and the other functions (like setData for example)
Here is a random example of mocking a fetch request with "react testing library" that renders div with test id that outputs text data from request
import YourComponent from '../'
import React from 'react';
import '#testing-library/jest-dom/extend-expect';
import { render, waitForElement } from '#testing-library/react';
describe('59892259', () => {
let originFetch;
beforeEach(() => {
originFetch = (global as any).fetch;
});
afterEach(() => {
(global as any).fetch = originFetch;
});
it('should pass', async () => {
const fakeResponse = { data: 'example data' };
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any);
(global as any).fetch = mockedFetch;
const { getByTestId } = render(<YourComponent />);
const div = await waitForElement(() => getByTestId('test'));
expect(div).toHaveTextContent('example data');
expect(mockedFetch).toBeCalledTimes(1);
expect(mRes.json).toBeCalledTimes(1);
});
});
you can write up a test that will expect to see "{tasks.length} Tasks" span's text as anything you mock, just make sure to mock or import the global store(redux/mobx) or hooks in your testing env so you will be able to mock the method calls as well

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

ReactJs : useRef current getting null

I am trying to turnoff camera and flashlight when the component gets unmount , I am using react hook I have two function startCamera() and stopCamera() , I am calling startcamera when the component gets mount and stop camera when component gets unmount.
But its showing me error when stopCamera is called while unmounting
i have created an another button to test if StopCamera() working and i found its working , but i want to call the function when component is getting unmounted
my code:
CameraScreen.js
import "./styles.css";
import { useState, useEffect, useRef } from "react";
export default function CameraScreen() {
const videoElement = useRef(null);
const [facingMode, setFacingMode] = useState("environment");
const handleFacingModeToggle = () => {
stopCamera();
facingMode === "environment"
? setFacingMode("user")
: setFacingMode("environment");
};
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
const stopCamera = () =>
videoElement.current.srcObject &&
videoElement.current.srcObject.getTracks().forEach((t) => t.stop());
function startCamera() {
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
video: { facingMode: facingMode },
width: { ideal: 1280 },
height: { ideal: 720 }
})
.then(function (stream) {
if (videoElement.current) videoElement.current.srcObject = stream;
const track = stream.getVideoTracks()[0];
//Create image capture object and get camera capabilities
const imageCapture = new ImageCapture(track);
const photoCapabilities = imageCapture
.getPhotoCapabilities()
.then(() => {
//todo: check if camera has a torch
//let there be light!
track.applyConstraints({
advanced: [{ torch: true }]
});
});
})
.catch(function (error) {
alert("Please check your device permissions");
console.log("Something went wrong!");
console.log(error);
});
if (videoElement.current)
videoElement.current.onloadeddata = function () {
if (window.NativeDevice)
window.NativeDevice.htmlCameraReadyToRecord(true);
};
}
}
return (
<>
<video
autoPlay={true}
ref={videoElement}
style={{
minHeight: "67.82vh",
maxHeight: "67.82vh",
maxWidth: "100%",
minWidth: "100%"
}}
className="border-3rem bg-[#666]"
></video>
<button onClick={stopCamera}> stopCamera</button>
</>
);
}
App.js
import "./styles.css";
import { useState } from "react";
import CameraScreen from "./cameraScreen";
export default function App() {
const [switchS, setSwitchS] = useState(false);
return (
<div>
<button className="" onClick={() => setSwitchS(!switchS)} value="switch">
switch
</button>
{switchS && <CameraScreen />}
{!switchS && "Blank Screen"}
</div>
);
}
PS: the above code working at :https://5t2to.csb.app/
codesandbox link : https://codesandbox.io/s/practical-fast-5t2to?file=/src/cameraScreen.js
You can use useLayoutEffect hook. It works just before unmounting, like componentWillUnmount.
Here is an example to that
https://codesandbox.io/s/happy-swartz-ikqdn?file=/src/random.js
You can go to https://ikqdn.csb.app/rand in sandbox browser and check the console on clicking to home button.
You can see the difference in working while unmounting of both useEffect and useLayoutEffect
It preserves ref.current, so what you can do is, you can pass ref.current in the function that you are calling just before unmounting, to prevent ref to the dom elment.
It took a bit of debugging/sleuthing to find the issue. So even though you have a ref attached to the video element, when the component is unmounted the ref is still mutated and becomes undefined. The solution is to save a reference to the current videoElement ref value and use this in a cleanup function.
useEffect(() => {
startCamera();
const ref = videoElement.current;
return () => {
ref.srcObject.getTracks().forEach((t) => t.stop());
};
}, []);
Simply add useLayoutEffect to stop camera
useEffect(() => {
// const getUserMedia = async () => {
// try {
// const stream = await navigator.mediaDevices.getUserMedia({
// video: true
// });
// videoElement.current.srcObject = stream;
// } catch (err) {
// console.log(err);
// }
// };
// getUserMedia();
startCamera();
}, []);
useLayoutEffect(()=>()=>{
stopCamera();
},[]);
Just need to change useEffect() to useLayoutEffect()and it will works like a charm.
useLayoutEffect(() => {
const ref = videoElement;
console.log(ref);
startCamera();
return function cleanup() {
stopCamera();
};
}, []);
sanbox link :- https://codesandbox.io/s/naughty-murdock-by5tc?file=/src/cameraScreen.js:415-731

useState wouldn't work asynchronously (React)

Building a recording app on React + Type Script. I tried to set state with getting stream, and it seems to be successfully gotten on console. But it couldn't be set up on record target stream.
const [recordTargetStream, setRecordTargetStream] = useState<MediaStream>()
// click request permissions
const requestPermissions = useCallback(async() => {
const stream = await window.navigator.mediaDevices.getUserMedia({audio: true, video: true})
// stream is successfully gotten
setRecordTargetStream(stream)
}, [])
const startRecording = useCallback(() => {
console.log('start recording', recordTargetStream)
// record target stream is undefined
...
return (
<>
<button onClick={() => requestPermissions()}>Request permissions</button>
<button onClick={() => startRecording()}>Start recording</button>
</>
)
You should use the recordTargetStream state as useCallback hook's dependencies.
useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.
every value referenced inside the callback should also appear in the dependencies array.
index.tsx:
import React, { useCallback, useState } from 'react'
export default function MyComponent() {
const [recordTargetStream, setRecordTargetStream] = useState<MediaStream>()
const requestPermissions = useCallback(async() => {
const stream = await window.navigator.mediaDevices.getUserMedia({audio: true, video: true})
setRecordTargetStream(stream)
}, [])
const startRecording = useCallback(() => {
console.log('start recording', recordTargetStream)
}, [recordTargetStream])
return (
<>
<button onClick={() => requestPermissions()}>Request permissions</button>
<button onClick={() => startRecording()}>Start recording</button>
</>
)
}
index.test.tsx:
import {fireEvent, render, screen, act} from '#testing-library/react';
import React from 'react';
import MyComponent from './';
describe('69354798', () => {
test('should pass', async () => {
const mockMediaDevices = {
getUserMedia: jest.fn().mockResolvedValue('test stream')
}
Object.defineProperty(window.navigator, 'mediaDevices', {
value: mockMediaDevices
})
render(<MyComponent/>)
await act(async () => {
fireEvent.click(screen.getByText(/Request permissions/))
})
fireEvent.click(screen.getByText(/Start recording/))
})
})
test result:
PASS stackoverflow/69354798/index.test.tsx
69354798
✓ should pass (54 ms)
console.log
start recording test stream
at stackoverflow/69354798/index.tsx:12:13
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.211 s

Testing State variable using Jest

I am learning to test React app using jest and Enzyme . I have created a component and using redux to maintain and update the state . The component code is below .
Now i want to write the test to check initial value of prodOverviewAccordion which we are setting as true in context file.
I have tried writing , but getting error . Sharing the test code also . Please help
const ProdOverview = () => {
const {
productState,
setProdOverviewAccordion
} = React.useContext(ProductContext);
const {prodOverviewAccordion } = productState;
const [completeStatusprod, setCompleteStatusprod] = useState(false);
return (
<div onClick={toggleTriggerProd}>
<s-box>
<Collapsible
trigger={
<Accordion
name={ProductConfig.accordionTriggerLabels.prodOverviewLabel}
completeStatusIcon={completeStatusprod ? 'check-circle' : 'alert-triangle'}
completeStatus={completeStatusprod}
/>
}
easing='ease-out'
handleTriggerClick={() => {
if (!prodOverviewAccordion) {
setProdOverviewAccordion(true);
} else {
setProdOverviewAccordion(false);
}
}}
open={prodOverviewAccordion}
data-test='prodOverViewCollapsible'
>
<p>Test</p>
</Collapsible>
</s-box>
</div>
);
};
export default ProdOverview;
const prodsetup = (props = {}) => {
return shallow(<ProdOverview />);
};
describe('Product Overview panel Test', () => {
const mockSetCurrentGuess = jest.fn();
beforeEach(() => {
mockSetCurrentGuess.mockClear();
});
test('should render Collapsible panel', () => {
const wrapper = prodsetup();
const component = findByTestAttr(wrapper, 'prodOverViewCollapsible');
expect(component.length).toBe(1);
});
test('Product Overview Panel should be in open state', () => {
const wrapper = prodsetup();
expect(wrapper.state().prodOverviewAccordion.to.equal(true));
});
});

How to test useRef with Jest and react-testing-library?

I'm using create-react-app, Jest and react-testing-library for the configuration of the chatbot project.
I have a functional component that uses useRef hook. When a new message comes useEffect hook is triggered and cause scrolling event by looking a ref's current property.
const ChatBot = () => {
const chatBotMessagesRef = useRef(null)
const chatBotContext = useContext(ChatBotContext)
const { chat, typing } = chatBotContext
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
// eslint-disable-next-line
}, [chat, typing])
return (
<>
<ChatBotHeader />
<div className='chatbot' ref={chatBotMessagesRef}>
{chat && chat.map((message, index) => {
return <ChatBotBoard answers={message.answers} key={index} currentIndex={index + 1} />
})}
{typing &&
<ServerMessage message='' typing isLiveChat={false} />
}
</div>
</>
)
}
I want to be able to test whether is scrollTo function triggered when a new chat item or typing comes, do you have any ideas? I couldn't find a way to test useRef.
You can move your useEffect out of your component and pass a ref as a parameter to it. Something like
const useScrollTo = (chatMessagesRef, chat) => {
useEffect(() => {
if (typeof chatMessagesRef.current.scrollTo !== 'undefined' && chat && chat.length > 0) {
chatBotMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
behavior: 'smooth'
})
}
}, [chat])
}
Now in your component
import useScrollTo from '../..'; // whatever is your path
const MyComponent = () => {
const chatBotMessagesRef = useRef(null);
const { chat } = useContext(ChatBotContext);
useScrollTo(chatBotMessagesRef, chat);
// your render..
}
Your useScrollTo test:
import useScrollTo from '../..'; // whatever is your path
import { renderHook } from '#testing-library/react-hooks'
it('should scroll', () => {
const ref = {
current: {
scrollTo: jest.fn()
}
}
const chat = ['message1', 'message2']
renderHook(() => useScrollTo(ref, chat))
expect(ref.current.scrollTo).toHaveBeenCalledTimes(1)
})

Resources