Unit testing Apollo Graphql in React using Hooks - reactjs

I am trying to create a unit test using React and Apollo Graphql, however I keep getting this error:
Watch Usage: Press w to show more. console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:104
Warning: An update to ThemeHandler inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in ThemeHandler (at theme-handler.spec.tsx:51)
in ApolloProvider (created by MockedProvider)
in MockedProvider (at theme-handler.spec.tsx:50)
Here is my code:
import { createMuiTheme, MuiThemeProvider } from '#material-ui/core';
import * as Sentry from '#sentry/browser';
import React, { useState } from 'react';
import { BrandTheme, useGetBrandThemeQuery } from '../../generated/graphql';
/**
* Handles the app theme. Will set the default theme or the brand theme taken from the backend.
*/
export default function ThemeHandler(props: React.PropsWithChildren<any>): React.ReactElement {
const brandId = Number(process.env.REACT_APP_BRAND);
// Default Onyo theme
const [theme, setTheme] = useState({
palette: {
primary: { main: '#f65a02' },
secondary: { main: '#520075' },
},
typography: {
fontFamily: 'Quicksand, sans-serif',
},
});
useGetBrandThemeQuery({
variables: { brandId },
skip: brandId <= 0,
onCompleted: data => {
if (
!data.brandTheme ||
!data.brandTheme.brandThemeColor ||
data.brandTheme.brandThemeColor.length === 0
) {
console.warn('Empty brand theme returned, using default');
Sentry.captureMessage(`Empty brand theme for brandId: ${brandId}`, Sentry.Severity.Warning);
} else {
const palette = parseBrandPalette(data.brandTheme as BrandTheme);
setTheme({ ...theme, palette });
console.log('Theme', theme, data.brandTheme);
}
},
});
return <MuiThemeProvider theme={createMuiTheme(theme)}>{props.children}</MuiThemeProvider>;
}
function parseBrandPalette(brandTheme: BrandTheme) {
const pallete: any = {};
for (const color of brandTheme.brandThemeColor!) {
if (color && color.key === 'primaryColor') {
pallete.primary = { main: color.value };
} else if (color && color.key === 'darkPrimaryColor') {
pallete.secondary = { main: color.value };
}
}
return pallete;
}
And my test:
import renderer from 'react-test-renderer';
import React from 'react';
import ThemeHandler from './theme-handler';
import { MockedProvider, wait } from '#apollo/react-testing';
import { GetBrandThemeDocument } from '../../generated/graphql';
import { Button } from '#material-ui/core';
const { act } = renderer;
describe('Theme Handler', () => {
const originalEnv = process.env;
beforeEach(() => {
// https://stackoverflow.com/questions/48033841/test-process-env-with-jest/48042799
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.REACT_APP_BRAND;
});
afterEach(() => {
process.env = originalEnv;
});
it('should use a theme retrieved from the backend', async () => {
process.env.REACT_APP_BRAND = '39';
const mocks = [
{
request: {
query: GetBrandThemeDocument,
variables: { brandId: 39 },
},
result: {
data: {
brandTheme: {
brandThemeColor: [
{ key: 'primaryColor', value: '#182335' },
{ key: 'darkPrimaryColor', value: '#161F2F' },
],
},
},
},
},
];
let wrapper;
act(() => {
wrapper = renderer.create(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeHandler>
<Button color='primary' id='test-obj'>
Hello world!
</Button>
</ThemeHandler>
</MockedProvider>
);
});
await wait(0);
expect(wrapper).toBeTruthy();
});
});
I also tried to use Enzyme's mount instead of the React test renderer, but the result is the same.
As far as I could tell, this error is being caused because I am changing the current state using an async function and hooks. But I am not sure what could I do differently for this to work.

I solved my problem by wrapping everything on my test with act. I believe that this error was happening because part of the test was wrapped in act, but the async part wasn't, so the change was happening outside the scope of this function.
Here is the updated test, that is passing:
import React from 'react';
import ThemeHandler from './theme-handler';
import { MockedProvider, wait } from '#apollo/react-testing';
import { GetBrandThemeDocument } from '../../generated/graphql';
import { Button } from '#material-ui/core';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
describe('Theme Handler', () => {
const originalEnv = process.env;
beforeEach(() => {
// https://stackoverflow.com/questions/48033841/test-process-env-with-jest/48042799
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.REACT_APP_BRAND;
});
afterEach(() => {
process.env = originalEnv;
});
it('should use a theme retrieved from the backend', async () => {
process.env.REACT_APP_BRAND = '39';
await act(async () => {
const mocks = [
{
request: {
query: GetBrandThemeDocument,
variables: { brandId: 39 },
},
result: {
data: {
brandTheme: {
brandThemeColor: [
{ key: 'primaryColor', value: '#182335' },
{ key: 'darkPrimaryColor', value: '#161F2F' },
],
},
},
},
},
];
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeHandler>
<Button color='primary' id='test-obj'>
Hello world!
</Button>
</ThemeHandler>
</MockedProvider>
);
expect(wrapper).toBeTruthy();
await wait(0);
wrapper.update();
expect(wrapper.find('#test-obj')).toBeTruthy();
});
});
});

Related

NextRouter was not mounted." occurs in storybook

I am using next13. So I have raised storybook to version7.
However, when I start up story-book, I get the next-route error.
The cause is that I am using router in AppContext. If I comment out the router part, the storybook works.
I am making the transition to the screen according to the error content in the AppContext. (using router).
error
NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
import * as React from 'react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { AppContextProvider } from '../src/context/AppContextProvider';
import { NextPageWithLayout } from '../src/pages/_app.page';
import {
mockOrganization,
mockList,
} from '../src/stories/mocks/msw-handlers';
import 'style/index.css';
import 'style/utils/slider.css';
import 'tailwindcss/tailwind.css';
initialize();
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
layout: 'fullscreen',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers: {
'/organization': mockOrganization(),
'/master': mockList(),
},
},
chromatic: {
viewports: [375, 768, 1200],
},
};
export const decorators = [
(Story: NextPageWithLayout) => {
const getLayout = Story.getLayout ?? (page => page);
return <AppContextProvider>{getLayout(<Story />)}</AppContextProvider>;
},
mswDecorator,
];
Make sure that the Router is properly initialized and mounted in your Storybook setup
import * as React from 'react';
import { Router } from 'next/router';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { AppContextProvider } from '../src/context/AppContextProvider';
import { NextPageWithLayout } from '../src/pages/_app.page';
import {
mockOrganization,
mockList,
} from '../src/stories/mocks/msw-handlers';
import 'style/index.css';
import 'style/utils/slider.css';
import 'tailwindcss/tailwind.css';
initialize();
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
layout: 'fullscreen',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers: {
'/organization': mockOrganization(),
'/master': mockList(),
},
},
chromatic: {
viewports: [375, 768, 1200],
},
};
const RouterDecorator = (Story: NextPageWithLayout) => {
const [isRouterReady, setIsRouterReady] = React.useState(false);
React.useEffect(() => {
Router.ready(() => {
setIsRouterReady(true);
});
}, []);
const getLayout = Story.getLayout ?? (page => page);
return (
<React.Fragment>
{isRouterReady && (
<AppContextProvider>
{getLayout(<Story />)}
</AppContextProvider>
)}
</React.Fragment>
);
};
export const decorators = [RouterDecorator, mswDecorator];

React component test case coverage of inline functions

Can someone help me cover this test case, I am not able to figure out how to cover this inline function
Note: DropdownField is a wrapper component and contains the actual which is imported from
import { Field } from "redux-form";
dropdown input inside
I have tried to call mockfunction and jest.fn() but nothing works, Any help will be appreciated because I am totally blank at the moment. Thanks in advance to all the wonderful devs
import React from "react";
import DropdownField from "components/FormFields/DropdownField";
import get from "lodash/get";
 
const AddressLookup = props => {
const {
change,
formValues,
fetchAddressLookup,
postalCodeOptions,
type = "delivery",
placeholder = "type_to_search",
field
} = props;
const selectedDeliveryMethod = get(formValues, "delivery_method", {});
 
return (
<DropdownField
placeholder={placeholder}
options={postalCodeOptions}
{...selectedDeliveryMethod.fields.zip_code}
isSearchable={true}
field={field}
onInputChange={value => {
if (value.length >= 2) fetchAddressLookup({ q: value });
}}
onChange={({ value }) => {
const [city, state, zipCode] = value.split("-");
change(field, value);
change(`${type}_state`, state);
change(`${type}_city`, city);
change(`${type}_zip_code`, zipCode);
}}
/>
);
};
 
export default AddressLookup;
I have tried this approach but It failed to cover. First test case covers the UI part only as you can see it is matching to snapshot. In second test cases I removed some code and commented some because nothing works
import * as React from 'react';
import { render, fireEvent, wait } from '#testing-library/react';
import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux';
import { reduxForm } from 'redux-form';
import configureStore from 'redux-mock-store';
import messages from '__fixtures__/messages.json';
import AddressLookup from '../index';
const DecoratedAddressLookup = reduxForm({ form: 'testForm' })(AddressLookup);
const testProps = {
change: jest.fn(),
fetchAddressLookup: jest.fn(),
postalCodeOptions: [
{
name: 'abc-abcd-1234',
value: 'abc-abcd-1234',
},
],
formValues: {
delivery_method: {
fields: {
zip_code: 'BD-BDBD-1234',
},
},
},
field: 'zip_code',
};
describe('<AddressLookup />', () => {
let testStore;
let addressField;
const mockStore = configureStore([]);
const store = mockStore({});
const spy = jest.fn();
beforeAll(() => {
testStore = store;
});
const renderComponent = () => {
return render(
<Provider store={testStore}>
<IntlProvider locale='en' messages={messages}>
<DecoratedAddressLookup
{...testProps}
onInputChange={spy}
onChange={spy}
/>
</IntlProvider>
</Provider>
);
};
it('should render and match the snapshot', () => {
const {
getByTestId,
container: { firstChild },
} = renderComponent();
addressField = getByTestId('zip_code');
expect(firstChild).toMatchSnapshot();
});
it('should type a value', async () => {
addressField = addressField.querySelector('input');
// expect(addressField).toBeTruthy();
// console.log('addressField', addressField);
// const input = screen.getByTestId('add-word-input');
fireEvent.change(addressField, { target: { value: 'abc-abcd-1234' } });
expect(addressField).toHaveValue('abc-abcd-1234');
// expect(testProps.change).toBeCalled();
await wait(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
});
});

React Testing Library's waitFor methods don't work, only wait

I'm having a React Native application of which packages I've recently upgraded to newest versions (with a few exceptions).
I have a very simple component:
import React, { FC } from 'react';
import { useParams } from 'react-router-native';
import { View } from 'react-native';
import { useItemByIdQuery } from '../../hooks/apollo/apollo-generated';
export const Item: FC = () => {
const { itemId } = useParams<{ itemId: string }>();
const { data, previousData, loading, error } = useItemByIdQuery({
variables: { _id: itemId },
notifyOnNetworkStatusChange: true,
});
const item = (data || previousData)?.getItemById;
console.log(data, previousData, error, loading, itemId);
if (!item && loading) {
return <View testID='spinner' />;
}
return null;
};
It returns null to keep it simple, let's say we only want to test that the spinner disappears.
import React from 'react';
import { MockedResponse } from '#apollo/client/testing';
import * as ReactRouter from 'react-router-native';
import { GetItemById } from '../graphql';
import {
MockedProvider,
renderWithRedux,
wait,
waitFor,
waitForElementToBeRemoved,
} from '../utils/test/react-testing-library';
import { Item } from '../containers/Item';
const getItemMockResult = {
_id: 'item',
};
const getItemMock = {
request: {
query: GetItemById,
variables: { _id: 'item' },
},
result: {
data: {
getItemById: getItemMockResult,
},
},
};
const setup = (mocks: MockedResponse[]) =>
renderWithRedux(
<MockedProvider mocks={mocks} addTypename={false}>
<ReactRouter.NativeRouter initialEntries={['/item']}>
<ReactRouter.Route path="/:itemId">
<Item />
</ReactRouter.Route>
</ReactRouter.NativeRouter>
</MockedProvider>,
);
describe('Item', () => {
test(`Spinner disappears once item is loaded`, async () => {
const component = setup([getItemMock]);
const spinner = await component.findByTestId('spinner');
await waitFor(() => expect(component.queryByTestId('spinner')).toBeFalsy());
});
});
It logs undefined undefined undefined true item. It's stuck at the loading state.
If I use wait(0) before the waitFor, it passes. However when I set a higher timeout for the waitFor, it doesn't work.
export const wait = (timeout = 0) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, timeout);
});
I've met with situations where inside waitFor it was like a different state of the component (based on the console logs the elements were completely different inside the component, and inside waitFor), but I have no idea what causes it.
This is being tested alone to make sure no inference with other tests.
Any ideas?
If I duplicate the test (without wait), and run them, one of them passes as expected, and one fails.

useSelector of Undefined

Hi I am writting unit testing for a component it has redux in it. While testing it , throws an error UseSelector state undefined. UseSelector will be updated once after getting response from the api. Till that its value is undefined. But while unit tetsing it throws error on the first itself . How to overcome this issue.
Test File
import React from 'react';
import { render, fireEvent } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import '#testing-library/jest-dom/extend-expect';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import LinkApplication from '../index';
import {reducer} from '../../../redux/summary/reducer';
let INITIAL_STATES = {
isFetching: false,
errorMessage: "",
requestExtensionErrorMessage: "",
requestExtensionSuccessMessage: "",
linkApplicationSuccess: "",
linkApplicationFailure: null,
linkLoanSuccessMessage: "",
linkLoanErrorMessage: "",
LinkApplicationErrorMessage:"",
};
function renderWithRedux(
component,
{ INITIAL_STATES, store = createStore(reducer, INITIAL_STATES) } = {},
) {
return {
...render(<Provider store={store}>{component}</Provider>),
};
}
it('sumbits the form', async () => {
const onSubmit = jest.fn();
const { getByText, getByTestId } = renderWithRedux(
<LinkApplication onSubmit={onSubmit} />,
);
const Dob_Input = getByTestId('dob-input');
const Phone_Input = getByTestId('phone-input');
const form = getByTestId('form');
act(() => {
fireEvent.keyPress(Dob_Input, {
target: { value: '1995-09-27' },
});
fireEvent.keyPress(Phone_Input, { target: { value: '9500902621' } });
});
expect(Dob_Input.value).toBe('1995-09-27');
expect(Phone_Input.value).toBe('9500902621');
await act(() => {
fireEvent.submit(form);
});
expect(onSubmit).not.toHaveBeenCalled();
})
Component
const LinkApplication = () => {
const dispatch = useDispatch();
let LinkApplicationErrorMessage = useSelector(
state => state.summary.linkApplicationFailure
);
}
Error
TypeError: Cannot read property 'linkApplicationFailure' of undefined
const dispatch = useDispatch();
let LinkApplicationErrorMessage = useSelector(
state => state.summary.linkApplicationFailure
^
);
Please help me with that.
I think you need to mock react-redux
import * as React from 'react';
import { shallow } from 'enzyme';
jest.mock('react-redux', () => ({
useDispatch: () => {},
useSelector: () => ({
your: 'state',
}),
}));
import Component from './index';
describe('Test component', () => {
it('Should render and match the snapshot', () => {
const wrapper = shallow(<Component />);
});
});

React Apollo Testing is not working. Query data is always coming undefined

I am getting loading state only and data as undefined in testing. I don't know why I am following everything in the given example. Please help.
Testing file. When i am waiting thsi line toexecute await wait(() => getByTestId('edit-category'));. It is giving response data of query as undefined.
Error: TypeError: Cannot read property 'getCategory' of undefined
Line 34 on editConatinerCategory.tsx => category={data!.getCategory!}
import React from 'react';
import gql from 'graphql-tag';
import { cleanup, wait } from 'react-testing-library';
import { customRender } from '../../../test-utils/customRender';
import { EditCategoryContainer } from './Container';
afterEach(() => {
cleanup();
console.error;
});
console.error = jest.fn();
const getCategoryMock = {
request: {
query: gql`
query getCategory($id: Int!) {
getCategory(id: $id) {
id
name
active
position
}
}
`,
variables: {
id: 1
}
},
result: {
data: {
getCategory: {
id: 1,
name: 'category',
active: true,
position: 1
}
}
}
};
describe('create edit category module', () => {
test('Rendering correct', async () => {
const { container, debug, getByTestId } = customRender(<EditCategoryContainer />, [
getCategoryMock
]);
await wait(() => getByTestId('edit-category'));
await wait(() => expect(container).toMatchSnapshot());
//Getting this TypeError: Cannot read property 'getCategory' of undefined. Because i am data as undefined from my query response
});
});
CustomRender.tsx
import React from 'react';
import { render } from 'react-testing-library';
import { MockedProvider, MockedResponse } from 'react-apollo/test-utils';
import { Router, Switch } from 'react-router-dom';
import { createMemoryHistory } from 'history';
export const customRender = (
node: JSX.Element | null,
mocks?: MockedResponse[],
{
route = '/',
history = createMemoryHistory({ initialEntries: [route] })
} = {}
) => {
return {
history,
...render(
<MockedProvider mocks={mocks} addTypename={false}>
<Router history={history}>
<Switch>{node}</Switch>
</Router>
</MockedProvider>
)
};
};
EditCategoryContainer.tsx
import React from 'react';
import { withRouter } from 'react-router';
import { Spin } from 'antd';
import {
AddCategoryComponent,
GetCategoryComponent
} from '../../../generated/graphql';
import { EditCategory } from './Edit';
import { LoadingComponent } from '../../../components/LoadingComponent';
export const EditCategoryContainer = withRouter(({ history, match }) => {
const id: number = parseInt(match.params.id, 10);
return (
<GetCategoryComponent
variables={{
id
}}
>
{({ data, loading: getCategoryLoading }) => {
console.log(getCategoryLoading, 'getCategoryLoading');
if (getCategoryLoading) {
return <LoadingComponent />;
}
if (data && !data.getCategory) {
return <div>Category not found!</div>;
}
console.log(data);
return (
<AddCategoryComponent>
{(addCategory, { loading, error }) => {
return (
<EditCategory
data-testid="edit-category"
category={data!.getCategory!}
loading={loading || getCategoryLoading}
onSubmit={values => {
addCategory({ variables: values }).then(() => {
history.push('/dashboard/categories');
});
}}
/>
);
}}
</AddCategoryComponent>
);
}}
</GetCategoryComponent>
);
});
Edit:
I tried #mikaelrs solution which is passed match. But it is not working. I also tried to pass id:1 as fixed. But it is still giving error.
<GetCategoryComponent
variables={{
id:1
}}
>
...rest of code.
</GetCategoryComponent>
This is not working. My query without veriable is working fine. Mutation is also working fine. I am having only problem with this. When i have to pass like varible like this.
What I do to wait for the loading state of the MockedProvider to pass is to use the wait function from waait. This is actually what Apollo recommends as well.
So in your test you would do:
import React from 'react';
import gql from 'graphql-tag';
import { cleanup } from 'react-testing-library';
import wait from 'waait'
import { customRender } from '../../../test-utils/customRender';
import { EditCategoryContainer } from './Container';
afterEach(() => {
cleanup();
});
const getCategoryMock = {
request: {
query: gql`
query getCategory($id: Int!) {
getCategory(id: $id) {
id
name
active
position
}
}
`,
variables: {
id: 1
}
},
result: {
data: {
getCategory: {
id: 1,
name: 'category',
active: true,
position: 1
}
}
}
};
describe('create edit category module', () => {
test('Rendering correct', async () => {
const { container, debug } = customRender(<EditCategoryContainer />, [
getCategoryMock
]);
await wait(0);
// Your loading state should be false after this, and your component should
// get it's data from apollo for you to do any assertion you would like to
// after this point. To see that the component is rendered with data invoke
// the debug function from react-testing-library after this point
debug();
expect(container).toMatchSnapshot()
});
});
Another solution is to use react-testing-librarys wait function to wait for an element that would be present after the loading state switches to true.
For instance
describe('create edit category module', () => {
test('Rendering correct', async () => {
const { container, debug, queryByText } = customRender(<EditCategoryContainer />, [
getCategoryMock
]);
await wait(()=> queryByText("Some Data"));
// Your loading state should be false after this, and your component should
// get it's data from apollo for you to do any assertion you would like to
// after this point
expect(container).toMatchSnapshot()
});
});
I faced a similar issue. Here is how I resolved my issue.
First, wait for the query to resolve, as recommended by #mikaelrs and the docs:
await new Promise(resolve => setTimeout(resolve, 0));
After doing that, the loading property was false, but data was still undefined. I discovered that my mock result object was missing a property. Once I added that missing property to the mock result, the data was populated as expected.

Resources