Mocking React Context for testing - javascript

I have a React app which utilizes the context hook. The app functions properly but I am having difficulty writing passing tests.
My context looks like
import React, { createContext } from 'react';
const DataContext = createContext();
export const DataProvider = (props) => {
const [personSuccessAlert, setPersonSuccessAlert] = React.useState(false);
return (
<DataContext.Provider
value={{
personSuccessAlert,
setPersonSuccessAlert,
}}>
{props.children}
</DataContext.Provider>
);
};
export const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
The app uses this context in a useEffect hook
import React, { useEffect } from 'react';
import { Alert } from '../../../components/alert';
const PersonRecord = ({
match: {
params: { id },
},
setPersonSuccessAlert,
personSuccessAlert,
}) => {
const closeAlert = () => {
setTimeout(() => {
setPersonSuccessAlert(false);
}, 3000);
};
useEffect(() => {
closeAlert();
}, [location]);
return (
<>
<Alert
open={personSuccessAlert}
/>
</>
);
};
export default withContext(PersonRecord);
This all works as expected. When I run my tests I know I need to import the DataProvider and wrap the component but I keep getting an error.
test('useeffect', async () => {
const history = createMemoryHistory();
history.push('/people');
const setPersonSuccessAlert = jest.fn();
const { getByTestId, getByText } = render(
<DataProvider value={{ setPersonSuccessAlert }}>
<MockedProvider mocks={mocksPerson} addTypename={false}>
<Router history={history}>
<PersonRecord match={{ params: { id: '123' } }} />
</Router>
</MockedProvider>
</DataProvider>,
);
const alert = getByTestId('styled-alert');
await act(async () => new Promise((resolve) => setTimeout(resolve, 4000)));
});
There are a few different errors I get depending on how I change things up but the most common is
[TypeError: setPersonSuccessAlert is not a function]
I think my context is setup slightly different than others which is why I am having trouble using other methods found on here.

Related

Content provider data no being loaded into a component

I have the main page 'feed' where i used to have three functions, but I moved it into custom context. The console logs in context file output all the objects correctly, but nothing is visible in feed when i concole.log them.
context file:
import React, { useEffect, createContext, useContext, useState } from 'react'
import { useMemo } from 'react';
import getPosts from '../api/getPosts';
import filterImportedPosts from '../utils/filterImportedPosts';
export const ItemContext = createContext({
postData: {}, setPostData: () => { }
});
export const FilteredItemsContext = createContext({ filteredItems: [], setFilteredItems: () => { } })
export const FilterContext = createContext({ filter: '', setFilter: () => { } })
export function useItemContext() {
return useContext(ItemContext)
}
export function useFilteredItemsContext() {
return useContext(FilteredItemsContext)
}
export function useFilterContext() {
return useContext(FilterContext)
}
export default function PostProvider({ children }) {
const [postData, setPostData] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
const [filter, setFilter] = useState('');
useEffect(() => {
getPosts(setPostData)
console.log('postData: ', postData)
}, []);
useEffect(() => {
// console.log(filter);
const tempFiltItems = filterImportedPosts(postData, filter);
setFilteredItems(tempFiltItems);
console.log('tempFiltItems: ', filteredItems)
}, [filter, postData]);
const filteredItemsState = useMemo(() => {
return { filteredItems, setFilteredItems }
}, [filteredItems, setFilteredItems])
return (
<FilterContext.Provider value={{ filter, setFilter }}>
<FilteredItemsContext.Provider value={filteredItemsState}>
<ItemContext.Provider value={{ postData, setPostData }}>
{children}
</ItemContext.Provider>
</FilteredItemsContext.Provider >
</FilterContext.Provider>
)
}
and here the feed file:
import React, { useState, useEffect } from 'react';
import SinglePost from '../components/singlePost/singlePost';
import FilterPane from '../components/filterPane/filterPane.feedPost';
import { Box, Spinner, Text } from '#chakra-ui/react';
import getPosts from '../api/getPosts';
import Loader from '../../common/Loader';
import filterImportedPosts from '../utils/filterImportedPosts';
import PostProvider, { useFilterContext, useFilteredItemsContext, useItemContext } from './../context/PostDataContext';
export default function Feed() {
//-----------------IMPORT DATA FROM SERVER----------------------
const [error, setError] = useState(null);
const { postData, setPostData } = useItemContext();
const { filter, setFilter } = useFilterContext();
const { filteredItems, setFilteredItems } = useFilteredItemsContext();
useEffect(() => {
console.log(filteredItems)
}, [filteredItems, setFilteredItems])
// this helps while the data is loaded
// if (postData.length === 0) {
// return (
// <Box pos='absolute' top='45vh' left='40%'>
// <Loader />
// </Box>
// )
// }
// console.log(filteredItems);
return (
<PostProvider>
<Box mt={'7vh'} mb={'7vh'} ml={'3vw'} mr={'3vw'} zIndex={200}>
<FilterPane
setFilter={setFilter}
filter={filter}
filteredItems={filteredItems}
/>
{error && (
<div>Error occurred while loading profile info. Details: {error}</div>
)}
{!error && (
<>
{filteredItems.map((item, index) => {
return <SinglePost key={index} item={item} />;
})}
</>
)}
</Box>
</PostProvider>
);
}
console window printscreen. As you can see the filteredItems in context exist but nothing gets shown in the actual feed - the objects are empty. Could someone assist please?

How do I refactor this into `withAuth` using HOC? Or is it possible to use hooks here in Next.js?

import { useSession } from 'next-auth/react'
import { LoginPage } from '#/client/components/index'
const Homepage = () => {
const session = useSession()
if (session && session.data) {
return (
<>
<div>Homepage</div>
</>
)
}
return <LoginPage />
}
export default Homepage
Basically, I don't want to write the same boilerplate of Login & useSession() on every page.
I want something like:
import { withAuth } from '#/client/components/index'
const Homepage = () => {
return (
<>
<div>Homepage</div>
</>
)
}
export default withAuth(Homepage)
Or if possible withAuthHook?
I currently have done the following:
import React from 'react'
import { useSession } from 'next-auth/react'
import { LoginPage } from '#/client/components/index'
export const withAuth = (Component: React.Component) => (props) => {
const AuthenticatedComponent = () => {
const session = useSession()
if (session && session.data) {
return <Component {...props} />
}
return <LoginPage />
}
return AuthenticatedComponent
}
But I get an error:
JSX element type 'Component' does not have any construct or call signatures.ts(2604)
If I use React.ComponentType as mentioned in the answer below, I get an error saying:
TypeError: (0 , client_components_index__WEBPACK_IMPORTED_MODULE_0_.withAuth) is not a function
Have you tried:
export const withAuth = (Component: React.ComponentType) => (props) => {
...
https://flow.org/en/docs/react/types/#toc-react-componenttype
Edit:
Try like this:
export const withAuth = (Component: React.ComponentType) => (props) => {
const session = useSession()
if (session && session.data) {
return <Component {...props} />
}
return <LoginPage />
}
return AuthenticatedComponent
}
The answer was hidden in the docs. I had to specify the following Auth function in _app.tsx:
import { useEffect } from 'react'
import { AppProps } from 'next/app'
import { SessionProvider, signIn, useSession } from 'next-auth/react'
import { Provider } from 'urql'
import { client } from '#/client/graphql/client'
import '#/client/styles/index.css'
function Auth({ children }: { children: any }) {
const { data: session, status } = useSession()
const isUser = !!session?.user
useEffect(() => {
if (status === 'loading') return
if (!isUser) signIn()
}, [isUser, status])
if (isUser) {
return children
}
return <div>Loading...</div>
}
interface AppPropsWithAuth extends AppProps {
Component: AppProps['Component'] & { auth: boolean }
}
const CustomApp = ({ Component, pageProps: { session, ...pageProps } }: AppPropsWithAuth) => {
return (
<SessionProvider session={session}>
<Provider value={client}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</Provider>
</SessionProvider>
)
}
export default CustomApp
And on my actual page, I had to specify Component.auth as true:
const Homepage = () => {
return (
<>
<div>Homepage</div>
</>
)
}
Homepage.auth = true
export default Homepage
A nice summary of what it does can be found on https://simplernerd.com/next-auth-global-session

Next.js getServerSideProps loading state

Is there a way we can have a loading state similar to when fetching data on the client-side?
The reason I would like a loading state is to have something like a loading-skeleton with for instance react-loading-skeleton
On the client-side we could do:
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
But for SSR (getServerSideProps) I cannot figure out if that is doable for example could we have a loading state?
function AllPostsPage(props) {
const router = useRouter();
const { posts } = props;
function findPostsHandler(year, month) {
const fullPath = `/posts/${year}/${month}`;
router.push(fullPath);
}
if (!data) return <div>loading...</div>; // Would not work with SSR
return (
<Fragment>
<PostsSearch onSearch={findPostsHandler} />
<PosttList items={posts} />
</Fragment>
);
}
export async function getServerSideProps() {
const posts = await getAllPosts();
return {
props: {
posts: posts,
},
};
}
export default AllPostsPage;
Recently Next.js has released getServerSideProps should support props value as Promise https://github.com/vercel/next.js/pull/28607
With that we can make a promise but am not sure how to implement that and have a loading state or if that is even achievable. Their example shows:
export async function getServerSideProps() {
return {
props: (async function () {
return {
text: 'promise value',
}
})(),
}
}
Currently watching Next.conf (25/10/2022) this issue looks promising:
https://beta.nextjs.org/docs/data-fetching/streaming-and-suspense
You can modify the _app.js component to show a Loading component while the getServerSideProps is doing async work like a fetch as shown here https://stackoverflow.com/a/60756105/13824894. This will apply on every page transition within your app.
You can still use your loading logic client-side independently.
you can set loading state on _app.js
import Router from "next/router";
export default function App({ Component, pageProps }) {
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const start = () => {
console.log("start");
setLoading(true);
};
const end = () => {
console.log("findished");
setLoading(false);
};
Router.events.on("routeChangeStart", start);
Router.events.on("routeChangeComplete", end);
Router.events.on("routeChangeError", end);
return () => {
Router.events.off("routeChangeStart", start);
Router.events.off("routeChangeComplete", end);
Router.events.off("routeChangeError", end);
};
}, []);
return (
<>
{loading ? (
<h1>Loading...</h1>
) : (
<Component {...pageProps} />
)}
</>
);
}
My choice is to use isReady method of useRouter object
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
function MyApp({ Component, pageProps }) {
const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
useEffect(() => {
router.isReady && setIsLoading(false)
}, []
)
return <>{isLoading ? <>loading...</> : <Component {...pageProps} />}</>
}
export default MyApp
I have not tried this feature yet but in theory I think it should work. If all you want is to have the client side access to a promise via server props, try as below. Basically your props is a async lambda function so you do any work needed e.g fetching data etc inside it so the client-side should access props as a promise and await for it.
export async function getServerSideProps() {
return {
props: (async function () {
const posts = await getAllPosts();
return {
posts: posts,
}
})(),
}
}
//then on client-side you can do the following or similar to set loading state
function MyComponent(props) {
const [isLoading, setIsLoading] = useState(false);
const [posts, setPosts] = useState({});
useEffect(async () => {
setIsLoading(true);
const tempPosts = await props?.posts;
setPosts(posts);
setIsLoading(false);
}, [])
return (
{isLoading && <div>loading...</div>}
);
}
export default MyComponent;
This works for me using MUI v.5
import Router from "next/router";
import Head from "next/head";
import { useEffect, useState } from "react";
import { CacheProvider } from "#emotion/react";
import {
ThemeProvider,
CssBaseline,
LinearProgress,
CircularProgress,
circularProgressClasses,
Box,
} from "#mui/material";
import { alpha } from "#mui/material/styles";
import createEmotionCache from "/src/createEmotionCache";
import theme from "/src/theme";
import Layout from "/src/components/layout/Layout";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
function Loader(props) {
return (
<Box
sx={{
position: "fixed",
top: 0,
left: 0,
right: 0,
}}
>
<LinearProgress />
<Box sx={{ position: "relative", top: 8, left: 8 }}>
<CircularProgress
variant="determinate"
sx={{
color: alpha(theme.palette.primary.main, 0.25),
}}
size={40}
thickness={4}
{...props}
value={100}
/>
<CircularProgress
variant="indeterminate"
disableShrink
sx={{
animationDuration: "550ms",
position: "absolute",
left: 0,
[`& .${circularProgressClasses.circle}`]: {
strokeLinecap: "round",
},
}}
size={40}
thickness={4}
{...props}
/>
</Box>
</Box>
);
}
function MyApp({
Component,
pageProps,
emotionCache = clientSideEmotionCache,
}) {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
Router.events.on("routeChangeStart", () => {
setIsLoading(true);
});
Router.events.on("routeChangeComplete", () => {
setIsLoading(false);
});
Router.events.on("routeChangeError", () => {
setIsLoading(false);
});
}, [Router]);
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={theme}>
<CssBaseline />
{isLoading && <Loader />}
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
</CacheProvider>
);
}
export default MyApp;

React Using useState/Hooks In HOC Causes Error "Hooks can only be called inside of the body of a function component"

Is it not allowed to use hooks inside of a Higher Order Component? When I try to do it with this simple pattern I'm getting the error Invalid hook call. Hooks can only be called inside of the body of a function component.
// App.js
import React, { useState } from 'react';
const WithState = (Component) => {
const [state, dispatch] = useState(0);
return () => <Component state={state} dispatch={dispatch} />;
}
const Counter = ({ state }) => {
return (
<div style={{ textAlign: 'center', margin: '0 auto'}}>
{state}
</div>
)
}
const CounterWithState = WithState(Counter);
const App = () => {
return <CounterWithState />;
}
export default App;
I believe you should use the hooks inside the HOC:
const WithState = (Component) => {
const WithStateComponent = () => {
const [state, dispatch] = useState(0);
return <Component state={state} dispatch={dispatch} />;
}
return WithStateComponent;
}
Inspired by Rafael Souza's answer, you can make it even cleaner with:
const WithState = (Component) => {
return () => {
const [state, dispatch] = useState(0);
return <Component state={state} dispatch={dispatch} />
}
}

How can I properly test a component which is an Apollo GraphQL query?

I have a situation where I am getting frustrated after some hours of working on the same test. I have a query component which returns a datatable. That component I tested it already and it has 100% test coverage.
The test claims to be missing coverage on lines 42, 145, 148, 151 (see the component below to see notes about the lines).
If you look until the end of the component it claims for coverage on some actions handlers.
But now I am testing its parent component which is this:
// imports
const GetShipments = ({
t,
shipmentsPaginationHandler,
toggleFiltersModalHandler,
isFiltersModalOpened,
}) => {
const removeFilterConst = filterKey => () => {
removeFilterHandler(filterKey);
};
return (
<>
<TableToolbarComp
toggleFiltersModal={toggleFiltersModalHandler} // LINE 42
isFiltersModalOpened={isFiltersModalOpened}
removeFilter={removeFilterConst}
/>
<Query
query={GET_SHIPMENTS}
variables={{
...filters,
shippedDate: filters.shippedDate,
limit: pagination.pageSize,
offset: (pagination.page - 1) * pagination.pageSize,
}}
context={{ uri: `/this?url=${softlayerAccountId}` }}
>
{({ loading, error, data }) => {
let tableRowsModel;
let itemsCount;
if (error) {
return (...);
}
if (loading) return (...);
if (data && data.GetShipments) {
({ itemsCount } = data.GetShipments);
if (data.GetShipments.shipments) {
tableRowsModel = data.GetShipments.shipments.map(row => ({
...row,
id: `${row.id}`,
type: row.type.name : '',
status: row.status ? row.status.name : '',
}));
} else {
tableRowsModel = [];
}
setCSVDataHandler(data.GetShipments);
}
return (
<ShipmentsTable tableRows={tableRowsModel} />
);
}}
</Query>
</>
);
};
GetShipments.propTypes = {
// propTypes validation
};
export default compose(
connect(
store => ({
softlayerAccountId: store.global.softlayerAccountId,
isFiltersModalOpened: store.shipments.filtersModalOpened,
}),
dispatch => ({
removeFilterHandler: filterKey => {
dispatch(removeFilter(filterKey));
},
toggleFiltersModalHandler: () => {
dispatch(toggleFiltersModal()); // LINE 145
},
}),
),
translate(),
)(GetShipments);
The most I create tests it seems to be just ignoring what I am doing.
This is what I have so far:
import React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { mount } from 'enzyme';
import { Provider as ReduxProvider } from 'react-redux';
import GetShipments from '../../containers/GetShipments';
import createMockStore from '../../../../utils/createMockStore';
import store from '../../../../redux/store';
import mocks from '../../__fixtures__/shipments-mock';
jest.mock('react-i18next', () => ({
// this mock makes sure any components using the translate HoC receive the t function as a prop
translate: () => Component => {
Component.defaultProps = { ...Component.defaultProps, t: key => key };
return Component;
},
}));
describe('Container to test: GetShipments', () => {
let props;
beforeEach(() => {
props = {
t: jest.fn(() => k => k),
softlayerAccountId: 123,
isFiltersModalOpened: false,
toggleFiltersModalHandler: jest.fn(() => k => k),
setFiltersHandler: jest.fn(() => k => k),
removeFilterHandler: jest.fn(() => k => k),
};
});
it('should render without errors', () => {
mount(
<MockedProvider
mocks={mocks.filter(mock => mock.id === 'get-shipments-default')}
>
<ReduxProvider store={store}>
<GetShipments
{...props}
store={createMockStore({
global: {
accountGuid: 'abcd-1234',
softlayerAccountId: '1234',
},
})}
/>
</ReduxProvider>
</MockedProvider>,
);
});
it('should render loading state initially', () => {
const wrapper = mount(
<MockedProvider mocks={[]} addTypename={false}>
<ReduxProvider store={store}>
<GetShipments {...props} />
</ReduxProvider>
</MockedProvider>,
);
expect(wrapper.find('DataTableSkeleton')).toHaveLength(1);
});
it('should render data', async () => {
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ReduxProvider store={store}>
<GetShipments store={store} {...props} />
</ReduxProvider>
</MockedProvider>,
);
await new Promise(resolve => setTimeout(resolve));
await wrapper.update();
console.log(wrapper.debug());
});
it('should NOT render any data', () => {
mount(
<MockedProvider mocks={[]} addTypename={false}>
<GetShipments store={store} />
</MockedProvider>,
);
});
});
So what am I missing?
First off, you seem to be missing assertions in a number of your tests. Is this purposeful? For example, I'd expect you to be using a snapshot assertion in your first test case ("it should render without errors"). The only test case that's asserting something is "should render loading state".
Regarding the missing coverage, without seeing the full Jest test report, I'm going to simply presume that the functions you've highlighted are never running, i.e. you're not testing that dispatch(toggleFiltersModal()) is running when you execute toggleFiltersModalHandler. In order to achieve 100% test coverage, you need to test these functions. As you're using Enzyme, this could be as simple as:
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ReduxProvider store={store}>
<GetShipments store={store} {...props} />
</ReduxProvider>
</MockedProvider>
);
wrapper.props().toggleFiltersModalHandler();
Unfortunately, as you are using HOCs, you will most likely have to find your component before you can execute the function on the prop, like so:
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ReduxProvider store={store}>
<GetShipments store={store} {...props} />
</ReduxProvider>
</MockedProvider>
);
wrapper.find(GetShipments).props().toggleFiltersModalHandler();
You'll no doubt have to do some trial and error to find the prop in your wrapper, but once you've found it, simply execute it and assert that the function has executed using a jest spy.

Categories