Testing onEnter in react-redux-router - reactjs

I've got an onEnter defined in my routes container and I'm trying to figure out how to write some tests for this;
export default class Root extends React.Component<IRootProps, void> {
store: Redux.Store<IAppState> = configureStore();
history: ReactRouterReduxHistory = syncHistoryWithStore(hashHistory, this.store);
checkAuth = (nextState: RouterState, replace: RedirectFunction): void => {
console.log(this.store.getState().authToken);
if (nextState.location.pathname !== '/login') {
if (this.store.getState().authToken) {
if (nextState.location.state && nextState.location.pathname) {
replace(nextState.location.pathname);
}
} else {
replace('/login');
}
}
}
render() {
return (
<Provider store={this.store}>
<div>
<Router history={this.history}>
<Route path='/login' component={Login}/>
<Route onEnter={this.checkAuth} path='/' component={App}>
<IndexRoute component={Counter}/>
</Route>
</Router>
<DevTools />
</div>
</Provider>
);
}
}
Writing a test to mount the component was easy enough;
function setup() {
const middlewares: any[] = [];
const mockStore = configureStore(middlewares);
const initialState = {};
const store:any = mockStore(initialState);
const component = mount(<Root store={store as Store<IAppState>} />);
return {
component: component,
history: history
};
}
describe('Root component', () => {
it('should create the root component', () => {
const {component, history} = setup();
expect(component.exists()).toBeTruthy();
});
});
But I'm at a loss for how to actually test that checkAuth is invoked and actually performs the replace call correctly (and renders the right thing).
I'm sure this is trivial but google fails me so far.. Any examples/pointers would be appreciated.

You can create a separate file with your onEnter callback and test it there. I am calling such files guards. Your root guard will look for example like this:
export default (store: Redux.Store<IAppState>) => (nextState: RouterState, replace: RedirectFunction): void => {
if (nextState.location.pathname !== '/login') {
if (store.getState().authToken) {
if (nextState.location.state && nextState.location.pathname) {
replace(nextState.location.pathname);
}
} else {
replace('/login');
}
}
}
and the route:
<Route onEnter={rootGuard(this.store)} path='/' component={App}>

Related

How to test component that depend on config set on app.js?

I'm trying to run a test that checks if the history.goBack has been called, by using jest.fn. I have set my routes in app.js with an outer FirebaseConfigProvider
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<FirebaseConfigProvider>
<AutomaticRedirectContextProvider>
<Routes>
<Route exact path={PAGES.HOME} component={HomePage} />
<Route path={PAGES.CONFIRMATION} component={ConfirmationPage} />
</Routes>
</AutomaticRedirectContextProvider>
</FirebaseConfigProvider>
</ThemeProvider>
);
}
export default App;
And I'm using the createMemoryHistory() to set initialEntries with pathname and state, however the test seems to ignore the configuration from FirebaseConfigProvider, set in app.js.
confirm.js
export default function ConfirmationPage() {
const handleGoBack = () => {
// this line works when running the app, but breaks when running the test
firebase.analytics().logEvent('click_back_button', firebaseAnalyticsData);
history.goBack();
};
return (
<div>...</div>
);
}
confirm.test.js
import { mockPerson } from '../mocks/person';
const mockHistoryGoBack = jest.fn();
async function setupPage() {
await wait(async () => {
const history = createMemoryHistory({initialEntries: [
{
pathname: PAGES.CONFIRMATION.replace(
':id',
1
),
state: mockPerson
}
]});
history.goBack = mockHistoryGoBack;
render(
<Router history={history}>
<ConfirmationPage />
</Router>
);
});
}
describe('View/Pages/Confirmation', () => {
it('calls history.goBack when back button is clicked', async () => {
await setupPage();
const backButton = screen.getByTestId(HISTORY_BACK_ID);
fireEvent.click(backButton);
expect(mockHistoryGoBack).toHaveBeenCalled();
});
}
I have also tried with but got the same results. Error "TypeError: _firebase.default.analytics is not a function" on this line from confirm.js: firebase.analytics().logEvent('click_back_button', firebaseAnalyticsData);
What am I missing?

Problem when dynamically registering routes in an application with microfrontends concept

I have an Typescript + Redux (with RTK) application using the microfrontends concept. All the steps for the construction came from this tutorial: Microfrontends tutorial.
The main component is Microfrontend.tsx (omitted imports):
interface Manifest {
files: {
'main.js': string
'main.js.map': string
'index.html': string
}
entrypoints: string[]
}
const MicroFrontend = ({
name,
host,
module
}: {
name: string
host: string | undefined
module: string
}) => {
const history = useHistory()
useEffect(() => {
const renderMicroFrontend = () => {
// #ts-ignore
window[`render${name}`] && window[`render${name}`](`${name}-container`, history)
}
if (document.getElementById(name)) {
renderMicroFrontend()
return
}
const manifestUrl = `${
isDevProfile ? host : ''
}/${module}/view/asset-manifest.json`
fetch(manifestUrl)
.then(res => res.json())
.then((manifest: Manifest) => {
const script = document.createElement('script')
script.id = name
script.crossOrigin = ''
script.src = `${host}${manifest.files['main.js']}`
script.onload = () => {
renderMicroFrontend()
}
document.head.appendChild(script)
})
return () => {
// #ts-ignore
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`)
}
})
return (
<main id={`${name}-container`} style={{ height: '100%' }} />
)
}
MicroFrontend.defaultProps = {
document,
window
}
export default MicroFrontend
I'm trying to render the routes of the child components in a dynamic way, however, when I do this, I have a very strange effect: Bug.
The code snippet that generates this effect is this (omitted imports):
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
{loadWithSuccess ? avaibleModuleLinks?.map(
(subMenuPath: SubMenuPath | undefined, index: number) => {
const subMenuPathKey = subMenuPath ? subMenuPath.key : ''
let micro = () => (
<MicroFrontend
module={subMenuPathKey}
host="127.0.0.1"
name={subMenuPath ? subMenuPath.key.charAt(0).toUpperCase() : ''}
/>
)
return (
<Route
key={index}
path={`/dfe/view/${subMenuPathKey}`}
component={micro}
/>
)
}
): <></>}
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
Only when I don't render routes dynamically do I have the desired effect: desired behavior
The code snippet that generates this effect is this (omitted imports):
const ModuleNfe = () => (
<MicroFrontend host="127.0.0.1" name="Nfe" module="nfe" />
)
const App = () => {
const dispatch = useAppDispatch()
const { loadWithSuccess } = useSelector(moduleSelectors)
const avaibleModuleLinks = useSelector(avaibleModuleLinksWhitoutHome)
useEffect(() => {
dispatch(fetchAvaibleModules()).then(response =>
dispatch(fetchAvaibleModuleLinks(response.payload as string[]))
)
}, [dispatch])
return (
<BrowserRouter>
<Template>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/dfe/view/nfe" component={ModuleNfe} />
</Switch>
</Template>
</BrowserRouter>
)
}
export default App
As you may have noticed, the desired behavior is for my page to be rendered inside the Template component. But for some reason, this is not the case.

react-router-dom with react hooks Does not redirect to correct route

I am new to react and I am sure there is an explanation I do not understand, but I cannot find an answer...
I Want to app if user is logged in,so I followed some guide and created HOC(PrivateRoute) but for some reason I keep ending up in the login page of the app,
Even if localStorage contains a valid JWT and I am asking for the specific path(/wallet/portfolio), it is still redirecting me to login page,
can someone please help me understand the issue(I am using typescript but I am sure it is unrelated to the issue)
My Main Component(App.tsx)
//imports...
export const App = () => {
const[jWTContext, setJWTContext] = useState(jWTContextInitialState);
const[isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
if (!isLoggedIn) {
jWTContext.isValid().then(loggedIn => {
setIsLoggedIn(loggedIn);
}
).catch(() => {setIsLoggedIn(false)});
}
});
return (
<JWTContext.Provider value={jWTContext}>
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/wallet/portfolio" component={AppPortfolio} isSignedIn={isLoggedIn}/>
<Route exact path="/wallet/login" component={AppLogin} />
<Route exact path="/wallet/register" component={AppRegister} />
<Redirect to={isLoggedIn ? "/wallet/portfolio" : "/wallet/login"}/>
</Switch>
</BrowserRouter>
</JWTContext.Provider>
);
};
PrivateRoute.tsx(copied from a guide on the web...)
import * as React from 'react';
import {Route, Redirect, RouteProps} from 'react-router-dom';
interface PrivateRouteProps extends RouteProps {
component: any;
isSignedIn: boolean;
}
const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, isSignedIn, ...rest } = props;
return (
<Route
{...rest}
render={(routeProps) =>
isSignedIn ? (
<Component {...routeProps} />
) : (
<Redirect
to={{
pathname: '/wallet/login',
state: { from: routeProps.location }
}}
/>
)
}
/>
);
};
export default PrivateRoute;
JWTContext.tsx(My Context provider):
import React from 'react';
import {JWTBody, JWTHeader, JWTToken} from "../interfaces/JWTToken"
export interface JWTContext {
jWTToken: JWTToken | null;
setJWTToken(jWTToken: JWTToken | null): void;
isValid(): Promise<boolean>;
updateFromLocalStorage(): (JWTToken | null);
}
export const jWTContextInitialState : JWTContext = {
jWTToken: initFromLocalStorage(),
setJWTToken: (jWTToken) => {
jWTContextInitialState.jWTToken = jWTToken;
},
isValid: async() => {
if (jWTContextInitialState.jWTToken) {
let response = await fetch("/auth/tokenvalid",{
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": JSON.stringify(jWTContextInitialState.jWTToken)}});
let res = await response.json();
if (res.code === 200)
return true;
}
return false;
},
updateFromLocalStorage: () => {
return initFromLocalStorage()
}
};
function initFromLocalStorage() {
let localStorageToken = localStorage.getItem("velonaJWT");
if (localStorageToken) {
return parseJwt(localStorageToken);
}
return null;
}
function parseJwt (token: string): JWTToken {
token = token.replace("Bearer ", "");
let tokenArray = token.split('.');
let header = tokenArray[0];
header = header.replace('-', '+').replace('_', '/');
let bufferHeader = Buffer.from(header, "base64");
let dataHeader: JWTHeader = JSON.parse(bufferHeader.toString());
let body = tokenArray[1];
body = body.replace('-', '+').replace('_', '/');
let bufferBody = Buffer.from(body, "base64");
let dataBody: JWTBody = JSON.parse(bufferBody.toString());
return ({
header: dataHeader,
body: dataBody,
sig: tokenArray[2]
});
//return JWTToken(JSON.parse(bufferHeader.toString()), JSON.parse(bufferBody.toString()), tokenArray[2])
}
export const JWTContext = React.createContext(jWTContextInitialState);
and finally the (JWTToken.tsx -> just for interfaces)
// represent velona WJT token
export interface JWTToken {
header: JWTHeader,
body: JWTBody,
sig: string,
}
export interface JWTHeader {
typ: string,
alg: string
}
export interface JWTBody {
sub: number,
jti: string,
authorities: Authorities[],
iat: number,
nbf: number,
exp: number,
environment: string
}
export enum Authorities {
ROLE_USER,
ROLE_TRUSTEE,
ROLE_EMPLOYEE,
ROLE_ADMIN
}
#Tholle helped me understand the issue,
because at first the user is not authenticated(until JWT is authed by the server -> async) we need to add the following in the react dom Switch:
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/wallet/portfolio" component={AppPortfolio} isSignedIn={isLoggedIn}/>
/*all other private routes here*/
{ isLoggedIn ? <Redirect to="/wallet/portfolio"/> : "" }
<Route exact path="/wallet/login" component={AppLogin} />
<Route exact path="/wallet/register" component={AppRegister} />
<Redirect to={isLoggedIn ? "/wallet/portfolio" : "/wallet/login"}/>
</Switch>
</BrowserRouter>
of course the downside is that if user is logged in he will not be able to go to any public page(like registration), but I can live with that...
thanks #Tholle

this.props.history.push not re-rendering react component

In my component I use this.props.history.push(pathname:.. search:..) to rerender the component and fetch new data form a third party service. When I first call the page it renders. But when I call history push inside the component the URL updates correctly BUT the component doesn't rerender. I read a lot but couldn't get it working. Any ideas?
I'm using react router v4
//index.js
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login}/>
<Route path="/" component={Main}/>
</Switch>
</BrowserRouter>
</Provider>
//Main.js
//PropsRoute is used to push props to logs component so I can use them when fetching new data
const PropsRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={props => <Component {...props} />}/>
);
};
class Main extends Component {
render() {
return (
<div className="app">
<NavigationBar/>
<div className="app-body">
<SideBar/>
<Switch>
<PropsRoute path="/logs" component={Log}/> //this component is not rerendering
<Route path="/reports" component={Reports}/>
<Route path="/gen" component={Dashboard}/>
<Redirect from="/" to="/gen"/>
</Switch>
</div>
</div>
)
}
}
export default Main;
//inside 'Log' component I call
import React, {Component} from 'react';
import {getSystemLogs} from "../api";
import {Link} from 'react-router-dom';
import _ from "lodash";
import queryString from 'query-string';
let _isMounted;
class Log extends Component {
constructor(props) {
super(props);
//check if query params are defined. If not re render component with query params
let queryParams = queryString.parse(props.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
this.props.history.push({
pathname: '/logs',
search: `?page=1&pageSize=25&type=3&application=fdce4427fc9b49e0bbde1f9dc090cfb9`
});
}
this.state = {
logs: {},
pageCount: 0,
application: [
{
name: 'internal',
id: '...'
}
],
types: [
{
name: 'Info',
id: 3
}
],
paginationPage: queryParams.page - 1,
request: {
page: queryParams.page === undefined ? 1 : queryParams.page,
type: queryParams.type === undefined ? 3 : queryParams.type,
pageSize: queryParams.pageSize === undefined ? 25 : queryParams.pageSize,
application: queryParams.application === undefined ? 'fdce4427fc9b49e0bbde1f9dc090cfb9' : queryParams.application
}
};
this.onInputChange = this.onInputChange.bind(this);
}
componentDidMount() {
_isMounted = true;
this.getLogs(this.state.request);
}
componentWillUnmount() {
_isMounted = false;
}
getLogs(request) {
getSystemLogs(request)
.then((response) => {
if (_isMounted) {
this.setState({
logs: response.data.Data,
pageCount: (response.data.TotalCount / this.state.request.pageSize)
});
}
});
}
applyFilter = () => {
//reset page to 1 when filter changes
console.log('apply filter');
this.setState({
request: {
...this.state.request,
page: 1
}
}, () => {
this.props.history.push({
pathname: '/logs',
search: `?page=${this.state.request.page}&pageSize=${this.state.request.pageSize}&type=${this.state.request.type}&application=${this.state.request.application}`
});
});
};
onInputChange = () => (event) => {
const {request} = this.state; //create copy of current object
request[event.target.name] = event.target.value; //update object
this.setState({request}); //set object to new object
};
render() {
let logs = _.map(this.state.logs, log => {
return (
<div className="bg-white rounded shadow mb-2" key={log.id}>
...
</div>
);
});
return (
<main className="main">
...
</main>
);
}
}
export default Log;
Reactjs don't re-run the constructor method when just props or state change, he call the constructor when you first call your component.
You should use componentDidUpdate and do your fetch if your nextProps.location.pathname is different than your this.props.location.pathname (react-router location)
I had this same issue with a functional component and I solved it using the hook useEffect with the props.location as a dependency.
import React, { useEffect } from 'react';
const myComponent = () => {
useEffect(() => {
// fetch your data when the props.location changes
}, [props.location]);
}
This will call useEffect every time that props.location changes so you can fetch your data. It acts like a componentDidMountand componentDidUpdate.
what about create a container component/provider with getderivedstatefromprops lifecycle method, its more react-look:
class ContainerComp extends Component {
state = { needRerender: false };
static getderivedstatefromprops(nextProps, nextState) {
let queryParams = queryString.parse(nextProps.location.search);
if (!(queryParams.page && queryParams.type && queryParams.pageSize && queryParams.application)) {
return { needRefresh: true };
} else {
return { needRefresh: false };
}
}
render() {
return (
<div>
{this.state.needRefresh ? <Redirect params={} /> : <Log />}
</div>
);
}
}

HOC as an App Wrapper for React Redux

I wanted to have an app HOC that wraps each component view.
This HOC authenticates user and sets Google Analytics tracking.
I'm upgrading to Router 4, and having an issue with making it work.
It's giving me the following error -
TypeError: (0 , _AppWrapper2.default) is not a function
Which is likely related to how I am creating the HOC.
Any ideas?
routes.js
export default (
<Switch>
<Route exact path="/" component={AppWrapper(Home)} />
<Route exact path="/channels" component={AppWrapper(Channels)} />
</Switch>
);
const AppWrapper = (WrappedComponent) => {
return class AppWrapperComponent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const page = this.props.location.pathname;
this.trackPage(page);
}
componentWillReceiveProps(nextProps) {
const currentPage = this.props.location.pathname;
const nextPage = nextProps.location.pathname;
if (currentPage !== nextPage) {
this.trackPage(nextPage);
}
}
trackPage = page => {
GoogleAnalytics.set({
page,
...options,
});
GoogleAnalytics.pageview(page);
};
render() {
return (
<div>
{this.state.isMounted && !window.devToolsExtension && process.env.NODE_ENV === 'development' && <DevTools />}
<WrappedComponent {...this.props.chidren} />
</div>
);
}
}
Looks like you're not exporting AppWrapper. If you import it with import AppWrapper from .., add this line at the end of AppWrapper.js:
export default AppWrapper;
or replace the const declaration with
export default (WrappedComponent) => { ..
If you import it with import {AppWrapper} from .., you can insert an export before the const:
export const AppWrapper = (WrappedComponent) => {

Resources