I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.
Related
I'm using #googlemaps/react-wrapper to make a map component in my react application using the example from googlemaps, and adding an event on drag marker to refresh coordinates, this works fine now. but i need to call the map component outside to refresh a input value with the coordiantes.
The error i get it is:
Binding element 'childToParent' implicitly has an 'any' type.*
Please could help me to understand how i could send the values to paren using typescript
Greetings
In parent i have this
const [coordinate,SetCoordinate]=useState("");
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={3}>
<RHFTextField name="lat" label="Coord X" />
<RHFTextField name="lng" label="Coord Y" />
</Stack>
<Stack>
<br/>
<LocationMap childToParent={setCoordinate}/>
</Stack>
<Stack>
<LoadingButton
fullWidth
size="large"
type="submit"
variant="contained"
>
Save
</LoadingButton>
</Stack>
</FormProvider>
);
My Location map component is like this
const render = (status: Status) => {
return <h1>{status}</h1>;
};
interface MapProps extends google.maps.MapOptions {
style: { [key: string]: string };
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
}
//function to pass value to parent
interface LocationProps {
childToParent: (arg0: string)=>string;
}
export default function LocationMap({childToParent,...props}){
const [clicks, setClicks] = useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = useState(3); // initial zoom
const [center, setCenter] = useState<google.maps.LatLngLiteral>({
lat: 0.0,
lng: 0.0,
});
const [markerLocation, setMarkerLocation] = useState<google.maps.LatLng>();
const dragend = (e: google.maps.MapMouseEvent) => {
// avoid directly mutating state
setMarkerLocation(e.latLng!)
setClicks([...clicks, e.latLng!]);
};
const onClick = (e: google.maps.MapMouseEvent) => {
};
const onIdle = (m: google.maps.Map) => {
//.log("onIdle");
setZoom(m.getZoom()!);
setCenter(m.getCenter()!.toJSON());
};
const ref = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<google.maps.Map>();
useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
return (
<>
<div style={{ display: "flex", height: "100%" }}>
<Wrapper apiKey={apiKey} render={render}>
<Map
center={center}
onClick={onClick}
onIdle={onIdle}
zoom={zoom}
style={{ flexGrow: "1", height: "25em", width: "400px" }}
>
<Marker key="point" draggable={true} dragend={dragend} />
</Map>
</Wrapper>
</div>
<div id="coordinate">
{clicks.map(function (latLng, i, row) {
var element = document.getElementById("coordenadas");
if (element === null) {
console.error("error cleaning coordinates");
} else {
element.innerHTML = "";
}
return (
childToParent(latLng.toJSON())
);
})
}
</div>
</>
)
};
interface MapProps extends google.maps.MapOptions {
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
}
const Map: React.FC<MapProps> = ({
onClick,
onIdle,
children,
style,
...options
}) => {
const ref = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<google.maps.Map>();
useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
if (map) {
map.setOptions(options);
}
}, [map, options]);
useEffect(() => {
if (map) {
["click", "idle"].forEach((eventName) =>
google.maps.event.clearListeners(map, eventName)
);
if (onClick) {
map.addListener("click", onClick);
}
if (onIdle) {
map.addListener("idle", () => onIdle(map));
}
}
}, [map, onClick, onIdle]);
return (
<>
<div ref={ref} style={style} />
{Children.map(children, (child) => {
if (isValidElement(child)) {
// set the map prop on the child component
return cloneElement(child, { map });
}
})}
</>
);
};
interface MarkerProps extends google.maps.MarkerOptions {
dragend?: (e: google.maps.MapMouseEvent) => void;
}
const Marker: React.FC<MarkerProps> = ({
dragend,
...options
}) => {
const [marker, setMarker] = useState<google.maps.Marker>();
console.log(options);
useEffect(() => {
if (!marker) {
setMarker(new google.maps.Marker({
position: {
lat: 0,
lng: 0,
},
}));
}
// remove marker from map on unmount
return () => {
if (marker) {
marker.setMap(null);
}
};
}, [marker]);
useEffect(() => {
if (marker) {
marker.setOptions(options);
}
}, [marker, options]);
useEffect(() => {
if (marker) {
["dragend"].forEach((eventName) =>
google.maps.event.clearListeners(marker, eventName)
);
marker.setOptions(options);
if (dragend) {
//map.addListener("click", onClick);
marker.addListener("dragend", dragend);
}
}
}, [marker, dragend, options]);
return null;
};
const deepCompareEqualsForMaps = createCustomEqual(
(deepEqual) => (a: any, b: any) => {
if (
isLatLngLiteral(a) ||
a instanceof google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof google.maps.LatLng
) {
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
}
// TODO extend to other types
// use fast-equals for other objects
return deepEqual(a, b);
}
);
function useDeepCompareMemoize(value: any) {
const ref = useRef();
if (!deepCompareEqualsForMaps(value, ref.current)) {
ref.current = value;
}
return ref.current;
}
function useDeepCompareEffectForMaps(
callback: React.EffectCallback,
dependencies: any[]
) {
useEffect(callback, dependencies.map(useDeepCompareMemoize));
}
export default LocationMap;
It is warning you because you have not passed the correct function. This should fix the problem:
const [coordinate, setCoordinate] = useState("");
const deepCompareEqualsForMaps = createCustomEqual((deepEqual: any, ref: any) => (a: any, b: any) => {
if (
isLatLngLiteral(a) ||
a instanceof window.google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof window.google.maps.LatLng
) {
return new window.google.maps.LatLng(a).equals(new window.google.maps.LatLng(b));
}
// TODO extend to other types
// use fast-equals for other objects
return deepEqual(a, b);
}
);
Here's are a few snippets from my code. I have a function component defined as:
function AssociationViewer(props) {
const core = React.useRef<GraphCore | null>(null);
const dataService = React.useRef<DataService>(new DataService(props.graphApi));
const selectionBoxDimensions = React.useRef(null);
const initialGraphRender = React.useRef<boolean>(false);
const filterRef = React.useRef(null);
const getElementRef = React.useCallback((el) => {
if (el && !core.current) {
core.current = new GraphCore({ container: el });
// TODO: Change data service to accept core as an argument and initialize it here?
dataService.current.addGraphDataFn = (opts) => getRksGraph().addData(opts);
dataService.current.setGraphDataFn = (opts) => getRksGraph().setData(opts);
onMount();
return onUnmount;
}
}, []);
.
.
.
return (
<>
{props.enableSearch && <div style={{zIndex: 10000, position: 'absolute', marginTop: 10, right: 15}}>
<button onClick={flashSearchedNodes}>Search</button>
<input
value={searchText}
placeholder='Find node by text'
onKeyDown={(e) => e.key == 'Enter' && flashSearchedNodes()}
onChange={(e) => setSearchText(e.target.value)}
/>
<input readOnly style={{width: '60px', textAlign: 'center'}} type="text" value={searchedElesDisplayText} />
<button onClick={prevFoundNode}>Prev</button>
<button onClick={nextFoundNode}>Next</button>
<button onClick={cancelFlashNodes}>Clear</button>
</div>}
<div
style={{ height: '100%', width: '100%' }}
id={props.componentId || 'kms-graph-core-component'}
ref={getElementRef}
></div>
{core.current && (
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current}
/>
)}
{props.loadingMask && !hasCustomLoadMask() && (
<DefaultLoadMask
active={showLoading}
loadingClass={getLoadingClass()}
onClick={() => {
setShowLoading(false);
}}
/>
)}
{props.loadingMask && showLoading && hasCustomLoadMask() && props.customLoadingMask()}
</>
);
}
export default AssociationViewer;
I have an angular app that uses a service to call this component as follows:
ReactService.render(AssociationViewer,
{
ref: function (el) {
reactElement = el;
},
component: 'association-viewer-1',
getImageUrl: getImageUrl,
graphApi: graphApi,
pairElements: $ctrl.pairElements,
theme: theme,
view: 'testView'
}
'miniGraphContainer',
() => {
if ($ctrl.pairElements.edges) {
reactElement.core.select($ctls.pairElements.edges($ctrl.currEdge).id);
}
}
Here's GraphTooltip:
import React from 'react';
import GraphCore from '../GraphCore';
function useForceUpdate() {
const [value, setValue] = React.useState(0);
return () => setValue((value) => ++value);
}
interface IGraphTooltipProps{
tooltipHoverShowDelay: number;
tooltipHoverHideDelay: number;
core: GraphCore;
tooltip: Function;
tippyOptions: any;
}
const GraphTooltip = (props: IGraphTooltipProps) => {
const tooltipRef = React.useRef<React.Element>(null);
const forceUpdate = useForceUpdate();
const setRef = (ref) => {
tooltipRef.current = ref;
};
const [currentElement, setCurrentElement] = React.useState<React.Element>();
React.useEffect(() => {
props.core.getAllSubGraphs().forEach((subgraph) => {
subgraph.setOptions({
tooltip: {
domElementCallback: (e) => {
// this isn't changing so it's not picking up a render loop
setCurrentElement(e);
forceUpdate();
return tooltipRef.current;
},
hoverShowDelay: props.tooltipHoverShowDelay,
hoverHideDelay: props.tooltipHoverHideDelay,
options: props.tippyOptions
}
});
});
}, []);
return <>{props.tooltip(setRef, currentElement, props.core)}</>;
};
export default GraphTooltip;
When triggering the event that causes this ReactService to render the AssociationViewer component, I get the warning: Function component cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Also, reactElement is undefined since the ref cannot be accessed. How can I use React.forwardRef() in the AssociationViewer component to forward the ref to the calling component?
So, here you just have to wrap your functional component inside React.forwardRef()
<GraphTooltip
tooltip={props.tooltipCallback}
tooltipHoverHideDelay={props.tooltipHoverHideDelay}
tooltipHoverShowDelay={props.tooltipHoverShowDelay}
tippyOptions={props.tippyOptions}
core={core.current} // Due to this you are getting warning
/>
Go to your functional component "GraphTooltip" and wrap your export statement in forwardRef().
export const React.forwardRef(GraphTooltip)
Hope it helps!
I'm using function component to create a MUI dataGrid, and trying to add a button in a column, and I have a onRowClick function to open a side pane when user clicking row. The problem is, once I click row, react will report error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Here is the code:
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
};
const formatDates = (columns): GridColDef[] => {
return columns;
};
const addTooltipsToData = (columns: GridColDef[]): GridColDef[] => {
console.log('render tool bar called');
return columns.map((column) => {
const { description, field, headerName } = column;
console.log('inside map');
if (field === ID) {
console.log('直接return');
return column;
}
return {
...column,
renderCell: (): JSX.Element => {
console.log('render run');
return (
<Tooltip arrow title={description || ''} >
<span className={classes.headerCell}>{headerName}</span>
</Tooltip>
);
},
};
});
};
const formatColumns = (columns: GridColDef[]): GridColDef[] => {
const dateFormatted = formatDates(columns);
return addTooltipsToData(dateFormatted);
};
console.log('generic table rendered');
return (
<MuiThemeProvider theme={theme}>
<DataGrid
columns={formatColumns(columns)}
rows={rows}
autoHeight
className={classes.table}
components={{
Toolbar: CustomToolbar,
}}
density={GridDensityTypes.Compact}
filterMode={tableMode}
hideFooterSelectedRowCount
loading={loading}
onFilterModelChange={handleFilterChange}
onSortModelChange={handleSortChange}
sortModel={sortModel}
sortingMode={tableMode}
onRowClick={openViewPane}
/>
</MuiThemeProvider>
);
However, if I change the renderCell to renderHeader, it will work fine.
setRightSlidePlaneContent
setRightSlidePlaneOpen
Above are two state passed by parent component in props. it will open a slide pane.
After I comment setRightSliePlaneOpen, it will work well. But no slide pane show.
Please help me slove it. Or do you know how can I add a button in column not using renderCell?
const PageFrame: FC<IProps> = (props: IProps) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const { Component, userInfo } = props;
const [navBarOpen, setNavBarOpen] = useState(false);
const [rightSlidePlaneOpen, setRightSlidePlaneOpen] = useState(false);
const [rightSlidePlaneContent, setRightSlidePlaneContent] = useState(
<Fragment></Fragment>,
);
const [rightSlidePlaneWidthLarge, setRightSlidePlaneWidthLarge] = useState(
false,
);
useEffect(() => {
dispatch({
type: `${GET_USER_LOGIN_INFO}_${REQUEST}`,
payload: {
empId: userInfo.empId,
auth: { domain: 'GENERAL_USER', actionType: 'GENERAL_USER', action: 'VIEW', empId: userInfo.empId},
},
meta: { remote: true },
});
}, []);
return (
<div className={classes.root}>
<HeaderBar
navBarOpen={navBarOpen}
toggleNavBarOpen={setNavBarOpen}
/>
<NavigationBar open={navBarOpen} toggleOpen={setNavBarOpen} />
<Component
setRightSlidePlaneContent={setRightSlidePlaneContent}
setRightSlidePlaneOpen={setRightSlidePlaneOpen}
setRightSlidePlaneWidthLarge={setRightSlidePlaneWidthLarge}
/>
<PersistentDrawerRight
content={rightSlidePlaneContent}
open={rightSlidePlaneOpen}
rspLarge={rightSlidePlaneWidthLarge}
/>
</div>
);
};
export default PageFrame;
The component that calls setRightSidePlaneOpen
interface IProps {
setRightSlidePlaneContent: React.Dispatch<React.SetStateAction<JSX.Element>>;
setRightSlidePlaneOpen: React.Dispatch<React.SetStateAction<boolean>>;
setRightSlidePlaneWidthLarge: React.Dispatch<SetStateAction<boolean>>;
}
const TagDashboard = (props: IProps): JSX.Element => {
const { setRightSlidePlaneContent, setRightSlidePlaneOpen, setRightSlidePlaneWidthLarge } = props;
const employeeId = useAppSelector((store) => store.userInfo.info.employeeNumber);
const rows = useAppSelector((state) => state.tag.rows);
const accountId = useAppSelector(store => store.userInfo.accountId);
const updateContent = useAppSelector(state => state.tag.updateContent);
const numOfUpdates = useAppSelector(state => state.tag.numOfUpdates);
const dispatch = useAppDispatch();
const closeAddForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
};
const openAddForm = (): void => {
setRightSlidePlaneContent(
<AddForm
category={'tag'}
close={closeAddForm}
title={ADD_FORM_TITLE}
createFunction={createTag}
/>);
setRightSlidePlaneOpen(true);
};
const closeForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
setRightSlidePlaneWidthLarge(false);
};
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
setRightSlidePlaneWidthLarge(true);
};
// to the RSP.
return (
<GenericDashboard
addFunction={openAddForm}
description={DESCRIPTION}
title={TITLE}
columns={columns}
handleRowClick={openViewPane}
rows={rows}
numOfUpdates={numOfUpdates}
updateContent={updateContent}
/>
);
};
This is the component of the right slide pane
const { content, open, rspLarge } = props;
const classes = useStyles();
const drawerClass = rspLarge ? classes.drawerLarge : classes.drawer;
const drawerPaperClass = rspLarge ? classes.drawerPaperLarge : classes.drawerPaper;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={drawerClass}
variant='temporary'
anchor='right'
open={open}
classes={{
paper: drawerPaperClass,
}}
>
<Fragment>{content}</Fragment>
</Drawer>
</div>
);
I want to add a debounce function to not launch the search api for every character:
export const debounce = <F extends ((...args: any) => any)>(
func: F,
waitFor: number,
) => {
let timeout: number = 0;
const debounced = (...args: any) => {
clearTimeout(timeout);
setTimeout(() => func(...args), waitFor);
};
return debounced as (...args: Parameters<F>) => ReturnType<F>;
};
Search.tsx:
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getAllTasks } from "./taskSlice";
import { ReturnedTask } from "../../api/tasks/index";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import { AppState } from "../../app/rootReducer";
import { debounce } from "../../app/utils";
const SearchTask = () => {
const dispatch = useDispatch();
const [open, setOpen] = React.useState(false);
const debounceOnChange = useCallback(debounce(handleChange, 400), []);
const { tasks, userId } = useSelector((state: AppState) => ({
tasks: state.task.tasks,
userId: state.authentication.user.userId,
}));
const options = tasks.length
? tasks.map((task: ReturnedTask) => ({ title: task.title }))
: [];
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
function handleChange(
e: React.ChangeEvent<{}>,
value: string,
) {
const queryParams = [`userId=${userId}`];
if (value.length) {
console.log(value);
queryParams.push(`title=${value}`);
dispatch(getAllTasks(queryParams));
}
}
return (
<Autocomplete
style={{ width: 300 }}
open={open}
onOpen={handleOpen}
onClose={handleClose}
onInputChange={(e: React.ChangeEvent<{}>, value: string) =>
debounceOnChange(e, value)}
getOptionSelected={(option, value) => option.title === value.title}
getOptionLabel={(option) => option.title}
options={options}
renderInput={(params) => (
<TextField
{...params}
label="Search Tasks"
variant="outlined"
InputProps={{ ...params.InputProps, type: "search" }}
/>
)}
/>
);
};
export default SearchTask;
The actual behavior of the component is launching the api for every character typed. I want to launch the api only after an amount of time. How can I fix it?
As #Jayce444 mentioned in the comment, the the context was not preserved between every call and the function.
export const debounce = <F extends ((...args: any) => any)>(
func: F,
waitFor: number,
) => {
let timeout: NodeJS.Timeout;
return (...args: any) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), waitFor);
};
};
I am trying to pass a onSelection function to a component:
const GenderScreen = (props) => {
const onSelection = () => {console.log('clicked')};
const buttons = ['one','two', 'three'];
const breadcrumb = `${i18n.t('stage 1')} > ${i18n.t('stage 2')} > ${i18n.t('stage 3')}`;
const sceneConfig = {
centredButtons: ['one','two', 'three'],
question: {
text: [breadcrumb, i18n.t('What is your ethnicity?')],
},
selectNumber: {},
onSelection: this.onSelection
};
return <SceneCentredButtons { ...props} sceneConfig={sceneConfig} />;
};
Child component:
const SceneCentredButtons = props => (
<LYDSceneContainer goBack={props.goBack} subSteps={props.subSteps}>
<Flexible>
<LYDSceneQuestion {...props.sceneConfig.question} />
<LYDCentredButtons
{...props.sceneConfig}
onSelection={props.sceneConfig.onSelection}
/>
</Flexible>
</LYDSceneContainer>
);
function LYDCentredButtons(props) {
const buttons = this.props;
return (
<View style={styles.main}>
{props.centredButtons.map((button, i) => {
const isLast = i + 1 === props.centredButtons.length;
const marginBottomStyle = !isLast && {
marginBottom: theme.utils.em(1.5),
};
return (
<LYDButton
style={[styles.button, marginBottomStyle]}
label={button.text}
onPress={() => props.onSelection(button.value)}
/>
);
})}
</View>
);
}
However, when in my component I get the error:
props.onSelection is not a function
How can I pass a function to use when my buttons are clicked?
GenderScreen is a stateless functional component, you don't need to use this keyword (this will be undefined).
So instead of this.onSelection use onSelection.
Like this:
const sceneConfig = {
centredButtons: ['one','two', 'three'],
question: {
text: [breadcrumb, i18n.t('What is your ethnicity?')],
},
selectNumber: {},
onSelection: onSelection
};