e.preventDefault prevents me from unit testing with Enzyme - reactjs

I have a reactJS application with a button.
When user presses the button, the user gets verificationCode on their cellphone.
At first, the form was refreshing randomly when user presses the button but I figured out that I have to use
event.preventDefault();
to stop the button refreshing.
So in my onClick handler, I have the following structure.
CODE -
const handleOnClick = async(event) => {
event.preventDefault();
logic.. (which includes async calls to backend API)
}
However, my problem is that when I create an unit test with Enzyme, the function returns in 'preventDefault()' and never executes the logic.
Is there anyway to test unit test in this case?
import React, {useState} from 'react';
import TextField from "#material-ui/core/TextField";
import Grid from "#material-ui/core/Grid";
import {
isInputNumeric,
processKoreaCellphone
} from '../../../../api/auth/authUtils';
import {requestMobileVerificationCode} from "../../../../api/auth/authApiConsumer";
import Select from "#material-ui/core/Select";
import OutlinedInput from "#material-ui/core/OutlinedInput";
import MenuItem from "#material-ui/core/MenuItem";
import {makeStyles} from "#material-ui/core";
import Button from '#material-ui/core/Button';
import Typography from "#material-ui/core/Typography";
import Box from "#material-ui/core/Box";
import Link from '#material-ui/core/Link';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
import Collapse from '#material-ui/core/Collapse';
const useStyles = makeStyles(theme => ({
cellphoneCountryCodeStyle: {
marginTop: '8px',
marginBottom: '4px'
},
requestVerificationMsgBtnStyle: {
marginTop: '8px',
marginBottom: '4px',
minHeight: '40px',
},
txtLabel: {
paddingTop: '0px',
fontSize: '0.75rem',
color: 'rgba(0, 0, 0, 0.54)'
},
txtLabelGrid: {
paddingTop: '0px',
},
}));
export const CellphoneTextField = props => {
const {onStateChange} = props;
const [state, setState] = useState({
errors: [],
onChange: false,
pristine: false,
touched: false,
inProgress: false,
value: {
cellphoneCountryCode: '82',
cellphone: '',
},
verificationCode: [],
isLocked: false
});
const [open, setOpen] = useState(false);
const [verificationCode, setVerificationCode] = useState('');
const [isVerificationCodeError, setIsVerificationCodeError] = useState(false);
const handleOnClick = async (event) => {
const eventCurrentTarget = event.currentTarget.name;
if (eventCurrentTarget === 'resendBtn' || eventCurrentTarget
=== 'resetBtn') {
event.preventDefault();
}
if ((eventCurrentTarget === 'requestVerificationMsgBtn' && state.isLocked
=== false) || eventCurrentTarget === 'resendBtn') {
const updateState = {
...state,
isLocked: true,
inProgress: true,
};
setState(updateState);
onStateChange(updateState);
const lang = navigator.language;
const cellphoneCountryCode = state.value.cellphoneCountryCode;
const cellphone = state.value.cellphone;
const response = await requestMobileVerificationCode(lang,
cellphoneCountryCode, cellphone).catch(e => {});
const updatedState = {
...state,
isLocked: true,
inProgress: false,
verificationCode: state.verificationCode.concat(response),
};
setState(updatedState);
onStateChange(updatedState);
}
};
const classes = useStyles();
return (
<Grid container spacing={1}>
<Grid item xs={12} p={0} className={classes.txtLabelGrid}>
<Typography className={classes.txtLabel} component="h5" id="infoMsg"
name="infoMsg"
variant="caption">
Did not receive the code? <Link
component="button"
variant="body2"
id="resendBtn"
name="resendBtn"
to="/"
className={classes.txtLabel}
onClick={handleOnClick}
>
[resend VericationCode]
</Link>
</Typography>
<Box m={1}/>
</Grid>
</Grid>
)
};
export default CellphoneTextField;
My unit test code
jest.mock("../../../../api/auth/authApiConsumer");
configure({adapter: new Adapter()});
describe('<CellphoneTextField />', () => {
const handleStateChange = jest.fn();
let shallow;
beforeAll(() => {
shallow = createShallow();
});
let wrapper;
beforeEach(() => {
wrapper = shallow(<CellphoneTextField onStateChange={handleStateChange}/>);
});
it('should allow user to resend verification code', (done) => {
act(() => {
wrapper.find('#resendBtn').simulate('click', {
currentTarget: {
name: 'resendBtn'
}
});
});
When I run the unit test, code beyond
event.preventDefault();
is not executed.

The second argument to .simulate('click', ...) is a mock event.
You need to pass in a no-op preventDefault function with that event when you simulate the click, because your code is trying to call e.preventDefault() but preventDefault doesn't exist on the (mock) event.
This should work:
wrapper.find('#resendBtn').simulate('click', {
preventDefault() {},
currentTarget: {
name: 'resendBtn'
}
});

Related

How can I change my data fetching strategy to avoid stale data on initial dynamic route renders?

I'm building a Next.js app which allows users to create and view multiple Kanban boards. There are a couple of different ways that a user can view their different boards:
On the Home page of the app, users see a list of boards that they can click on.
The main navigation menu has a list of boards users can click on.
Both use Next.js Link components.
Clicking the links loads the following dynamic page: src/pages/board/[boardId].js The [boardId].js page fetches the board data using getServerSideProps(). An effect fires on route changes, which updates the redux store. Finally, the Board component uses a useSelector() hook to pull the data out of Redux and render it.
The problem I'm experiencing is that if I click back and forth between different boards, I see a brief flash of the previous board's data before the current board data loads. I am hoping someone can suggest a change I could make to my approach to alleviate this issue.
Source code:
// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'
const BoardPage = ({ board, tasks }) => {
const router = useRouter()
const dispatch = useDispatch()
useEffect(() => {
dispatch(hydrateTasks({ board, tasks }))
}, [router])
return (
<Board />
)
}
export async function getServerSideProps ({ query, req, res }) {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
const { boardId } = query
const boardQuery = prisma.board.findUnique({
where: {
id: boardId
},
select: {
name: true,
description: true,
id: true,
TO_DO: true,
IN_PROGRESS: true,
DONE: true
}
})
const taskQuery = prisma.task.findMany({
where: {
board: boardId
},
select: {
id: true,
title: true,
description: true,
status: true,
board: true
}
})
try {
const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
return { props: { board, tasks } }
} catch (error) {
console.log(error)
return { props: { board: {}, tasks: [] } }
}
}
export default BoardPage
// src/Components/Board/Board.js
import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch, useSelector } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'
const Board = () => {
const [css, theme] = useStyletron()
const dispatch = useDispatch()
useEffect(() => {
document.querySelector('body').style.background = theme.colors.backgroundPrimary
}, [theme])
// get data from Redux
const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)
// set up a boolean and a trigger to control "done"" animation
const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })
const doneTrigger = doneUseTrigger({ duration: 4000 })
// get width and height for confetti animation
const { width, height } = useWindowSize()
// curry the drag end handler for the drag and drop UI
const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })
return (
<ErrorBoundary>
<DragDropContext onDragEnd={curriedDragEnd}>
<div className={css({
marginLeft: '46px',
marginTop: '16px',
fontFamily: 'Roboto',
display: 'flex',
alignItems: 'baseline'
})}>
<h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
{boardDescription &&
<p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
}
</div>
<StyledBoardMain>
{columnOrder.map(columnKey => {
const column = columns[columnKey]
const tasksArray = column.taskIds.map(taskId => tasks[taskId])
return (
<Column
column={columnKey}
key={`COLUMN_${columnKey}`}
tasks={tasksArray}
title={column.title}
status={column.status}
/>
)
})}
</StyledBoardMain>
</DragDropContext>
{showDone && <Confetti
width={width}
height={height}
/>}
</ErrorBoundary>
)
}
export default Board
// src/pages/index.tsx
import React, {PropsWithChildren} from 'react'
import {useSelector} from "react-redux";
import {authOptions} from 'pages/api/auth/[...nextauth]'
import {unstable_getServerSession} from "next-auth/next"
import CreateBoardModal from 'Components/Modals/CreateBoard/CreateBoard'
import Link from 'next/link'
import {useStyletron} from "baseui";
const Index: React.FC = (props: PropsWithChildren<any>) => {
const {board: boards} = useSelector(state => state)
const [css, theme] = useStyletron()
return boards ? (
<>
<div style={{marginLeft: '46px', fontFamily: 'Roboto', width: '600px'}}>
<h1 className={css({fontSize: '22px'})}>Boards</h1>
{boards.map(({name, description, id}) => (
<Link href="/board/[boardId]" as={`/board/${id}`} key={id}>
<div className={css({
padding: '20px',
marginBottom: '20px',
borderRadius: '6px',
background: theme.colors.postItYellow,
cursor: 'pointer'
})}>
<h2 className={css({fontSize: '20px'})}>
<a className={css({color: theme.colors.primary, width: '100%', display: 'block'})}>
{name}
</a>
</h2>
<p>{description}</p>
</div>
</Link>
))}
</div>
</>
) : (
<>
<h1>Let's get started</h1>
<button>Create a board</button>
</>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(context.req, context.res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
return {props: {session}}
}
export default Index
It looks like there's only ever one board in the redux. You could instead use a namespace so that you don't have to keep swapping different data in and out of the store.
type StoreSlice = {
[boardId: string]: Board;
}
Then the "brief flash" that you will see will either be the previous data for the correct board, or nothing if it has not yet been fetched.

How can I make an API call on clicking a button in React

I want to fetch data from a currency API when I click the Button component. I have placed the code to request data in a useEffect hook but when I attempt to place the useEffect in the handleClickOpen function, it returns an error. Should I leave out useEEffect? Where should I place the API call in the code below?
import * as React from 'react';
import { useEffect } from "react"
import axios from "axios"
import PropTypes from 'prop-types';
import Button from '#mui/material/Button';
import Avatar from '#mui/material/Avatar';
import List from '#mui/material/List';
import ListItem from '#mui/material/ListItem';
import ListItemAvatar from '#mui/material/ListItemAvatar';
import ListItemText from '#mui/material/ListItemText';
import DialogTitle from '#mui/material/DialogTitle';
import Dialog from '#mui/material/Dialog';
import { blue } from '#mui/material/colors';
const emails = [
{title: "Pound sterling", symbol: "£", id: 1, acronym: "GBP"},
{title: "Euro", symbol: "€", id: 2, acronym: "EUR"},
{title: "Nigerian Naira", symbol: "₦", id: 3, acronym: "NGN"},
{title: "Saudi Arabian riyal", symbol: "SR", id: 4, acronym: "SAR"},
{title: "Indian rupee", symbol: "₹", id: 5, acronym: "INR"},
{title: "United States dollar", symbol: "$", id: 6, acronym: "USD"},
]
function SimpleDialog(props) {
const { onClose, selectedValue, open } = props;
const handleClose = () => {
onClose(selectedValue);
};
const handleListItemClick = (value) => {
onClose(value);
};
useEffect(() => {
const options = {
method: 'GET',
url: 'https://exchangerate-api.p.rapidapi.com/rapid/latest/USD',
headers: {
'X-RapidAPI-Key': 'c0d2e70417msh3bde3b7dbe9e25ap12748ejsncd1fe394742c',
'X-RapidAPI-Host': 'exchangerate-api.p.rapidapi.com'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
},[])
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle>Choose a currency</DialogTitle>
<List sx={{ pt: 0 }}>
{emails.map((email) => (
<ListItem button onClick={() => handleListItemClick(email.symbol)} key={email.id}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: blue[100], color: blue[600] }}>
{email.symbol}
</Avatar>
</ListItemAvatar>
<ListItemText primary={email.title} />
</ListItem>
))}
</List>
</Dialog>
);
}
SimpleDialog.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
selectedValue: PropTypes.string.isRequired,
};
export default function SimpleDialogDemo({selectedValue, setSelectedValue}) {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (value) => {
setOpen(false);
setSelectedValue(value);
};
return (
<div>
<Button variant="text" onClick={handleClickOpen} sx={{
ml: 30,
mb: 0,
p: 0,
}}>
{selectedValue} {emails.find(email => email.symbol===selectedValue).acronym}
</Button>
<SimpleDialog
selectedValue={selectedValue}
open={open}
onClose={handleClose}
/>
</div>
);
}
Just move the API call to a separate function, say getAPIData or makeAPICall. Either use a normal function or use useCallback to avoid multiple creations of the function.
You can now call this function in useEffect on initial load as well as anywhere else you want to make the same API call.
use a state variable to store the data obtained from the API call and use this variable to render the JSX Element.
const makeAPICall = () => {
//api call and response
//store response in a state variable
}
useEffect(()=>{
makeAPICall();
}, [])
const handleClickEvent = () => {
makeAPICall();
}
I recommending you using reducer for handling such action, so when you click a button it will dispatch a state to do API fetch in reducer and dispatch a state when page load inside useEffect.
First thing is react does not recomment or allow using of hooks like useEffect in other functions, thats why you are getting error. Now since you need your data on click, place the fetch code you have written in useEffect in ClickHandler ( you donot need useEffect for the scenario defined above ).
const handleListItemClick = () => {
const options = {
method: 'GET',
url: 'https://exchangerate-api.p.rapidapi.com/rapid/latest/USD',
headers: {
'X-RapidAPI-Key': 'c0d2e70417msh3bde3b7dbe9e25ap12748ejsncd1fe394742c',
'X-RapidAPI-Host': 'exchangerate-api.p.rapidapi.com'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
And if you want to make use of this data fetched, add it in a state variable and use it. :)

React newbie getting an error "Property 'blockStyleFn' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes"

I was trying to set styles for my codeblock and got an error from this part,
blockStyleFn={myBlockStyleFn}
It says "Property 'blockStyleFn' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly'."
Below is the full code of TextEditor.tsx for my draftjs editor and customBlock.css which is located in the same folder. I don't understand why this specific property is not identified while others are working well under Editor. I guess I may have to specify the type of property somewhere. How can I fix this error?
TextEditor.tsx
import { Typography } from "#mui/material";
import { ContentBlock, ContentState, convertFromHTML, convertToRaw, EditorState } from "draft-js";
import draftToHtml from "draftjs-to-html";
import React, { useEffect, useState } from "react";
import { Editor } from "react-draft-wysiwyg";
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
import "./customBlock.css";
const TextEditor = ({ onChange, value } : any) => {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const [updated, setUpdated] = useState(false);
useEffect(() => {
if (!updated) {
const defaultValue = value ? value : "";
const blocksFromHtml = convertFromHTML(defaultValue);
const contentState = ContentState.createFromBlockArray(
blocksFromHtml.contentBlocks,
blocksFromHtml.entityMap
);
const newEditorState = EditorState.createWithContent(contentState);
setEditorState(newEditorState);
}
}, [value]);
const onEditorStateChange = (editorState : any) => {
setUpdated(true);
setEditorState(editorState);
return onChange(draftToHtml(convertToRaw(editorState.getCurrentContent())));
};
const toolbar = {
options: ["inline", "blockType", "list"],
inline: {
inDropdown: false,
options: ['bold', 'underline', 'italic']
},
blockType: {
inDropdown: false,
options: ["Normal", 'Blockquote', 'Code']
},
list: {
inDropdown: false,
options: ["unordered", "ordered"]
},
};
function myBlockStyleFn(contentBlock: ContentBlock) {
const type = contentBlock.getType();
if (type === 'code-block') {
return 'customBlock';
}
}
return (
<React.Fragment>
<div className="editor" style={{maxHeight: "570px" , width:'100%' }}>
<Typography sx={{fontSize: 20}}>
<Editor
spellCheck
editorState={editorState}
onEditorStateChange={onEditorStateChange}
toolbar={toolbar}
editorStyle={{height: "500px" }}
blockStyleFn={myBlockStyleFn}
/>
</Typography>
</div>
</React.Fragment>
);
};
export default TextEditor;
customBlock.css
.customBlock {
color: #999;
font-family: 'Hoefler Text', Georgia, serif;
display:block;
}

Why I can't update state with Context API

I Make Kakaotalk with ReactJS
I make Search User Page now, and I use Context API because use Global State
addFriend/Index.tsx
import Header from "components/friends/Header";
import InputUserId from "./InputUserId";
import Profile from "./Profile";
import { AddFriendProvider } from "./AddFriendContext";
const AddFriend = () => {
<>
<AddFriendProvider>
<Header />
<InputUserId />
<Profile />
</AddFriendProvider>
</>;
};
export default AddFriend;
addFriend/AddFriendContext.tsx
import React, { createContext, useState } from "react";
const AddFriendContext = createContext<any>({
state: {
userId: "",
searchResult: "",
},
actions: {
setUserId: () => {},
setSearchResult: () => {},
},
});
const AddFriendProvider = ({ children }: any) => {
const [userId, setUserId] = useState("");
const [searchResult, setSearchResult] = useState("");
const value = {
state: { userId, searchResult },
actions: { setUserId, setSearchResult },
};
console.log(value);
return (
<AddFriendContext.Provider value={value}>
{console.log(setUserId, setSearchResult)}
{children}
</AddFriendContext.Provider>
);
};
export { AddFriendProvider };
export default AddFriendContext;
addFriend/InputUserId.tsx
import styled from "styled-components";
import Input from "components/common/Input";
import { ChangeEvent, useContext } from "react";
import AddFriendContext from "./AddFriendContext";
const StyledInputWrapper = styled.div`
width: 370px;
height: 80px;
display: flex;
align-items: center;
`;
const InputUserId = () => {
const { state, actions } = useContext(AddFriendContext);
console.log(state, actions);
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
actions.setUserId(value);
};
const inputStyle = {
width: "100%",
height: "40px",
paddingLeft: "10px",
fontWeight: "bold",
};
const subInputStyle = {
borderBottom: "2px solid lightgray",
focus: "2px solid black",
};
return (
<StyledInputWrapper>
<Input
type="text"
placeholder="카카오톡 아이디를 입력해주세요."
required
inputStyle={inputStyle}
subInputStyle={subInputStyle}
value={state.userId}
onChange={onChange}
/>
</StyledInputWrapper>
);
};
export default InputUserId;
If i change input element in InputUserId.tsx call actions.setUserId(value) but It not worked.
I think login is >
When Change Input, call actions.setUserId and update state.userId
Change Input value when state.userId Update
But it now Worked..
Help me and if some trouble in my code feedback me plz. thanks.

React-Admin I Custom button not able to get form data

I have created a custom button to reject a user. I want to send the reason for rejecting but not able to send the form data.I am using "FormWithRedirect" in userEdit component.
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import ThumbDown from '#material-ui/icons/ThumbDown';
import { useUpdate, useNotify, useRedirect } from 'react-admin';
/**
* This custom button demonstrate using a custom action to update data
*/
const useStyles = makeStyles((theme) => ({
button: {
color: 'red',
borderColor: 'red',
"&:hover": {
color: '#fff',
background: 'red',
borderColor: 'red',
}
},
}));
const RejectButton = ({ record }: any) => {
const notify = useNotify();
const redirectTo = useRedirect();
const classes = useStyles();
const [reject, { loading }] = useUpdate(
`users/${record.id}/_reject`,
'',
{ reason: 'Due to missinng information or may be the license picture is not visible. Please upload again.' },
record,
{
undoable: true,
onSuccess: () => {
notify(
'User profile rejected!',
'info',
{},
true
);
redirectTo('/users');
},
onFailure: (error: any) => notify(`Error: ${error.error}`, 'warning'),
}
);
return (
<Button
variant="outlined"
color="primary"
size="small"
onClick={reject}
disabled={loading}
className={classes.button}
startIcon={<ThumbDown />}
>
Reject
</Button>
);
};
RejectButton.propTypes = {
record: PropTypes.object,
};
export default RejectButton;
this is my code for rejectButton component and in reason I am sending the static value but I want to send whatever I typed in TextArea component.

Resources