jest enzyme routing how to test component with Route? - jestjs

I have an App component which contains main route definitions:
export default function App() {
return (
<div>
<MuiThemeProvider theme={createMuiTheme(theme)}>
<Route
path={`${URL_PREFIX}/:module?/:id?`}
render={() => (<div>
<MapContainer />
<NavigationBox />
<FeedbackMessage />
</div>)}
/>
<Switch>
<Route
exact
path={URL_PREFIX}
render={() => (
<Redirect to={`${URL_PREFIX}/restaurants`} />
)}
/>
</Switch>
</MuiThemeProvider>
</div>
);
}
I have the main file, where I have render function, which renders App on proper HTML node and wraps it everything what is needed:
const render = (messages) => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE
);
};
So far everything works.
I also tried to write the tests for App but this part doesn't work at work at all. It is my test file:
describe('<App />', () => {
describe('should render other components on proper route', () => {
let renderedComponent;
beforeEach(() => {
renderedComponent = shallow(
<MemoryRouter initialEntries={[URL_PREFIX]} initialIndex={0}><App /></MemoryRouter>
).dive().dive();
});
it('should render <MapContainer />', () => {
expect(renderedComponent.find(MapContainer).length).toBe(1);
});
it('should render <NavigationBox />', () => {
expect(renderedComponent.find(NavigationBox).length).toBe(1);
});
it('should render <FeedbackMessage />', () => {
expect(renderedComponent.find(FeedbackMessage).length).toBe(1);
});
});
});
If I add more .dive() I got an error:
TypeError: ShallowWrapper::dive() can not be called on Host Components
If there is only 2 dives or 1 dive it says that expected value (1) is different than received (0), so tests fail.
If I try with render or mount it complains, that store is expected but has value undefined.
Is there any way to test if on the chosen path my components are rendered?

Related

How should I pass data from react app.js to profile.js and set id in url?

Edited the post!!!
id coming trough login request, at the else branch at handleChangeId! it gets the correct id! i try to push at the top!
export function Login({handleChangeId}) {
const login = () => {
//node port egyezés szükséges
Axios.post('http://localhost:3001/login', {
LoginUsername: usernameLog,
LoginPassword: passwordLog,
}).then((response) => {
if (response.data.message) {
setLoginCorrect(response.data.message)
}
else {
handleChangeId(response.data[0].id);
navigate("/App");
}
});
};
}
than at App.js i try to get the id from the login Route, and push trough profile Route
let id = null;
function changeID(newId) {
id= newId;
}
ReactDOM.render(
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Login handleChangeId={changeID} />}
<Route exact path="/profile" element={<Profile id={id} />} />
</Routes>
</BrowserRouter>,
document.getElementById('root')
); />
at Profile.js, i also try to get the id this way, but the value=null every way that i tried! And this is what im looking for - how to read and set the id and get their personal datas, when the users hit profile on the menubar!
export function Profile({ id }) {
const [customers, setCustomers] = useState([]);
useEffect(() => {
Axios.get(`http://localhost:3001/profile?userId=${id}`)
.then((response) => {
if (response) {
setCustomers(response.data);
}
else {
alert("Data currently unavailable!")
}
});
}, []);
}
Issue
let id = null;
function changeID(newId) {
id= newId;
}
Doesn't work because id is declared each render cycle with a null value, and simply mutating it doesn't trigger a component rerender with the updated value to be passed to the Profile component.
Solution
Make id part of the component state so updating it triggers a rerender with the updated value closed over in scope.
Example:
const [id, setId] = React.useState(null);
...
const changeID = (newId) => {
setId(newId);
}
...
<Routes>
<Route path="/" element={<Login handleChangeId={changeID} />}
<Route
path="/profile"
element={<Profile id={id} />} // <-- updated id state value passed here
/>
</Routes>

My react app (login form) doesn't work like a spa page

I used CRA to create simple Login form. I've set up database with mongoose and built crud with node.
I don't think it has anything to do with the backend.
My intention with this little boiler plate was:
(not logged in) -> landing page shows 'Welcome' with Home, sign in menu.
(logged in) -> landing page shows 'Welcome, name' with Home, MyPage menu.
Down below is Login.js.
import React, { useState } from "react";
import { Link } from "react-router-dom";
import axios from "axios";
import "../App.css";
function Login() {
const [Email, setEmail] = useState("");
const [Password, setPassword] = useState("");
const [Error, setError] = useState("");
const onEmailHandler = (e) => {
setEmail(e.currentTarget.value);
};
const onPasswordHandler = (e) => {
setPassword(e.currentTarget.value);
};
const onSubmitHandler = (e) => {
e.preventDefault();
const body = {
email: Email,
password: Password,
};
axios
.post("/api/users/login", body)
.then((response) => {
if (!response.data.token) {
setError(response.data.error);
} else {
window.location.replace("/");
//props.history.push("/");
}
})
.catch((e) => {
console.log(e);
});
};
return (
<div>
<div>
<form className="login_form">
<input
type="email"
placeholder="Email"
onChange={onEmailHandler}
value={Email}
/>
<br />
<input
type="password"
placeholder="Password"
onChange={onPasswordHandler}
value={Password}
/>
<br />
<button onClick={onSubmitHandler}>Login</button>
</form>
</div>
<div
style={{
marginTop: 14,
fontSize: 15,
color: "red",
fontFamily: "Arial",
fontWeight: "lighter",
}}
>
{Error}
</div>
<div className="register_button">
<Link to="/register">Sign Up</Link>
</div>
</div>
);
}
export default Login;
As you can see, when you are signed in properly you are thrown to the landing page.
landing page looks like this.
import React, { useState } from "react";
import "../App.css";
import axios from "axios";
function Landing() {
const [Nickname, setNickname] = useState("");
axios.get("/api/users/authenticate").then((response) => {
if (response.data.name) {
setNickname(response.data.name);
}
});
return Nickname === "" ? (
<div className="welcome_msg">
<h4>Welcome!</h4>
</div>
) : (
<div className="welcome_msg">
<h4>Welcome, {Nickname}!</h4>
</div>
);
}
export default Landing;
And most importantly, App.js looks like down below.
import React, { useEffect, useState } from "react";
import { Route, BrowserRouter, Link } from "react-router-dom";
import "./App.css";
import axios from "axios";
import Landing from "./components/Landing";
import Login from "./components/Login";
import MyPage from "./components/MyPage";
import Register from "./components/Register";
function App() {
const [IsLoggedIn, setIsLoggedIn] = useState(false);
axios.get("/api/users/authenticate").then(
(response) => {
if (response.data.email) {
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
console.log(IsLoggedIn);
}
//[IsLoggedIn]
);
return IsLoggedIn ? (
<BrowserRouter>
<nav className="navigate">
<Link to="/">Home</Link>
<Link to="/mypage">Mypage</Link>
<hr />
</nav>
<Route exact path="/" component={Landing} />
<Route path="/login" component={Login} />
<Route path="/mypage" component={MyPage} />
<Route path="/register" component={Register} />
</BrowserRouter>
) : (
<BrowserRouter>
<nav className="navigate">
<Link to="/">Home</Link>
<Link to="/login">Sign in</Link>
<hr />
</nav>
<Route exact path="/" component={Landing} />
<Route path="/login" component={Login} />
<Route path="/mypage" component={MyPage} />
<Route path="/register" component={Register} />
</BrowserRouter>
);
}
export default App;
The Router api/user/authenticate returns json with user information(email, name, token).
It's not like there's an error to the app, but I think maybe it's re-rendered too many times? It's slow and doesn't work like a spa page. I've checked the network tab and there seems to be too many requests (mostly authentication) whenever i go to Home or Mypage.
Stay safe, stay away from virus and please help :(
That's because the submit handler must be passed to the form itself as an onSubmit method instead of the onClick of the button.
<form className="login_form" onSubmit={onSubmitHandler}>
...
</form>

Where to set up notifications/status updates for app?

I am implementing status updates/notifications, but I do not know where to make the calls from. Right now I am making them in my container app AsyncApp.js that holds all the navigation bars and my components(except login/logout etc...) the issue is that when ever I go to a new component my notifications start from fresh, which is wrong because I want it stay continuous throughout all pages.
AsyncApp.js
class AsyncApp extends Component {
constructor(props){
super(props)
this.startTimer = this.startTimer.bind(this)
this.handleEvent = this.handleEvent.bind(this)
this.handleClose = this.handleClose.bind(this)
this.state = {
redirect: false,
maxSessionInactivity: null,
showAlert: false,
sinceLastCheck: ''
}
}
async componentDidMount() {
this.show = null
let self = this
let messages;
const { dispatch } = this.props
await document.body.addEventListener("keypress", this.handleEvent);
await document.body.addEventListener("click", this.handleEvent);
await fetch('/api/getStatus').then(res => res.json()).then(function(res){
// if(!res.data.is_active){
// self.setState({redirect: true})
// }
console.log("IN GET STATUS ", res)
})
.catch(err => self.setState({redirect: true}))
await fetch('/api/getFirstNotification')
.then(res => res.json())
.then(function(res){
// if(res.status.errorOccured){
// self.setState({redirect: true})
// }
messages = res.data.messages
dispatch(updateMessages(res.data.messages))
self.setState({sinceLastCheck: res.data.since_last_check})
})
.catch(err => self.setState({redirect: true}))
//await fetch('/api/getStatus').then(res => res.json()).then(res => this.setState({maxSessionInactivity: res.data.session_inactivity_minutes - 1 * 1000}));
await this.startTimer()
await console.log("STATE J", this.state)
await this.interval(messages)
await this.notifications()
}
startTimer() {
this.firstTimer = setTimeout(function() {
this.setState({showAlert: true})
}.bind(this), 100000);
this.lastTimer = setTimeout(function() {
this.setState({redirect: true})
}.bind(this), 600000)
}
handleEvent(e){
console.log("event", e)
clearTimeout(this.firstTimer)
clearTimeout(this.lastTimer)
this.startTimer()
}
async interval(messages){
this.intervalStatus = await setInterval(async () => {
await this.notify(messages)
}, 15000)
};
async notifications(){
const { dispatch } = this.props
this.newNotifications = await setInterval( async () => {
let data = { since_last_checked : this.state.sinceLastCheck }
let res1 = await fetch('/api/getNotifications', {
method:'POST',
headers: {
'Content-type': 'application/json',
'accept': 'application/json'
},
body:JSON.stringify(data)
})
.then(res => res.json())
.catch(err => console.log(err))
console.log("NOTIFICATIONS NEXTTT", res1)
if(res1 === undefined || res1.data === undefined || res1.data === null){
this.setState({redirect: true})
}
if(res1 != undefined && res1.data != null) dispatch(updateMessages(res1.data.messages))
let res2 = await fetch('/api/getStatus')
.then(res => res.json())
.catch(err => console.log(err))
console.log("STATUSS", res2)
if(res2 === undefined || res2.data === undefined || res2.data === null || res2.data.is_active === 'N' || res2.data.status === 'closed'){
this.setState({redirect: true})
}
}, 5000)
}
handleClose(event){
this.setState({showAlert: false})
}
componentWillUnmount(){
console.log("componentWillUnmount!!!!")
clearInterval(this.newNotifications)
clearInterval(this.intervalStatus)
clearTimeout(this.firstTimer)
clearTimeout(this.lastTimer)
document.body.removeEventListener("keypress", this.handleEvent);
document.body.removeEventListener("click", this.handleEvent);
}
notify(arr){
if(arr === undefined) return null
if(typeof arr === 'string'){
return toast.success(`${arr}`)
}
if(arr.length < 4){
let messages = arr.map(message => toast.success(`${message.message_text}`))
return messages
} else {
return toast.success(`You have ${arr.length} new Notifications!`)
}
};
render() {
const { classes } = this.props
if (this.state.redirect) return <Redirect to="/logout" />
return (
<div>
<ToastContainer />
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.showAlert}
autoHideDuration={6000}
onClose={this.handleClose}
>
<MySnackbarContentWrapper
onClose={this.handleClose}
variant="warning"
message="Your session will expire in one minute!"
/>
</Snackbar>
<ThemeProvider theme={theme}>
<div className={classes.root}>
<CssBaseline />
<nav className={classes.drawer}>
<Hidden xsDown implementation="css">
<Navigator PaperProps={{ style: { width: drawerWidth } }} />
</Hidden>
</nav>
<div className={classes.appContent}>
<Header onDrawerToggle={this.handleDrawerToggle} />
<main className={classes.mainContent}>
<div>
<Switch>
<Route exact path="/EditContracts/:contractId/sections/:section" component={EditSection} />
<Route exact path="/EditContracts/:contractId" component={EditContract} />
<Route exact path="/EditUsers/:userId" component={EditUser} />
<Route exact path="/EditEndpoints/:epId" component={EditEndpoint} />
<Route exact path="/EditContracts/:contractId/addSection" component={CreateSection} />
<Route exact path="/Contracts/List" component={Contracts} />
<Route exact path="/Contracts/Create" component={CreateContract} />
<Route exact path="/Contracts/Import" component={ImportContract} />
<Route exact path="/Users/List" component={Users} />
<Route exact path="/Users/Create" component={CreateUser} />
<Route exact path="/Endpoints/Create" component={CreateEndpoint} />
<Route exact path="/Endpoints/List" component={Endpoints} />
<Route exact path="/Pug_Community" component={PugCommunity} />
<Redirect exact from="/Users" to="/Users/List" />
<Redirect exact from="/Endpoints" to="/Endpoints/List" />
<Redirect exact from="/Contracts" to="/Contracts/List" />
</Switch>
</div>
</main>
</div>
</div>
</ThemeProvider>
</div>
)
}
}
App.js
export default class App extends Component {
render() {
return (
<Switch>
<Route exact path="/signin" component={SignIn} />
<Route exact path="/changePassword" component={ChangePassword} />
<Route exact path="/logout" component={Logout} />
<Redirect exact from="/" to="/signin" />
<Route path="/" component={AsyncApp} />
</Switch>
)
}
}
Root.js
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
)
}
}
At a first glance, it looks like you are doing more work than you need to in your AsyncApp using timers. I can think of two possible solutions that could simplify your workflow:
Since you are already using redux, I would take your fetch() calls out of the AsyncApp component entirely and store them in async redux action creators using redux-thunk.
Set only one timer on AsyncApp, call your async action methods like updateNotifications() and updateStatus() every interval, and then check whether the returned data is new in a redux reducer.
Your reducer can then also be the place that determines whether notifications have been "read". If you have control over the server and database, you will want a server side attribute to also store whether notifications have been read so that a total cache refresh would not redisplay old notifications as new.
While the first suggestion will likely be more intuitive with your current app design, I would argue a more elegant solution would use some kind of websocket implementation. Subscribing to a socket channel using socket.io or PusherJS. Implementing this kind of change requires server changes too, so it may not be feasible for you, but it would remove the need for timers. Pusher, for example, can be configured to subscribe to a notification channel and kick off a redux action with the new data anytime a new notification is received. See: pusher-redux.
Here are some ideas for how to organize the code for the first suggestion:
AsyncApp.js
import { updateNotifications, updateStatus } from './actionCreators.js'
class AsyncApp extends Component {
// ... other necessary code
public interval: any;
public getData = () => {
const { dispatch } = this.props
dispatch(updateNotifications())
dispatch(updateStatus())
}
public componentWillMount(){
//Grab data when the component loads the first time
this.getData()
// Then set a single interval timer to handle both data updates.
this.interval = setInterval(()=>{
this.getData()
},30000)
}
public componentWillUnMount(){
// clear the interval when the component unmounts.
clearInterval(this.interval)
}
}
actionCreators.js
// action type constants
export const RECEIVE_NOTIFICATIONS = 'RECEIVE_NOTIFICATIONS'
function receiveNotifications(notifications){
return({
type: RECEIVE_NOTIFICATIONS,
notifications,
})
}
// Here's the part that uses thunk:
export function updateNotifications() {
return function(dispatch) {
return fetch(`api/getNotifications`)
.then(
response => response.json(),
error => console.log('An error occurred.', error)
)
.then(json =>
dispatch(receiveNotifications(json))
)
}
}
// ... use the same pattern for updateStatus()
reducers.js
import { RECEIVE_NOTIFICATIONS } from './actionCreators.js'
const notifications = (state=[], action) => {
switch(action.type){
case RECEIVE_NOTIFICATIONS:
/* Depending on the structure of your notifications object, you can filter
or loop through the list to determine whether or not any items in the
list are new. This is also where you can toast if there are N number of
new notifications. */
return action.notifications
default:
return state
}
}

ComponentDidMount infinite loop

first of all I'd like to say I have read about the subject and I still can't understand what's happening, so sorry in advance if it's a duplicate. component that fetches data from a server on componentDidMount, but somehow it enters an infinite loop. I am using react router & redux saga and the parent of said component is also connected to the redux store (I think this is the problem).
App.js render method:
render() {
console.log(this.props.isAuthenticated);
let routes = (
<Switch>
<Route path="/login" component={Login} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
);
if (this.props.isAuthenticated) {
routes = (
<Switch>
<Route path='/logout' component={Logout} />
<Route path='/data/people' component={People} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
)
}
return (
<div>
<Layout>
{routes}
</Layout>
</div>
);
}
Call to fetch data (Called in People.js):
componentDidMount() {
this.props.fetchPeople()
}
App.js redux connection:
const mapStateToProps = state => {
return {
isAuthenticated: state.login.token !== null,
username: state.login.username
};
};
const mapDispatchToProps = dispatch => {
return {
autoLogin: () => dispatch(actions.checkLoginState())
}
};`
People.js redux connection:
const mapStateToProps = state => {
return {
people: state.data.people,
loading: state.data.loading
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPeople: () => dispatch(actions.fetchPeople())
};
};
actions.fetchPeople calls this saga:
export function* fetchPeopleSaga () {
try {
yield put(actions.fetchPeopleStart());
const people = yield axios.get('/data/people');
yield put(actions.fetchPeopleSuccess(people));
} catch (error) {
yield put(actions.fetchPeopleFail(error));
}
};
And here's the relevant reducer:
const initialState = {
people: null,
error: null,
loading: false
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PEOPLE_START:
return {
...state,
loading: true
}
case actionTypes.FETCH_PEOPLE_SUCCESS:
return {
...state,
people: action.people,
loading: false
}
case actionTypes.FETCH_PEOPLE_FAIL:
return {
...state,
people: null,
error: action.error,
loading: false
}
default:
return state
}
};
As you can see they aren't even accessing the same props, so I don't understand why it enters infinite rendering.
Help would be much appreciated!
You can add another state loaded and set it to true in case of either FETCH_PEOPLE_SUCCESS or FETCH_PEOPLE_FAIL. Then, add a check for both loaded and loading before you fetch people data, something like:
if (!loaded && !loading) {
this.props.fetchPeople()
}
ComponentDidMount get called once unless you redirect to that page again.
Please check, you must be getting redirecting on that page/component again and again.

Cannot match dynamic routes like /recipes/:id/ingredients

I am using the npm package react-router-dom#4.0.0-beta.6 for my react app.
I am able to match routes like:
/trends
/news
/recipes
But I can't match routes like:
/recipes/:id/ingredients or /recipes/1234/ingredients
If I switch from /recipes/1234 to /recipes/1234/ingredients I only match the route /recipes/:id but not /recipes/:id/ingredients.
The app looks like this:
const factory = (name, path) => {
return (props) => {
const { children, match, routes } = props;
const { params } = match;
// convert the child routes from /recipes/:id/ingredients to /recipes/1234/ingredients
const others = routes.map(x => pathToRegexp.compile(x.path)(params));
return (
<div>
<h3>{name}</h3>
<ul>
{others.map(x =>
<Link key={x} to={x}>{x}</Link>
)}
</ul>
{children}
</div>
);
};
};
const rootPattern = '/recipes/:id';
const routes = [
{
path: rootPattern,
component: factory('Root', rootPattern),
routes: [
{
path: `${rootPattern}/ingredients`,
component: factory('Ingredients', `${rootPattern}/ingredients`),
},
{
path: `${rootPattern}/steps`,
component: factory('Steps', `${rootPattern}/steps`),
},
],
},
];
// wrap <Route> and use this everywhere instead, then when
// sub routes are added to any route it'll work
const RouteWithSubRoutes = (route) => (
<Route path={route.path} render={props => (
// pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes}/>
)}/>
);
const RouteConfigExample = () => (
<Router>
<div>
<ul>
<li><Link to="/recipes/1234">Sandwiches</Link></li>
</ul>
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} {...route}/>
))}
</div>
</Router>
);
To be fair I don't really understand why my app doesn't match the dynamic routes.
You can find a complete example at https://github.com/jbrieske/react-router-config.

Resources