Testing async handler and subsequent changes on the page with jest - javascript

What i have is the simple page where I have the "Reset Password" button and "Confirm" button by defaut. If you will press "Reset password" it will be fetched from the server and displayed where this button was in "TextInput". Aslo "Confirm" button become active after that.
Here is my component.
export default function App({ profileId, onClose }) {
const [ resetPasswordIsPressed, setResetPasswordIsPressed ] = useState(false);
const [ temporaryPassword, setTemporaryPassword ] = useState(null);
const [ isButtonDisabled, setIsButtonDisabled ] = true;
const handleUserUpdate = () => {
handleChangePassword(false);
onClose()
}
const handleChangePassword = async isPressed => {
const tempPassword = await fetchPassword(isPressed, profileId);
setTemporaryPassword(tempPassword);
setResetPasswordIsPressed(isPressed);
setIsButtonDisabled(false);
}
return (
<div className="App">
{ resetPasswordIsPressed ? (
<TextInput
isDisabled
testId='passwordInput'
value={temporaryPassword}
/>
) : (
<PasswrodResetButton
onClick={() => handleChangePassword(true)}
text='PasswordReset'
/>
)
}
<ConfirmButton isDisabled={isButtonDisabled} onClick={handleUserUpdate}/>
</div>
);
}
and here is fetch function which is imported from separate file
const fetchPassword = async (isPressed, profileId) => {
const getPassword = httpPost(userService, userendPoint);
try {
const {data} = await getPassword({
data: {
profileId
}
});
const { tempPassword } = data;
return tempPassword;
} catch (e) {
console.log(e.message)
}
}
What i need is to test that after clicking "Reset Password" handler was called, after that the "Reset Password" button is not on the page anymore, the "TextInput" with fetched password IS on the page, and the confirm button become active. Here is what I'm trying to do.
describe('User handlers', () => {
it('Should trigger reset password handler', async () => {
const mockCallBack = jest.fn();
const action = shallow(<PasswordResetButton onClick={mockCallBack}/>);
action.simulate('click');
await screen.findByTestId('passwordInput')
})
})
But got 'Unable to find and element by: [data-testid="passwordInput"]'

You're rendering your 'action' separately from your App component, which makes it completely independent.
I can't see in your code what 'screen' is, but most likely it's your shallowly rendered App component, and this is exactly where you need to find your <PasswordResetButton /> and click it.
If you were to add to your <PasswordResetButton /> a testid like you did to <TextInput />, you'd be able to do something like this:
it('Should trigger reset password handler', async () => {
const action = await screen.findByTestId('#passwordResetButton');
action.simulate('click');
const passwordInput = await screen.findByTestId('passwordInput');
expect(passwordInput).toHaveLength(1);
})
But again, I'm not sure what your screen variable is and what exactly screen.findByTestId does and returns.

Related

Fetch not working on first click of onClick function

When I use modalOpen in a onClick function it wont fetch api on the 1st click causing the code to break but it will on 2nd click what can cause it
// get anime result and push to modal function
const modalAnime = async () => {
const { data } = await fetch(`${base_url}/anime/${animeId}`)
.then((data) => data.json())
.catch((err) => console.log(err));
SetAnimeModalData(data);
};
I am trying to get the fetch to work on the first click but it doesn't until second or third click
const modalOpen = (event) => {
SetAnimeId(event.currentTarget.id);
SetModalVisible(true);
modalAnime();
console.log(animeId);
};
const modalClose = () => {
SetModalVisible(false);
SetAnimeId("");
};
return (
<div className="app">
<Header
searchAnime={searchAnime}
search={search}
SetSearch={SetSearch}
mostPopular={mostPopular}
topRated={topRated}
/>
{loadingState ? (
<ResultLoading />
) : (
<Results
animeResults={animeResults}
topRated={topRated}
mostPopular={mostPopular}
modalOpen={modalOpen}
/>
)}
{modalVisible ? <AnimeInfoModal modalClose={modalClose} /> : <></>}
</div>
);
the modal opens fine but the ID isn't captured until the second or third click
I have more code but Stack Overflow won't let me add it.
SetAnimeId() won't update the animeId state property until the next render cycle. Seems like you should only update the visible and animeId states after you've fetched data.
You should also check for request failures by checking the Response.ok property.
// this could be defined outside your component
const fetchAnime = async (animeId) => {
const res = await fetch(`${base_url}/anime/${animeId}`);
if (!res.ok) {
throw res;
}
return (await res.json()).data;
};
const modalOpen = async ({ currentTarget: { id } }) => {
// Await data fetching then set all new state values
SetAnimeModalData(await fetchAnime(id));
SetAnimeId(id);
SetModalVisible(true);
};

mock value inside the component file

I would like to ask if I have the variable useState in the component which is used as a condition that determines the element inside it will appear or not. How to mock the variable? So that I can test the element inside the condition if the value is 'login'.
const [dataHistory,seDataHistory] = useState("")
const [data,setData] = useState("firstTimeLogin")
const func2 = () => {
getData({
item1: "0",
}).
then((res) => {
funct();
})
}
const funct = () =>
{setData("login")}
return
{data === "firstTimeLogin" && (
<div><button onClick="funct2()">next</button></div>
)}
{data === "login" && (
<div>flow login</div>
)}
Firstly, you need to add data-testid for your button
{data === "firstTimeLogin" && (
<div><button onClick="funct2" data-testid="next-button">next</button></div>
)}
You called onClick="funct2()" which is to trigger funct2 immediately after re-rendering, so I modified it to onClick="funct2".
Note that you also can use next content in the button for the event click but I'd prefer using data-testid is more fixed than next content.
In your test file, you should mock getData and call fireEvent.click to trigger funct2()
import { screen, fireEvent, render } from '#testing-library/react';
//'/getData' must be the actual path you're importing for `getData` usage
jest.mock('/getData', () => {
return {
getData: () => new Promise((resolve) => { resolve('') }) // it's mocked, so you can pass any data here
}
})
it('should call funct', async () => {
render(<YourComponent {...yourProps}/>)
const nextButton = await screen.findByTestId("next-button"); //matched with `data-testid` we defined earlier
fireEvent.click(nextButton); //trigger the event click on that button
const flowLoginElements = await screen.findByText('flow login'); //after state changes, you should have `flow login` in your content
expect(flowLoginElements).toBeTruthy();
})
You can create a button and onClick of the button call funct()

Component not updating after fireEvent.click

I'm new to Jest and the testing library. I have a NavbarContainer component that renders one button or another depending on a variable (menuOpen) that it's being changed with a function. I have already checked that the variable changes its value after the fireEvent, however, it seems like the component it's not updating. What am I doing wrong?
Here there is my component
export const NavbarContainer = ({functionality, menuOpen, activeLink}) => {
return (
<div className="navbar-container">
{ menuOpen && <Navbar functionality={functionality} activeLink={activeLink}/> }
{ menuOpen && <Button text="navbar.back" functionality={functionality}></Button> }
{ !menuOpen && <Button text="navbar.menu" functionality={functionality} modificator="back"></Button> }
</div>
);
};
Here is the button
export const Button = ({ text, type = "button", functionality, disabled = false}) => {
return (
<button onClick={functionality} type={type} disabled={disabled}>{i18n.t(text)}</button>
);
};
Here are the values and functions I am passing to the NavbarContainer component
const [menuOpen, setMenuOpen] = useState(false);
const [activeLink, setActiveLink] = useState("");
const openMenu = (link) => {
setMenuOpen(!menuOpen);
setActiveLink(link.toString());
};
Here are my tests
describe("NavbarContainer", () => {
i18n.changeLanguage('cimode');
let component;
let menuOpen = true;
const openMenu = () => {
menuOpen = !menuOpen;
};
beforeEach(() => {
component = render(
<BrowserRouter basename="/">
<NavbarContainer menuOpen={menuOpen} functionality={openMenu} activeLink=""/>
</BrowserRouter>
);
});
it("after click the menu button is shown", async () => {
const backButton = component.queryByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.queryByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
});
Here is the error I'm getting
expect(received).toBeInTheDocument()
the received value must be an HTMLElement or an SVG element.
Received has value: null
Since it might take a few seconds for your navbar.menu to appear you after your click, you need to use findBy to try and select your item. This adds a timeout of 5 seconds to try and find the item. If it still hasn't appeared after 5 seconds, then there is probably something else going on with your code.
If you want your test to be async, then I would recommend that your first call also be a findBy rather than a queryBy since queryBy isn't an asynchronous call.
it("after click the menu button is shown", async () => {
const backButton = await component.findyByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.findByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
You also probably don't need that last expect call because if it can't find the navbar.menu item via the previous call, then it would fail anyway. Same if it finds it.

How do I only re-render the 'Like' button in React JS?

const SiteDetails = ({site, user}) => {
const dispatch = useDispatch()
const updateSite = async (siteId, siteObject) => {
try {
const updatedSite = await siteService.update(siteId, siteObject)
dispatch(toggleStatus(siteId, updatedSite))
// dispatch(setNotification(`One like added to ${updatedSite.title}`))
} catch (exception) {
console.log(exception)
dispatch(setNotification("Could not update site"))
}
}
return (
<div className='site-details'>
<Link href={site.url}>{site.url}</Link>
<h2>
<LikeButton user={user} site={site} updateSite={updateSite} />
<VisitButton user={user} site={site} updateSite={updateSite} />
</h2>
<p>{site.description}</p>
<img src={site.imageUrl} alt={"Image could not be loaded"} />
</div>
)
}
const LikeButton = ({user, site, updateSite}) => {
const dispatch = useDispatch()
const likedList = site?.userLiked.find(n => n?.username === user.username)
useEffect(() => {
if (!likedList) {
const newUser = { username: user.username, liked: false }
const updatedArray = site.userLiked.concat(newUser)
const updatedSite = {...site, userLiked: updatedArray}
updateSite(site.id, updatedSite)
}
},[])
const liked = likedList?.liked
const handleLike = async () => {
const indexCurr = site.userLiked.indexOf(likedList)
//updatedSite is the parent site. This will have its liked status toggled
//actually updatedUserLiked can simply use username: user.username and liked: true
const updatedUserLiked = { username: likedList?.username, liked: !likedList.liked}
site.userLiked[indexCurr] = updatedUserLiked
const updatedSite = {...site, userLiked: site.userLiked}
updateSite(site.id, updatedSite)
//childSite is the spawned from the parent,
//it will contain a parent, which is the updatedSite
const childSite = {...site, parent: updatedSite, opcode: 100}
const newSite = await siteService.create(childSite)
// dispatch(createSite(newSite))
dispatch(initializeUsers())
dispatch(initSites())
}
return (
<Button
size='small' variant='contained'
color={liked ? 'secondary' : 'primary'}
onClick={!liked ? handleLike : null} className='site-like'>{liked ? 'Already Liked' : "like"}
</Button>
)
}
Expected Behaviour
Actual Behaviour
Hi everyone, my goal is to make it such that when the 'Like' button is clicked, the appearance of the button will change but the rest of the page does not refresh. However, if you look at the actual behaviour, after the button is clicked, the page is re-rendered and is restored to its default state. I have made my own component which upon clicking will display further details below.
I have tried looking at React.memo and useRef, none of which worked for me. Help on this would be very much appreciated. Thanks!
Have tried using the 'useState' hook?
import {useState} from 'react'
function LikeButton() {
//set default value to false
const [liked, setLiked] = useState(false);
const handleClick = () => {setLiked(!liked)}
return(
<Button color={liked? 'secondary' : 'primary'} onClick={() => handleClick()}
/>
);
}
export default LikeButton;
this should only re-render the button
check this out as well:
ReactJS - Does render get called any time "setState" is called?

React JS how would I render an alert inside of a function

I'm trying to make a login page and I want to display an error if the login fails.
I call the function here: onClick={ this.login }
But I want the function to display the error message without re-rendering everything. The function is inside a class
Try something like this:
const [username, setUsername] = useState('');
const [error, setError] = useState('');
const handleLogin = async () => {
// request a login to your server
const res = await fetch('http://yourapi.com/login');
// determine if the request was successful
if (res.error) {
setError('Invalid input.')
}
else {
// continue
}
};
return (
<Form onSubmit={handleLogin}>
<span>{error}</span>
<input onChangeText={text => setUsername(text)}/>
</Form>
);
Found out how to do it by creating an invisible alert that's visible when I set a variable to the error
{
this.state.status &&
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
{ this.state.status }
</Alert>
}

Categories

Resources