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

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?

Related

React Router Dom - v6 - useBlocker

As https://github.com/remix-run/react-router/issues/8139 is finished and we got useBlocker in v6, did anyone got it to work?
This is what I got so far and pretty much I'm stuck with error I quite don't understand
in App.js I have my BrowserRouter and everything is wrapped inside
Also I used example from implementer's gists (I copy pasted)
import * as React from "react";
import { useBeforeUnload, unstable_useBlocker as useBlocker } from "react-router-dom";
function usePrompt(message, { beforeUnload } = {}) {
let blocker = useBlocker(
React.useCallback(
() => (typeof message === "string" ? !window.confirm(message) : false),
[message]
)
);
let prevState = React.useRef(blocker.state);
React.useEffect(() => {
if (blocker.state === "blocked") {
blocker.reset();
}
prevState.current = blocker.state;
}, [blocker]);
useBeforeUnload(
React.useCallback(
(event) => {
if (beforeUnload && typeof message === "string") {
event.preventDefault();
event.returnValue = message;
}
},
[message, beforeUnload]
),
{ capture: true }
);
}
function Prompt({ when, message, ...props }) {
usePrompt(when ? message : false, props);
return null;
}
And then within my component I called Prompt like this
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false)
...
return (
...
<Prompt when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
)
}
And on page load of MyComponent I keep getting error
Error: useBlocker must be used within a data router. See
https://reactrouter.com/routers/picking-a-router.
at invariant (history.ts:308:1)
at useDataRouterContext (hooks.tsx:523:1)
at useBlocker (hooks.tsx:723:1)
at usePrompt (routerCustomPrompt.js:8:1)
at Prompt (routerCustomPrompt.js:37:1)
Did anyone got useBlocker in new version to work?
The error message is rather clear. In order to use the useBlocker hook it must be used within a component rendered by a Data router. See Picking a Router.
Example:
const MyComponent = (props) => {
const [showPrompt, setShowPrompt] = useState(false);
...
return (
...
<Prompt
when={showPrompt}
message="Unsaved changes detected, continue?"
beforeUnload={true}
/>
);
}
import {
createBrowserRouter,
createRoutesFromElements,
Route,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
{/* ... etc. */}
<Route path="myComponent" element={<MyComponent />} />
{/* ... etc. */}
</Route>
)
);
const App = () => <RouterProvider router={router} />;

How can I avoid infinite loops in my React Router private routes?

In my App.js, I have some authenticated pages I protect with <PrivateRoute>, like so:
<PrivateRoute path="/dashboard">
<Dashboard />
</PrivateRoute>
I implement <PrivateRoute> like so:
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} />} />
);
}
The problem is, the <CheckRedirect> function calls out to an endpoint on my server which dynamically tells you where to redirect.
Here's the function:
export const CheckRedirect = ({ children }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState(null);
const url = "https://example.com/get-redirect"
useEffect(() =>{
async function getPage() {
axios.get(url)
.then(function (response) {
setTarget(response.data.message)
}).finally(() => setIsChecking(false))
}
getPage();
}, []);
if (isChecking) {
return "... Checking";
}
return {target} ? (
<Redirect to={target} />
) : (
<Redirect to='/404' />
);
};
If you're not logged in, it will send back "/login" in the message field. If you're logged in, it will send "/dashboard".
If it sends back "/dashboard", then React Router produces an infinite loop! It tries the same <PrivateRoute> again, which calls out to the endpoint again, which will once again return "/dashboard", and so on...
Is there a way I can tell my <PrivateRoute> to not do the <CheckRedirect> function if this is already the result of a redirect?
I haven't tested it myself, but have you tried passing path as a prop to CheckRedirect and only do the setTarget in your getPage fetch if it returns a different route?
function PrivateRoute({ children, path, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} path={path} />} />
);
}
export const CheckRedirect = ({ children, path }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState(null);
const url = "https://example.com/get-redirect"
useEffect(() =>{
async function getPage() {
axios.get(url)
.then(function (response) {
const newPath = response.data.message
if (path !== newPath) {
setTarget(newPath)
}
}).finally(() => setIsChecking(false))
}
getPage();
}, []);
if (isChecking) {
return "... Checking";
}
return {target} ? (
<Redirect to={target} />
) : (
<Redirect to='/404' />
);
};
To avoid CheckRedirect to do any redirect if everything is ok (ie. it's a valid request for that route), ensure CheckRedirect actually returns null in that case. If you have control over the server response, I'd return a different value (not null, but -1 for example) for non-existent routes (ie. to redirect to 404), and keep null for when you really just want to return null.
In CheckRedirect component, you don't even use children prop. It renders a string and then redirects to a page. It's normal that it loops forever. Pass path as a prop to CheckRedirect component and if it's same as server response, render the children.
Add path prop and pass it:
export const CheckRedirect = ({ children, path }) => {
Add your conditional before redirecting:
if (target === path) {
return children
}
Just change your PrivateRoute Logic to something like this
const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render = { props =>
user.isOnline ? ( <Component {...props} /> ) :
(
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
}
then
<PrivateRoute exact path="/dashboard" component={Dashboard} />

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.

Simulate localStorage data in test

I am using Create React App.
I am trying to simulate isLoggedIn behaviour in my component to get all lines code coverage.
To do that localStorage key: user must exist with data.accessToken
I tried set localStorage data in the test but it is not working. the same method actually working in isLoggedIn function and generate 100% line coverage.
isLoggedIn function
export const isLoggedIn = () => {
const userFromLocalStorage = store('user');
return _get(userFromLocalStorage, 'data.accessToken', false);
};
PrivateRoute.js:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
isLoggedIn() ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: 'login' }} />
)
}
/>
);
PrivateRoute.spec.js
import store from 'store2';
describe('PrivateRoute Logged In', () => {
store('user', {
data: {
accessToken: 'dfg',
},
});
const ShallowPrivateRoute = shallow(
<PrivateRoute path="/" name="Home" component={TestComponent} />
);
it('should cover logged in case', () => {
expect(ShallowPrivateRoute).toBeDefined();
});
});
Is there the way I can mock isLoggedIn function to return true just for one test??
What is the best way to test that kind of behaviour?
You could mock the entire file like this:
jest.mock("you-module", () =>({...methodsMock}));
or you could recieve isLoggedIn in props, that way you only need to pass a mock function when you render your component in test.
<Component isLoggedIn={jest.fn().mockReturnValue(true)} />

Testing onEnter in react-redux-router

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}>

Resources