Alter useMemo when write input in react testing library - javascript

I'm trying to do a test, in which when changing the input, I have to read the useMemo and change the disabled of my button, the userEvent is not making this change, has anyone gone through this?
I'm going to put part of my source code here, where the component and the test script are.
<>
<input
data-testid="ipt-email"
value={form.email}
onChange={(e) => {
setForm({ ...form, email: e.target.value });
}}
/>
<button data-testid="submit-sendUser" disabled={isDisabled}>
OK
</button>
</>
This is my hook
const isDisabled = useMemo(() => {
const { email } = form;
if (!email.length) return true
return false;
}, [form]);
Right after that is my unit test, where I write to the input and wait for the state to change
import userEvent from "#testing-library/user-event";
it("Should enable button when form is valid", async () => {
const wrapper = render(<MyComponent />);
const getEmail = wrapper.getByTestId("ipt-email");
await userEvent.type(getEmail, 'example#example.com');
const getBtnSubmit = wrapper.getByTestId("submit-sendUser");
console.log(wrapper.container.innerHTML);
expect(getBtnSubmit).not.toBeDisabled();
});
I can't make the input change reflect in the button hook

Need to wait for changes to occur after the action
await waitFor(() => expect(getBtnSubmit).not.toBeDisabled())
moving code inside the handler applieds to useEffect, but I feel its better to handle the validation inside the handler to so that code changes remain in one place.
Hope it helps

The combination of fireEvents and forced move to the "next frame" worked for me
const tick = () => {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
};
// async test
fireEvent.change(getEmail, {target: {value: 'example#example.com'}})
await tick();
expect(getBtnSubmit).not.toBeDisabled();

In your constant getEmail you get a component with a data-testid='ipt-name' instead of 'ipt-email' (but this is no longer relevant since the requester has modified his question...). The code below works for me :
my test :
import { render, screen, waitFor } from '#testing-library/react';
import App from './App';
import userEvent from '#testing-library/user-event';
it("Should enable button when form is valid", async () => {
render(<App />);
expect(screen.getByTestId("submit-sendUser")).toBeDisabled();
const getEmail = screen.getByTestId("ipt-email");
userEvent.type(getEmail, 'example#example.com');
await waitFor(() => expect(screen.getByTestId("submit-sendUser")).not.toBeDisabled());
});
my component :
import React, { useMemo, useState } from "react";
export const App = () => {
const [form, setForm] = useState({ email: '' });
const isDisabled = useMemo(() => {
const { email } = form;
if (!email || !email.length) return true;
return false;
}, [form]);
return (
<div>
<input
data-testid="ipt-email"
value={form.email}
onChange={(e) => {
setForm({ ...form, email: e.target.value });
}}
/>
<button data-testid="submit-sendUser" disabled={isDisabled}>
OK
</button>
</div>
);
};
export default App;

Related

Jest: Expect a function inside component to have been called

Hi have this simple piece of code inspired by https://testing-library.com/docs/example-react-formik/
import React from "react";
import { Formik, Field, Form } from "formik";
const sleep = (ms: any) => new Promise((r) => setTimeout(r, ms));
export const MyForm = () => {
const handleSubmit = async (values: any) => {
await sleep(500);
console.log(values);
};
return (
<div>
<Formik
initialValues={{
firstName: "",
}}
onSubmit={handleSubmit}
>
<Form>
<label htmlFor="firstName">First Name</label>
<Field id="firstName" name="firstName" placeholder="Jane" />
<button type="submit">Submit</button>
</Form>
</Formik>
</div>
);
};
and the Test:
import React from "react";
import { render, screen, waitFor } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import { MyForm } from "./MyForm";
test("rendering and submitting a basic Formik form", async () => {
const handleSubmit = jest.fn(); // this doing nothing
render(<MyForm />);
const user = userEvent.setup();
await user.type(screen.getByLabelText(/first name/i), "John");
await user.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
});
Console.log printed the inputed value: { firstName: 'John' }, but the test fails due the fact it understand that handleSubmit was not been called.
What’s going wrong with this code?
Because you didn't pass the mock handleSubmit to <MyForm/> component. You should pass it to the component as onSubmit prop and call it when the internal handleSubmit event handler executes.
Let's see the RTL official formik testing example
MyForm.tsx:
export const MyForm = ({onSubmit}) => {
const handleSubmit = async values => {
await sleep(500)
submit(values)
}
return <div>...</div>
}
MyForm.test.tsx:
test('rendering and submitting a basic Formik form', async () => {
const handleSubmit = jest.fn();
render(<MyForm onSubmit={handleSubmit} />);
await user.click(screen.getByRole('button', {name: /submit/i}));
await waitFor(() => expect(handleSubmit).toHaveBeenCalledTimes(1));
})
Did you see the difference between the official example and your code?

I want to create a confirmation modal before route changes in Next.js

I have code block like this
const onRouteChangeStart = React.useCallback(() => {
if (formState.isDirty) {
if (window.confirm('Confirmation message')) {
return true;
}
NProgress.done();
throw "Abort route change by user's confirmation.";
}
}, [formState.isDirty]);
React.useEffect(() => {
Router.events.on('routeChangeStart', onRouteChangeStart);
return () => {
Router.events.off('routeChangeStart', onRouteChangeStart);
};
}, [onRouteChangeStart]);
It works as I want but I want to add a Custom Confirmation Modal instead of Native Confirmation.
When I added, route changes did not stop. That's why I couldn't wait for the user response.
What can I do? Thank you for your responses.
There is a good sample here where it aborts the current route change and saves it to state, prompts the custom model. If confirmed, it pushes the route again.
https://github.com/vercel/next.js/discussions/32231?sort=new?sort=new#discussioncomment-2033546
import { useRouter } from 'next/router';
import React from 'react';
import Dialog from './Dialog';
export interface UnsavedChangesDialogProps {
shouldConfirmLeave: boolean;
}
export const UnsavedChangesDialog = ({
shouldConfirmLeave,
}: UnsavedChangesDialogProps): React.ReactElement<UnsavedChangesDialogProps> => {
const [shouldShowLeaveConfirmDialog, setShouldShowLeaveConfirmDialog] = React.useState(false);
const [nextRouterPath, setNextRouterPath] = React.useState<string>();
const Router = useRouter();
const onRouteChangeStart = React.useCallback(
(nextPath: string) => {
if (!shouldConfirmLeave) {
return;
}
setShouldShowLeaveConfirmDialog(true);
setNextRouterPath(nextPath);
throw 'cancelRouteChange';
},
[shouldConfirmLeave]
);
const onRejectRouteChange = () => {
setNextRouterPath(null);
setShouldShowLeaveConfirmDialog(false);
};
const onConfirmRouteChange = () => {
setShouldShowLeaveConfirmDialog(false);
// simply remove the listener here so that it doesn't get triggered when we push the new route.
// This assumes that the component will be removed anyway as the route changes
removeListener();
Router.push(nextRouterPath);
};
const removeListener = () => {
Router.events.off('routeChangeStart', onRouteChangeStart);
};
React.useEffect(() => {
Router.events.on('routeChangeStart', onRouteChangeStart);
return removeListener;
}, [onRouteChangeStart]);
return (
<Dialog
title="You have unsaved changes"
description="Leaving this page will discard unsaved changes. Are you sure?"
confirmLabel="Discard changes"
cancelLabel="Go back"
isOpen={shouldShowLeaveConfirmDialog}
onConfirm={onConfirmRouteChange}
onReject={onRejectRouteChange}
/>
);
};

How to instantly display data after an API call in react hooks

I don't understand why the second line, which reads data from the props, is not displayed as instantly as the first, i would like them to be displayed instantly
I update the state when a button is clicked, which calls api, data is coming in, the state is updating, but the second line requires an additional press to display
How to display both lines at once after a call? What's my mistake?
I'm using react hooks, and i know that required to use useEffect for re-render component, i know, that how do work asynchronous call,but i'm a little confused, how can i solve my problem, maybe i need to use 'useDeep effect' so that watching my object properties, or i don't understand at all how to use 'useEffect' in my situation, or even my api call incorrectly?
I have tried many different solution methods, for instance using Promise.all, waiting for a response and only then update the state
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./test";
ReactDOM.render(<App />, document.getElementById("root"));
app.js
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const useDataApi = (initialState) => {
const [state, setState] = useState(initialState);
const stateCopy = [...state];
const setDate = (number, value) => {
setState(() => {
stateCopy[number].date = value;
return stateCopy;
});
};
const setInfo = async () => {
stateCopy.map((item, index) =>
getFetch(item.steamId).then((res) => setDate(index, res.Date))
);
};
const getFetch = async (id) => {
if (id === "") return;
const requestID = await fetch(`https://api.covid19api.com/summary`);
const responseJSON = await requestID.json();
console.log(responseJSON);
const result = await responseJSON;
return result;
};
return { state, setState, setInfo };
};
const Children = ({ data }) => {
return (
<>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.date ? item.date : "Not data"}
<br></br>
</li>
))}
</ul>
</>
);
};
const InfoUsers = ({ number, steamid, change }) => {
return (
<>
<input
value={steamid}
numb={number}
onChange={(e) => change(number, e.target.value)}
/>
</>
);
};
function App() {
const usersProfiles = [
{ date: "", id: 1 },
{ date: "", id: 2 }
];
const profiles = useDataApi(usersProfiles);
return (
<div>
<InfoUsers number={0} change={profiles.setID} />
<InfoUsers number={1} change={profiles.setID} />
<button onClick={() => profiles.setInfo()}>Get</button>
<Children data={profiles.state} loading={profiles} />
</div>
);
}
export default App;
To get the data, just click GET
In this example, completely removed useEffect, maybe i don’t understand how to use it correctly.
P.s: Sorry for bad english
You don't need stateCopy, as you have it in the callback of the setState:
const setInfo = async () => {
// we want to update the component only once
const results = await Promise.all(
state.map(item => getFetch(item.steamId))
);
// 's' is the current state
setState(s =>
results.map((res, index) => ({ ...s[index], date: res.Date })
);
};

How to implement router leave guard like vue-router in react-router-dom?

I need to ask user if submit form or not in react project like in vue-router beforeRouteLeave guard:
<template>
<div>
<input
type="text"
name="name"
v-model="name"
placeholder="input some text"
/>
<router-link to="/">go back or click goback menu in browser </router-link>
</div>
</template>
<script>
import { MessageBox } from "element-ui";
export default {
data() {
return {
name: "",
};
},
methods: {
http() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const isOk = Math.random() > 0.5;
isOk && resolve({ ok: true });
!isOk && reject(new Error("server 503"));
}, 2000);
});
},
},
async beforeRouteLeave(to, from, next) {
if (this.name) {
// form has input
try {
// it's better to use UI modal than window.confirm
await MessageBox.confirm("do u want to submit?", "tips", {
confirmButtonText: "yes",
cancelButtonText: "no",
type: "warning",
});
const res = await this.http(); //submit form
console.log(res);
// http success,let it go
res.ok && next();
} catch (error) {
next(false);
}
} else {
next();
}
},
};
</script>
demo in codesandbox online by vue-router
This work well when user click goback and forward button in browser menu and Programmatic Navigation work as well. How can I implement the same requirement in react-router-dom?
I have tried this way based on deechris27's answer, but it does not work as vue-router.
my key code :
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { Modal } from 'antd'
import { ExclamationCircleOutlined } from '#ant-design/icons'
const { confirm } = Modal
function Confirm() {
return new Promise((resolve, reject) => {
confirm({
title: 'Do you want to save data?',
icon: <ExclamationCircleOutlined />,
onOk() {
resolve({ ok: true })
},
onCancel() {
reject(new Error('cancel'))
},
})
})
}
function Users() {
const http = () => {
console.log("I'll show up on submit")
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: true })
}, 2000)
})
}
const history = useHistory()
const [input, setInput] = useState('')
const changeInput = (event) => {
const { value } = event.currentTarget
setInput(value)
}
useEffect(() => {
return history.listen(async (location, action) => {
console.log(location, action)
if (action === 'POP') {
// const ok = window.confirm('do u want to submit form?')
// url has change when confirm modal show
try {
await Confirm()
// url has change when confirm modal Component show
// send http request
const res = await http()
console.log(res)
}catch (error) {
// http error or cancel modal
}
}
})
}, [history])
const handleSubmit = async (e) => {
console.log('**********')
e.preventDefault()
if (input) {
// const ok = window.confirm('Do u want submit?')
try {
const { ok } = await Confirm()
console.log(ok)
if (ok) {
console.log('send http')
const res = await http() // answer is yet false
console.log(res)
res.ok && history.push('/')
}
} catch (error) {
// http error or cancel modal
}
} else {
history.push('/')
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={input} onChange={changeInput} placeholder="input some text" />
<br />
<button type="submit">submit</button>
</form>
)
}
export default Users
When custom Confirm Component or window.confirm showed, the url has chenged, so this way can not stop navigate away. Prompt has the same issue.
You could do the same in React using window.confirm. Doing it the react way by using state value will have one problem; timing the 2 async actions: setState and your async function call based on the value of answer.
Example below:
App.js
import "./styles.css";
import { Switch, Route } from "react-router-dom";
import Test from "./Test";
import Leave from "./Leaving";
export default function App() {
return (
<div className="App">
<Switch>
<Route exact path="/prompt" component={Leave} />
</Switch>
<Test />
</div>
);
}
Test.js
import React, { useState } from "react";
import { Prompt, useHistory } from "react-router-dom";
function Test() {
const [answer, setIsAnswer] = useState(false);
const history = useHistory();
const http = () => {
console.log("I'll show up on submit");
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: true });
}, 200);
});
};
const handleSubmit = async (e) => {
console.log("**********");
e.preventDefault();
e.target.reset();
setIsAnswer(true); // setting state is async
history.push("/prompt");
answer && (await http()); // answer is yet false
};
return (
<div className="Test">
<h1>Hello Jack</h1>
<form onSubmit={(e) => handleSubmit(e)}>
<Prompt when={true} message={(_) => "Do you want submit form?"} /> // **when**'s value should ideally depend on answer value, harcoded for example purpose.
<button>Click</button>
</form>
</div>
);
}
export default Test;
Only when you click the second time, the React way of prompt is in action since setIsAnswer is async.
You could use the same window.confirm onSubmit.
const handleSubmit = async (e) => {
console.log("**********");
e.preventDefault();
e.target.reset();
const answer = window.confirm("Do you want submit form?");
answer && history.push("/prompt");
answer && (await http());
};
I created a working example for you here in codesandbox https://codesandbox.io/s/charming-dew-u32wx
In the above sandbox, handleSubmit is plain JS way. handleSubmit2 is React way(click twice). Remove the commented <Prompt .../> inside the form and change onSubmit function to handleSubmit then handleSubmit2 to see both in action.
Explanation:
Any react module or approach that will rely on the state value will not help with your scenario, where you make HTTP call after prompt confirmation. window.comfirm is blocking code, using state for answer is non-blocking. If it's just about prompting before navigating away then you could go with the above react or any other react approach suggested.
Update in response to comments:
You could prevent the browser back button navigation by adding the below code.
const history = useHistory();
useEffect(() => {
return () => {
if (history.action === "POP") {
return false;
}
};
}, [history]);
or in your Prompt itself through callback
<Prompt
when={(location, action) => {
if (action === "POP") {
//
}
}}
message={(_) => "Are you sure, you want to go back?"}
/>
I've updated the Leaving.js in the codesandbox with this browser back button scenario.
I believe the answer is to use a blocking flag. See this: https://reactrouter.com/web/example/preventing-transitions
Either you can create this using react-router APIs or use an already available package like React Navigation Prompt.
It uses react-router under the hood
It's the simplest example to prevent a user to change the route by using the react-router and react-router-dom packages.
import React from "react";
import { BrowserRouter, Route, NavLink } from "react-router-dom";
import { Prompt } from "react-router";
import "./App.css";
function App() {
const [dirty, setDirty] = React.useState(false);
return (
<BrowserRouter>
<div className="App">
<input onChange={() => setDirty(true)} /> {dirty ? "dirty" : "clean"}
<br />
<NavLink to="/test">Leave</NavLink>
<br />
<Route path="/test" component={() => "Has left"} />
<Prompt message="Are you sure you want to go to /test?" />
</div>
</BrowserRouter>
);
}
export default App;
You just need to install these two packages react-router and react-router-dom. This is the simplest logic which is built like if your form is dirty then you should prompt the user and prevent them from the leaving the page.

Retrieving a value from a child component back to the parent for a LIstGroup?

I've been trying to retrieve a value from the child component in react for an app I am putting together. I think I am making a very simple error here. I originally asked a related question here:
React read value of button clicked
Right now my code looks like this:
App.js
import React, { Component } from 'react';
import { Form, Button, ListGroup } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import Match from './Match'
const my_data = require('./data/test.json')
class App extends Component {
state = {
links: [],
selectedLink:null,
userLocation: {},
searchInput: "",
showMatches: false,
matches: [],
searchLink:[]
}
componentDidMount() {
fetch('https://data.cityofnewyork.us/resource/s4kf-3yrf.json')
.then(res=> res.json())
.then(res=>
//console.log(json)
this.setState({links:res})
);
}
handleInputChange = (event) => {
event.preventDefault()
this.setState({searchInput: event.target.value })
console.log(event.target.value)
}
handleSubmit = (event) => {
event.preventDefault()
this.displayMatches();
}
findMatches = (wordToMatch, my_obj) => {
return my_obj.filter(place => {
// here we need to figure out the matches
const regex = new RegExp(wordToMatch, 'gi');
//console.log(place.street_address.match(regex))
return place.street_address.match(regex)
});
}
displayMatches =() => {
const matchArray = this.findMatches(this.state.searchInput, this.state.links);
const newStateMatches = matchArray.map(place => {
console.log(place.street_address);
return place
});
this.setState({matches:newStateMatches})
this.setState({showMatches:true})
}
alertClicked =(event) => {
event.preventDefault()
//alert('you clicked an item in the group')
const data = event.target.value
console.log('clicked this data:', data)
this.setState({searchLink: event.target.value})
console.log(this.state.searchLink)
}
render() {
return (
<div>
<input
placeholder="Search for a Link Near you..."
onChange = {this.handleInputChange}
value = {this.state.searchInput}
/>
<Button onClick={this.handleSubmit}>
Search
</Button>
<ListGroup defaultActiveKey="#link1">
{
this.state.matches.map(match => {
return <Match
address={match.street_address}
alertClicked={this.alertClicked}
value = {this.state.searchLink}/>
})
}
</ListGroup>
</div>
);
}
}
export default App;
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ( props ) => {
return (
<ListGroup.Item
className="Matches"
action onClick={props.alertClicked}
value = {props.value}>
<p>{`${props.address}`}</p>
</ListGroup.Item>
)
};
export default match;
I am trying to access the value of the ListGroup Item when I click on it with this:
alertClicked =(event) => {
event.preventDefault()
//alert('you clicked an item in the group')
const data = event.target.value
console.log('clicked this data:', data)
this.setState({searchLink: event.target.value})
console.log(this.state.searchLink)
}
But can't seem to get it to work. Probably been staring at this way too long. Appreciate the help guys.
Match.js
import React from 'react';
import { ListGroup } from 'react-bootstrap';
const match = ({ alertClicked, address }) => {
return (
<ListGroup.Item
className="Matches"
action
// function expressions could cause this to rerender unnecessarily.
onClick={(address) => alertClicked(address)}>
<p>{`${address}`}</p>
</ListGroup.Item>
)
Other.js
alertClicked = address => {
event.preventDefault(); // not sure what event you're preventing
this.setState({searchLink: address});
}
If you're worried about the unnecessary rendering, you should look for another method of achieving this functionality with a more defined element/component.

Categories