Simulate localStorage data in test - reactjs

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

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?

Rerender AppNavigator on state change

I am trying to render certain nav stacks depending on a isAuthenticated state. The problem that I am having is that AppNavigator is only rendered on the first render and not with any other changes and I am not sure why. I have tried a useEffect in the AppNavigator component to set a secondary local state with the callback being isAuthenticated but no go. I put everything pertinent below. I appreciate any advice.
I have an AppNavigator that is being rendered in my app.tsx file.
return (
<ToggleStorybook>
<ApolloProvider client={client}>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={"always"}>
<AppNavigator
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</ErrorBoundary>
</SafeAreaProvider>
</RootStoreProvider>
</ApolloProvider>
</ToggleStorybook>
)
The AppNavigator is returning
export const AppNavigator = (props: NavigationProps) => {
const { isAuthenticated } = useStores()
const colorScheme = useColorScheme()
useBackButtonHandler(canExit)
return (
<NavigationContainer
ref={navigationRef}
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
{...props}
>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
{isAuthenticated ? (
<Stack.Screen name="main" component={MainTabs} />
) : (
<Stack.Screen name="signup" component={SignUpStack} />
)}
</Stack.Navigator>
</NavigationContainer>
)
}
I am using mob-state-x-tree for state management and have a setUser action that is called onAuthStateChanged per the firebase Auth docs. I'm using email and password login and sign up. I've logged the auth state changes they are working as expected.
function onAuthStateChanged(user: any) {
if (user) {
if (rootStore) {
rootStore.setUser(user)
console.log("we have passed user to root store")
}
}
The setUser action sets a state isAuthenticated in a try catch
setUser: flow(function* (firebaseUser) {
try {
const idToken = yield firebaseUser.getIdToken()
yield AsyncStorage.setItem(
'#lessns:token',
idToken
);
self.isAuthenticated = true
self.user = {id: firebaseUser.uid}
} catch(err) {
console.log(err, 'this is the first time ')
self.isAuthenticated = false
}
}),
You need to make your AppNavigator component into an observer so that it will re-render when observable data it depends on changes.
export const AppNavigator = observer((props: NavigationProps) => {
// ...
})
try to put AppNavigator component into an observer?

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

React typescript useParams() always returns undefined in useEffect

I am trying to achieve my profile component fetching logged user's data on path /me and fetching someone else's data on path user/:username.
Following example from react router blogs, I came up with something like this:
function App(): JSX.Element {
//...
<PrivateRoute
exact
path="/me"
render={(props) => (
<ProfileComponent {...props} principal={states.username} isMyProfile={true} />
)}
/>
<PrivateRoute
path="/user/:username"
render={(props) => (
<ProfileComponent {...props} principal={states.username} isMyProfile={false} />
)}
/>
// ...
}
interface ParamTypes {
path: string;
}
export default function ProfileComponent
(props: { principal: string; isMyProfile: boolean }): JSX.Element {
const { path } = useParams<ParamTypes>();
useEffect(() => {
async function fetch(username: string) {
// ...
}
props.isMyProfile ? fetch(props.principal) : fetch(path);
}, [props.principal, path, props.isMyProfile]);
// ...
}
but the path is always undefined. What am I missing?
May be this can help you:
const { username } = useParams();
useEffect(()=>{
console.log(username)}
,[username]);
in case it's your profile then userName should be undefined otherwise you will get some value

Hide Some Element With Local Storage In React

I want to hide some navigation when the user haven't login yet, so I use local storage to save user's id and use if logic to hide and show the navigation, but when i clear the data in local storage and compare it to null, the navigation still showed up.
Here is the code to save data in local storage
loginUser = () => {
Axios.post('http://private-6fdd31-intern1.apiary-mock.com/interns/login', this.state.user)
.then((res) => {
if(res.data.role === "admin")
{
localStorage.setItem("user", res.data.user_id)
this.props.history.push('/member-list');
}
}, (err) => {
console.log(err);
})
}
This is how I compare and clear the data when logout navigation is clicked
handleLogout = () => {
localStorage.clear("user");
}
render() {
return(
<Router>
<Fragment>
<div className="navigation">
{ localStorage.getItem("user") !== null?
<Fragment>
<Link to="/member-list">Member</Link>
<Link to="/override-list">Override</Link>
<Link onClick={this.handleLogout} to="/">Logout</Link>
</Fragment>
: null
}
</div>
<Route path="/" exact component={routeProps => <Login {...routeProps}/>}/>
<Route path="/member-list" component={MemberDashboard}/>
<Route path="/override-list" component={OverrideDashboard}/>
</Fragment>
</Router>
)
}
react native wont call render() method as long as there is no state or props update, you need to call this.forceUpdate() to force a rerender. Documentation: https://facebook.github.io/react/docs/component-api.html like
handleLogout = () => {
localStorage.clear("user");
this.forceUpdate()
}
Maybe use the state to save the user when you set to local storage and same thing when you handleLogout?
if(res.data.role === "admin")
{
this.setState({ user: res.data.user_id})
localStorage.setItem("user", res.data.user_id)
this.props.history.push('/member-list');
}
handleLogout = () => {
localStorage.clear("user");
this.setState({ user: ""})
}
So when state is updated, the component re-renders.

Resources