My application use react-router to manage user navigation and now I need add unit tests bu I'm stuck on how to change route.
My <App /> is (simplified):
class AppUser extends Component {
render() {
return (
<div className="layout">
{this.props.children}
</div>
);
}
}
class Initial extends Component {
render() {
return (
<div className="initial" />
);
}
}
export default class App extends Component {
render() {
let masterPageBase = (props) => (
<AppUser>
{props.children}
</AppUser>
);
let notFound = () => (
<div>
<h1>Not found!</h1>
</div>
);
<Router history={browserHistory}>
<Route path="/" component={masterPageBase}>
<IndexRoute component={Initial} />
<Route path="*" component={notFound} />
</Route>
</Router>
}
}
And my test is:
describe('<App />', () => {
it('user', () => {
const wrapper = shallow(<App />);
// FIXME This fails
expect(wrapper.find('AppUser').length).toEqual(1);
});
});
How can I change the route so that will be an existing child.
This is how you can fake a route in your tests:
There is a module called history which you can use to create a fake browser history in your tests. In order to apply it, you need to make your router parametric in the history it uses, like this:
export default class App extends Component {
render() {
createRouter(browserHistory);
}
}
export function createRouter(history) {
let masterPageBase = (props) => (
<AppUser>
{props.children}
</AppUser>
);
let notFound = () => (
<div>
<h1>Not found!</h1>
</div>
);
return <Router history={history}>
<Route path="/" component={masterPageBase}>
<IndexRoute component={Initial} />
<Route path="*" component={notFound} />
</Route>
</Router>
}
In your tests, you can then use the history module to create a fake history:
import { useRouterHistory } from "react-router";
import createMemoryHistory from "history/lib/createMemoryHistory";
function navigatingTo(path) {
return mount(createRouter(useRouterHistory(createMemoryHistory)(path)));
}
describe('Router', () => {
it('user', () => {
expect(navigatingTo("/").find('AppUser').length).toEqual(1);
});
});
PS: If you run these tests in node.js then you need to make use of jsdom in order for enzyme's mount() to work.
Related
I'm testing if my components render with Redux successfully with React Testing Library. I'm having trouble having my utility component to pass the renderWithRedux test. This is my App component.
function App() {
return (
<>
<Router>
<NavBar />
<div className="container">
<Switch>
<Route exact path='/' component={Home}/>
<AuthRoute exact path='/login' component={Login} />
<AuthRoute exact path='/signup' component={Signup} />
<Route exact path='/users/:handle' component={UserProfile} />
<Route exact path='/users/:handle/post/:postId' component={UserProfile} />
</Switch>
</div>
</Router>
</>
);
}
Here is my AuthRoute utility component.
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
// if authenticated, redirect to homepage, otherwise redirect to signup or login
<Route
{...rest}
render={(props) =>
authenticated === true ? <Redirect to='/' /> : <Component {...props} />
}
/>
);
AuthRoute.test.js
const renderWithRedux = () => render(
<Provider store={myStore}>
<AuthRoute />
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(<AuthRoute />);
});
I've attempted the solutions from Invariant failed: You should not use <Route> outside a <Router>, but to no avail. I appreciate any help, thank you.
Render the component under test into a router
import { MemoryRouter } from 'react-router-dom';
const renderWithRedux = ({ children }) => render(
<Provider store={myStore}>
{children}
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(
<MemoryRouter>
<AuthRoute />
</MemoryRouter>
);
});
Just like the Provider to wrap redux things you have to wrap your components with routes using MemoryRouter for the tests.
import { MemoryRouter } from 'react-router';
Basically, you have two wrapper elements. It should go something like this, for example, renderWithReduxWrapp => renderWithRouter => YourTestingComponent.
I had a similar issue when trying to test Button render (which has a Link) depending on props, and was able to solve it by creating some helper functions.
Here is the example:
This is the main component, UserCard.js, which renders user data from redux, and only shows a button if withButton props is passed.
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
const CardComponent = ({ withButton }) => {
const userInfo = useSelector((state) => getUserSelector(state));
return (
<div>
<div>{userInfo}</div>
{withButton && (
<Link to="/settings" className="button-link">
<Button block>EDIT CONTACT INFO</Button>
</Link>
)}
</div>
);
};
export default CardComponent;
This is a CardComponent.test.js file.
First, you need to add these lines of code
const ReduxWrapper = ({ children }) => {
<Provider store={store}>{children} </Provider>;
}
const AppWrapper = ({ children }) => (
<BrowserRouter>
<ReduxWrapper>{children}</ReduxWrapper>
</BrowserRouter>
);
const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(ui, { wrapper: AppWrapper });
};
After that, you need to start your test with renderWithRouter instead of just render method.
it('should render settings button if prop withButton is passed', () => {
renderWithRouter(<CardComponent withButton />, { wrapper: ReduxWrapper });
// apply you code here. I only needed to check if the button is renederd or not.
const settingsButton = screen.queryByText(/edit contact info/i);
expect(settingsButton).toBeInTheDocument();
});
I am having trouble with the Route path <Route path="customers/:id" render={(props) => <CusDataForm {...props}/>}/> in the code below:
import CusDataCtrl from './cusdata/CusDataCtrl'
import CusDataForm from './cusdata/CusDataForm'
class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/customers" component={CusDataCtrl} />
<Route path="customers/:id" render={(props) => <CusDataForm {...props}/>}/>
</Switch>
</BrowserRouter>
);
}
}
export default App;
if I use <Route exact path="/customers/:id" component={CusDataForm} /> the component does render correctly; however, I need to pass some props down to this component.
My calling component is defined like so:
class CusDataGrid extends Component {
constructor(props) {
super(props)
this.state = {data: []}
}
componentDidMount() {
let me = this;
dbFetch("customers",data => me.setState({data:data}));
}
callEdit = e => {
let recid = e.target.getAttribute("data")
this.props.history.push("/customers/"+recid);
}
render() {
const rows = this.state.data.map((row, ndx) => {
return (
<div key={ndx}><button data={row.recordid} className="waves-effect waves-light btn-small" onClick={this.callEdit}>Edit</button></div>
);
});
return (
<div id="cusdata"><div className="data-scrollable">{rows}</div></div>
);
}
};
export default CusDataGrid;
and my target component is:
class CusDataForm extends Component{
componentDidMount = () =>{
this.setState({id: this.props.id ? this.props.id : ""});
}
render(){
return(<div>HELLO</div>)
}
}
export default CusDataForm;
Please let me know what I am doing incorrectly. Thanks!
you can use hook useParams for it
<Switch>
<Route path="/:id" children={<Child />} />
</Switch>
function Child() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
let { id } = useParams();
return (
<div>
<h3>ID: {id}</h3>
</div>
);
}
official documentation
My problem is when I change a state inside a redux store and based on this state I mount or unmount a component. The Code looks like this:
class Main extends Component {
render() {
const { dropdownState } = this.props;
return (
<div>
<SecondHeadBar />
<div className="main">
<Switch>
<Route exact path='/' component={withRouter(WebsiteIndex)}/>
<Route path='/track/:trackid' component={withRouter(MssTrack)}/>
<Route path='/album/:albumid' component={withRouter(Container.AlbumContainer)}/>
<Route path='/profile/:userName' component={withRouter(MssUser)}/>
<Route path='/upload/:albumid' component={withRouter(MssUploadTemplate)}/>
<Route path='/upload' component={withRouter(MssUploadTemplate)}/>
<Route path='/admin' component={withRouter(ControlCenter)}/>
<Route path='/kategorie' component={withRouter(Category)} exact/>
<Route path='/kategorie/:catName' component={withRouter(Folder)}/>
<Route path='/notFound' component={withRouter(NotFound)}/>
<Route path='/meine-eintraege' component={withRouter(Container.MyEntriesContainer)}/>
</Switch>
</div>
{dropdownState ? <DownloadDropdown /> : ''}
</div>
);
}
}
function mapStateToProps(state) {
return {
dropdownState: state.collection.dropdownState
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Main);
Whenever the prop dropdownState changes. And the Component DownloadDropdown gets mounted then everything in the Main Component gets rerendered. So the content flashes.
Simplest solution would be to have <DownloadDropdown /> be a container component that is connected to Redux and will always stay mounted although not visible. Then you can utilize a HOC or something that's always mounted and visible (like <SecondHeadBar />) and have it connected to a Redux action creator that toggles DownloadDropdown's visiblity. In other words, isolate Redux to two components, instead of over your entire route tree.
Working example: https://codesandbox.io/s/yw4m7yz8r1 (navigate around the routes and click the 'Download Schedule' link at the top!)
I'm not sure how you are triggering the mount/unmount, but let's stay it's being toggled by a button:
SecondHeadBar.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { handleDropdown } from '../actions';
class SecondHeadBar extends Component {
state = {...}
componentDidMount = () => { ... }
render = () => (
<div>
...
<button onClick={this.props.handleDropdown}>Toggle Dropdown</button>
...
</div>
)
}
export default connect(null, { handleDropdown })(SecondHeadBar)
DownloadDropdown.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class DownloadDropdown extends Component {
state = { ... }
componentDidMount = () => { ... }
render = () => (
this.props.isVisible
? <div>I'm visible!</div>
: null
)
}
export default connect(state => ({ isVisible: state.dropdown }))(DownloadDropdown)
actions.js
import { TOGGLE_DROPDOWN } from '../types'
export const handleDropdown = () => ({
type: TOGGLE_DROPDOWN
})
reducers.js
import { TOGGLE_DOWN } from '../types';
...
const dropdownReducer = (state=false, { type, payload }) => {
switch(type) {
case TOGGLE_DROPDOWN: return !state
default: return state
}
}
export default = combineReducer({
...
dropdown: dropdownReducer
...
})
routes.js
const Main = () => (
<div>
<SecondHeadBar />
<div className="main">
<Switch>
<Route exact path='/' component={withRouter(WebsiteIndex)}/>
<Route path='/track/:trackid' component={withRouter(MssTrack)}/>
<Route path='/album/:albumid' component={withRouter(Container.AlbumContainer)}/>
<Route path='/profile/:userName' component={withRouter(MssUser)}/>
<Route path='/upload/:albumid' component={withRouter(MssUploadTemplate)}/>
<Route path='/upload' component={withRouter(MssUploadTemplate)}/>
<Route path='/admin' component={withRouter(ControlCenter)}/>
<Route path='/kategorie' component={withRouter(Category)} exact/>
<Route path='/kategorie/:catName' component={withRouter(Folder)}/>
<Route path='/notFound' component={withRouter(NotFound)}/>
<Route path='/meine-eintraege' component={withRouter(Container.MyEntriesContainer)}/>
</Switch>
</div>
<DownloadDropdown/>
</div>
);
export default Main;
Now, when the user clicks the "Toggle Dropdown" button in <SecondHeadBar/>, it'll update <DownloadDropdown/>'s visibility without affecting your route tree.
I think you can use this lifecycle methods to check.
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.name !== prevState.name) {
return { name: nextProps.name};
}
}
or for older version check in componentwillreceiveProps and stops re render.
My app currently has three components, User to view a person's profile, Self for a user to view their dashboard, notifications, and settings and a login page.
Both User and Self share common components Nav and Side, where User would pass the self object and call the fetchUser action from redux to Nav and Side, while Self would pass the user and self object along with calling the fetchSelf action.
User.js
class User extends React.Component {
componentDidMount() {
this.props.fetchUser(this.props.username);
}
render() {
const { page, self, user } = this.props
return (
<main>
<Nav
self={self}
/>
<Side
page={page} user={user}
/>
<div>
.....
</div>
</main>
)
}
}
const mapStateToProps = state => ({
page: state.store.page,
self: state.store.self
});
export default connect(mapStateToProps, {fetchUser})(User);
Self.js
class Self extends React.Component {
componentDidMount() {
this.props.fetchSelf();
}
render() {
const { page, self } = this.props
return (
<main>
<Nav
self={self}
/>
<Side
page={page} self={self}
/>
{
tab === 'Dashboard'
? <Dashboard />
: tab === 'Notifications'
? <Notifications />
: tab === 'Settings'
? <Settings />
: null
}
</main>
)
}
}
const mapStateToProps = state => ({
page: state.store.page,
self: state.store.self
});
export default connect(mapStateToProps, {fetchSelf})(Self);
Login.js
class Login extends React.Component {
.....
handleChange = event => {
.....
}
render() {
return (
<div id="login">
.....
</div>
)
}
Side.js
const Side = (props) => {
const { page, self, user } = props;
return (
<aside>
{
page === 'user'
? <div>
<img src={'img/' + user.profile.avatar} alt={`${user.username}'s avatar`} />
</div>
: <div>
<img src={'img/' + self.profile.avatar} alt={`${self.username}'s avatar`} />
<div>
}
</aside>
)
}
What I'd like to do here is instead of using react-router like this
<BrowserRouter>
<Switch>
<Route path="/login" exact={true} component={Login} />
<Route path="/self" exact={true} component={Self} />
<Route path="/:username" component={User} />
</Switch>
</BrowserRouter>
I'd want to be able to do something like this instead
const LayoutForLoginAndSignup = (props) => {
return (
<div class="loginOrSignUp">
<ComponentToBePassedIn />
</div>
)
}
class LayoutWithNavAndSide extends React.Component {
componentDidMount() {
this.props.fetchSelf();
// this.props.fetchUser('someusername')
}
render() {
return (
<main>
<Nav self={this.props.self} />
<Side page={this.props.page} self={this.props.self} user={this.props.user} />
{Content of component that was passed in}
</main>
)
}
}
const mapStateToProps = state => ({
page: state.store.page,
self: state.store.self,
user: state.store.user
});
export default connect(mapStateToProps, {fetchUser, fetchSelf})(LayoutWithNavAndSide);
<BrowserRouter>
<Switch>
<LayoutForLoginAndSignup path="/login" exact={true} component={Login} />
<LayoutWithNavAndSide path='/self' component={Self} />
<LayoutWithNavAndSide path="/:username" component={User} />
</Switch>
</BrowserRouter>
Here's where I get confused as I'm still new to react/redux/react router, how do I get the component of User or Self to show up in the layout? how do I get it to call fetchUser (on componentDidMount) only if someone is accessing /someuser vice versa with fetchSelf only when they goto the /self route? is it possible to do the layout as a function rather than a class?
Create the component you will make a route for that contains both layouts and a condition.
const Layout = (props) => (
{props.layout ? <SelfLayout /> : <UserLayout />}
)
Create two layouts.
const SelfLayout = () => (
<div> Self Layout </div>
)
const UserLayout = () => )
<div> User Layout </div>
)
Create your route.
<Route path={`/layout/${this.state.somelayout}`} render={() => <Layout
layout={this.state.somelayout}/>} />
this.state.somelayout should be the conditional that decides which layout we are on and you can tailor it to the needs of your app this is just a guideline.
I have the following ProductThumbnail component where as i click on the link it updates the URL and changes the route to redirect toward ProductDesc component. It generates the URL /productDescRedux/itemId properly.
const ProductThumbnail = (props) => {
const itemId = props.product
return(
<div>
<Link to={`/productDescRedux/${itemId.id}`}>
<div>
<h1>{props.product.headline}</h1>
<img src={props.product.images[0].imagesUrls.entry[1].url} alt="Thumbnail small pic"/>
<p></p>
</div>
</Link>
</div>
)
}
ProductThumbnail.propTypes = {
product: React.PropTypes.object
};
export default ProductThumbnail;
However despite URL changes, it does not call the component ProductDesc and i have to reload the page to display the component ProductDesc. Below the routes and the component ProductDesc
const Routes = function(){
return(
<Provider store={store}>
<Router history={ hashHistory }>
<Route path="/" component={ Container }>
<IndexRoute component={ Home } />
<Route path="/productredux" component={ App } >
<IndexRoute component={ ProductsGrid }/>
<Route path="/productDescRedux/:id" component={ ProductDesc } />
</Route>
<Route path="*" component={ NotFound } />
</Route>
</Router>
</Provider>
)
}
export default Routes;
const ProductDesc = ()=>({
render(){
return (
<div>
<h1>hello</h1>
<p>Yeah</p>
</div>
)
}
})
And here for completion the App component which uses connect() as well as the Main component
function mapStateToProps(state){
return {
products:state.products
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actionCreators,dispatch);
}
const App = connect(mapStateToProps,mapDispatchToProps)(Main);
export default App;
//in a different file
const Main = (props) => ({
render(){
return (
<div>
{React.cloneElement(props.children, this.props)}
</div>
)
}
})
export default Main;
So I don't see why when changing the URL , routing is not calling the component ProductDesc. any insight ?
Syntax issue on the Main component which is the parent of all. I needed
{React.cloneElement(this.props.children, this.props)}
instead of
{React.cloneElement(props.children, this.props)}
Ref issue