Bypass fetch call in React component - reactjs

I am attempting to write a test for a react component that sends out a fetch GET request after mounting. The test just needs to check if the component has rendered and when fetch runs I get this: ReferenceError: fetch is not defined. I have searched around and cant seem to find anything that will fix my problem. I am using jest and Test Utils for testing the components.
My component code:
export class Home extends Component {
constructor(props) {
...
}
componentDidMount() {
fetch('/some/path', {
headers: {
'Key1': 'Data1',
'Key2': Data2
}
}).then(response => {
if (response.status == 200) {
response.json().then((data) => {
this.context.store.dispatch(setAssets(data))
}
);
} else {
return (
<Snackbar
open={true}
message={"ERROR: " + str(response.status)}
autoHideDuration={5000}
/>
);
}
}).catch(e => {});
...
);
}
componentWillUnmount() {
...
}
logout(e) {
...
}
render() {
return (
<div>
<AppBar
title="Title"
iconElementLeft={
<IconButton>
<NavigationClose />
</IconButton>
}
iconElementRight={
<IconMenu
iconButtonElement={
<IconButton>
<MoreVertIcon />
</IconButton>
}
targetOrigin={{
horizontal: 'right',
vertical: 'top'
}}
anchorOrigin={{
horizontal: 'right',
vertical: 'top'
}}
>
<MenuItem>
Help
</MenuItem>
</IconMenu>
}
/>
{
this.context.store.getState().assets.map((asset, i) => {
return (
<Card
title={asset.title}
key={i+1}
/>
);
})
}
</div>
);
}
}
Home.contextTypes = {
store: React.PropTypes.object
}
export default Home;
My Test Code:
var home
describe('Home', () => {
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {}
It errors when trying to render Home into document.
I have tried fetch-mock but this only allows for mock calls for API testing, which I'm not trying to do and doesn't mock the fetch calls in my component.
Mocking Home wont work because its the component I am trying to test. Unless there's a way to mock the componentDidMount() function that I have missed.
I just need a workaround for the fetch call. Any ideas??
EDIT: I'm using React's JSX for the component and JS for the test

Try https://github.com/jhnns/rewire:
rewire adds a special setter and getter to modules so you can modify
their behaviour for better unit testing
var fetchMock = { ... }
var rewire = require("rewire");
var myComponent = rewire("./myComponent.js");
myComponent.__set__("fetch", fetchMock);

Unfortunately I'm using babel, which is listed as a limitation for rewire, but I tried it anyways...
I Added:
...
store.getState().assets = a.assets
var fetchMock = function() {return '', 200}
var rewire = require("rewire");
var HomeComponent = rewire('../Home.jsx');
HomeComponent.__set__("fetch", fetchMock);
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
...
And received the error:
Error: Cannot find module '../Home.jsx'
at Function.Module._resolveFilename (module.js:440:15)
at internalRewire (node_modules/rewire/lib/rewire.js:23:25)
at rewire (node_modules/rewire/lib/index.js:11:12)
at Object.eval (Some/Path/Home-test.js:47:21)
I'm assuming this is because babel:
rename[s] variables in order to emulate certain language features. Rewire will not work in these cases
(Pulled from the link chardy provided me)
However the path isn't a variable, so I wonder if babel is really renaming it, and the path is 100% correct for my component's location. I don't think it's because I'm using JSX because it just cant find the component, its not a compatibility issue... Rewire still may not work even if it finds the file though, unfortunately, but I'd like to give it a shot all the same.

I found an answer that worked for me, and it was very simple without including any other dependencies. It was a simple as storing the main function into a variable and overwriting it, then restoring the proper function after the testcase
SOLUTION:
var home
describe('Home', () => {
const fetch = global.fetch
beforeEach(() => {
let store = createStore(assets);
let a = store.dispatch({
type: Asset,
assets: [{
'id': 1,
'title': 'TITLE'
}],
});
store.getState().assets = a.assets
global.fetch = () => {return Promise.resolve('', 200)}
home = TestUtils.renderIntoDocument(
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Provider store={store}>
<Home />
</Provider>
</MuiThemeProvider>
);
});
it('renders the main page, including cards and appbar', () => {
...
});
afterEach(() => {
global.fetch = fetch;
});
});

Using the global context to store components can be brittle and is probably not a good idea for any sizable project.
Instead, you can use the dependency injection (DI) pattern which is a more formalized method for switching out different component dependencies (in this case, fetch) based on your runtime configuration.
https://en.wikipedia.org/wiki/Dependency_injection
One tidy way to employ DI is by using an Inversion of Control (IoC) container, such as:
https://github.com/jaredhanson/electrolyte

What I did is I moved the fetch calls out of the components into a repository class then called that from within the component. That way the components are independent of the data source and I could just switch out the repository for a dummy repository in order to test or change the implementation from doing fetch to getting the data from localStorage, sessionStorage, IndexedDB, the file system, etc.
class Repository {
constructor() {
this.url = 'https://api.example.com/api/';
}
getUsers() {
return fetch(this.url + 'users/').then(this._handleResponse);
}
_handleResponse(response) {
const contentType = response.headers.get('Content-Type');
const isJSON = (contentType && contentType.includes('application/json')) || false;
if (response.ok && isJSON) {
return response.text();
}
if (response.status === 400 && isJSON) {
return response.text().then(x => Promise.reject(new ModelStateError(response.status, x)));
}
return Promise.reject(new Error(response.status));
}
}
class ModelStateError extends Error {
constructor(message, data) {
super(message);
this.name = 'ModelStateError';
this.data = data;
}
data() { return this.data; }
}
Usage:
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
Example:
export class Welcome extends React.Component {
componentDidMount() {
const repository = new Repository();
repository.getUsers().then(
x => console.log('success', x),
x => console.error('fail', x)
);
}
render() {
return <h1>Hello!</h1>;
}
}

Related

React test not working with current Jest config, Gatsby and gatsby-theme-i18n

I am fairly new to testing with Gatsby and Jest and I am encountering issues with my current setup.
I am trying to build a site with Gatsby and I would like for CI purposes to run unit tests with Jest and react-testing-library.
I initially was able to run a simple test:
describe('<Header>', () => {
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Header />
);
expect(container).toBeInTheDocument();
});
});
describe('click', () => {
const { container } = render(<Header />);
expect(screen.getByText('Light mode ☀')).toBeInTheDocument();
fireEvent(
getByText(container, 'Light mode ☀'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
expect(screen.getByText('Dark mode ☾')).toBeInTheDocument();
});
where my render is a customized one:
const AllTheProviders = ({ children }) => {
const [dark, setDark] = useState(true);
return (
<ThemeContext.Provider
value={{
dark,
toggleDark: () => setDark(!dark),
}}
>
{children}
</ThemeContext.Provider>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
// re-export everything
export * from '#testing-library/react';
// override render method
export { customRender as render };
and the Header a simple component that consumes the context and toggles the theme from dark to light.
I since then added gatsby-theme-i18n to handle i18n and tweaked my jest config to add:
transformIgnorePatterns: ['node_modules/(?!(gatsby-theme-i18n)/)']
The Header is now wrapped in a Layout component that provides the locale and location (with the Help for Location and #reach/router that comes with Gatsby) so I can have a language toggle in the header
In the Layout:
import { Location } from '#reach/router';
[...]
<Location>
{(location) => (
<Header
siteTitle={data.site.siteMetadata.title}
location={location}
locale={locale}
/>
)}
</Location>
The Header makes use of LocalizedLink from gatsby-theme-i18n` to prefix the right locale
<li key={langKey}>
<LocalizedLink key={langKey} to={to} language={langKey}>
{langKey}
</LocalizedLink>
</li>
I also changed the test to:
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(container).toBeInTheDocument();
});
});
However, I get an error when trying to run the test:
TypeError: Cannot read property 'themeI18N' of undefined
21 |
22 | const customRender = (ui, options) =>
> 23 | render(ui, { wrapper: AllTheProviders, ...options });
| ^
specifically coming from at useLocalization (node_modules/gatsby-theme-i18n/src/hooks/use-localization.js:9:7)
where the Gatsby theme provides the i18n info through a graphQL query:
const {
themeI18N: { defaultLang, config },
} = useStaticQuery(graphql`
{
themeI18N {
defaultLang
config {
code
hrefLang
dateFormat
langDir
localName
name
}
}
}
`)
I understand that Jest does not 'understand' what to do with the i18n passed through the gatsby-theme-i18n via the Layout and if I switch my LocalizedLink to a normal Gatsby Link, the test passes (and I can console.log the container to see it's indeed a React element with the right info).
I tried a few things I saw online like using const { useLocalization } = require('gatsby-theme-i18n'); but to no avail.
Any idea how to handle this?
Should I mock the utils from gatsby-theme-i18n like other elements from Gatsby are mocked (Link etc.) following the docs?
EDIT
Thanks to the author of the library, LekoArts, and a friend a mine, I managed to make it work :)
If somebody has the same issue, you need to mock the response from the useStaticQuery of the useLocalization hook.
I have a mock file that does it:
// Libs
const React = require('react');
const gatsby = jest.requireActual('gatsby');
// Utils
const siteMetaData = require('../src/utils/siteMetaData');
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement('a', {
...rest,
href: to,
}),
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn().mockImplementation(() => ({
site: {
siteMetadata: {
...siteMetaData,
},
},
themeI18N: {
defaultLang: 'en',
config: {
code: 'en',
hrefLang: 'en-CA',
name: 'English',
localName: 'English',
langDir: 'ltr',
dateFormat: 'MM/DD/YYYY',
},
},
})),
};
and my test:
// Libs
import React from 'react';
// Utils
import { Location } from '#reach/router';
import { render, fireEvent, getByText, screen } from './utils/test-utils';
// Component
import Header from '../header';
describe('<Header>', () => {
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(container).toBeInTheDocument();
});
});
describe('click', () => {
test('toggle language', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(screen.getByText('Light mode ☀')).toBeInTheDocument();
fireEvent(
getByText(container, 'Light mode ☀'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
expect(screen.getByText('Dark mode ☾')).toBeInTheDocument();
});
});
});
You can find more information here

In React, is there an elegant way of using the id in a RESTful edit url and loading the corresponding object into the initial state of my component?

I'm building a React 16.13 application. I have a search component, src/components/Search.jsx, that constructs search results and then builds a URL to edit those results ...
renderSearchResults = () => {
const { searchResults } = this.state;
if (searchResults && searchResults.length) {
return (
<div>
<div>Results</div>
<ListGroup variant="flush">
{searchResults.map((item) => (
<ListGroupItem key={item.id} value={item.name}>
{item.name}
<span className="float-right">
<Link to={"/edit/"+item.id}>
<PencilSquare color="royalblue" size={26} />
</Link>
</span>
</ListGroupItem>
))}
</ListGroup>
</div>
);
}
};
render() {
return (
<div className="searchForm">
<input
type="text"
placeholder="Search"
value={this.state.searchTerm}
onChange={this.handleChange}
/>
{this.renderSearchResults()}
</div>
);
}
Is there a more elegant way to load/pass the object I want to edit? Below I'm deconstructing the URL and launching an AJAX call but what I'm doing seems kind of sloppy. I'm familiar with Angular resolvers and that seems a cleaner way of decoupling the logic of parsing the URL and finding the appropriate objects but the below is all I could come up with ...
src/components/Edit.jsx
import React, { Component } from "react";
import FormContainer from "../containers/FormContainer";
export default class Edit extends Component {
render() {
return <FormContainer />;
}
}
src/containers/FormContainer.jsx
class FormContainer extends Component {
...
componentDidMount() {
let initialCountries = [];
let initialProvinces = [];
let coopTypes = [];
// Load form object, if present in URL
const url = window.location.href;
const id = url.split("/").pop();
fetch(FormContainer.REACT_APP_PROXY + "/coops/" + id)
.then((response) => {
return response.json();
})
.then((data) => {
const coop = data;
coop.addresses.map(address => {
address.country = FormContainer.DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
this.setState({
newCoop: coop,
});
});
You aren't posting all the relevant code but I know what you are trying to accomplish (correct me if I'm wrong). You want to use the id from the url parameters to fetch data. I think you are using react-router. You can use this example to refactor your code:
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom";
const REACT_APP_PROXY = "api";
const DEFAULT_COUNTRY_CODE = "20";
// You can use functional components and react hooks in React 16.13 to do everything
// No need for class components any more
function FormContainer() {
// useState hook to handle state in functional components
const [newCoop, setNewCoop] = useState({});
// useParams returns an object of key/value pairs of URL parameters. Use it to access match.params of the current <Route>.
const { id } = useParams();
// This will be called whenever one of the values in the dependencies array (second argument) changes
// but you can pass an empty array to make it run once
useEffect(() => {
fetch(REACT_APP_PROXY + "/coops/" + id)
.then(response => {
return response.json();
})
.then(data => {
const coop = data;
coop.addresses.map(address => {
address.country = DEFAULT_COUNTRY_CODE; // address.locality.state.country.id;
});
setNewCoop(coop);
});
// use an empty array as the second argument to run this effect on the first render only
// it will give a similar effect to componentDidMount
}, []);
return <div>Editing {id}</div>;
}
const Edit = () => <FormContainer />;
function App() {
return (
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/edit/:id">
<Edit />
</Route>
</Switch>
</Router>
);
}

How do I test Enzyme and apollo graphql hooks?

I have a Modal component that uses a reusable <Modal> component using useQuery and useMutation:
const CCPAModal = ({ addNotification, closeModal }: Props): Node => {
const { data, loading: memberInfoLoading } = useQuery(getMemberInfo, { variables: { id: '1234' } } );
const [ updateMemberMutation, { loading: updateMemberLoading } ] = useMutation(updateMemberPrivacyPolicyAgreedAt, {
variables : { input: { id: data && data.user.member.id } },
onCompleted : (): void => closeModal(modalIds.ccpaModal),
onError : (err): void => addNotification(getFirstError(err), 'error'),
});
return (
<Modal size="md" id={modalIds.ccpaModal}>
{memberInfoLoading && <LoadingCircle />}
{!memberInfoLoading && (
<>
<Subtitle>
copycopycopy <Link alias="PrivacyPolicy">Privacy Policy</Link>.
</Subtitle>
<FlexRow justification="flex-end">
<Button
text="Accept"
disabled={updateMemberLoading}
onClick={updateMemberMutation}
/>
</FlexRow>
</>
)}
</Modal>
);
};
const mapDispatchToProps = { addNotification, closeModal };
export default connect(null, mapDispatchToProps)(CCPAModal);
My issue is that I'm not entirely sure in which direction to take my tests. I want to test the loading state and quite possibly test the data of the mocks, but googling and my lack of knowledge of testing in general is leaving me a bit confused. So far this is my test:
__CCPAModal.test.js
import React from 'react';
// import render from 'react-test-renderer';
import { MockedProvider } from '#apollo/react-testing';
import { act } from 'react-dom/test-utils';
import { getMemberInfo } from 'operations/queries/member';
import { CCPAModal } from './CCPAModal';
jest.mock('#project/ui-component');
const mock = {
request: {
query: getMemberInfo,
{ variables: { id: '1234' } }
},
};
describe('CCPAModal', () => {
it('mounts', () => {
const component = mount(
<MockedProvider
mocks={[]}
>
<CCPAModal />
</MockedProvider>
);
console.log(component.debug())
});
});
Which will log
<MockedProvider mocks={{...}} addTypename={true}>
<ApolloProvider client={{...}}>
<CCPAModal>
<Connect(t) size="md" id="CCPA Modal" />
</CCPAModal>
</ApolloProvider>
</MockedProvider>
Which I'm unsure where to proceed from here. I wanted to be able to test the loading state and the contents after it loaded, but it doesn't seem I can even get past <Connect(t) size="md" id="CCPA Modal" />.
Otherwise, my alternative solution is to just make snapshot tests.
Can anyone advise on how to test with Enzyme / useQuery / useMutation hooks? How do I access the child components after it loads? And for that matter, I'm unsure how to even test the loading state and data.

Code splitting route components wrapped in a HOC with React Loadable

I am running into problems using React Loadable with route based code splitting using Webpack 3.11.
When I try to render my app on the server my async modules immediately resolve without waiting for the promise. Thus the SSR output becomes <div id="root"></div>.
App.js:
const App = () => (
<Switch>
{routes.map((route, index) => (
<Route key={index} path={route.path} render={routeProps => {
const RouteComponent = route.component
return <RouteComponent {...routeProps} />
}} />
))}
</Switch>
)
I've defined my async route components with React Loadable like this:
Routes.js
function Loading ({ error }) {
if (error) {
return 'Oh nooess!'
} else {
return <h3>Loading...</h3>
}
}
const Article = Loadable({
loader: () => import(/* webpackChunkName: "Article" */ '../components/contentTypes/Article'),
loading: Loading
})
const Page = Loadable({
loader: () => import(/* webpackChunkName: "Page" */ '../components/contentTypes/Page'),
loading: Loading,
render (loaded, props) {
let Component = WithSettings(loaded.default)
return <Component {...props}/>
}
})
export default [
{
path: `/:projectSlug/:env${getEnvironments()}/article/:articleSlug`,
component: Article,
exact: true
},
{
path: `/:projectSlug/:env${getEnvironments()}/:menuSlug?/:pageSlug?`,
component: Page
}
]
WithSettings.js
export default (WrappedComponent: any) => {
class WithSettings extends React.Component<WithSettingsProps, WithSettingsState> {
static displayName = `WithSettings(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
state = {
renderWidth: 1200
}
componentDidMount () {
this.loadSettings({ match: { params: { projectSlug: '', env: '' } } })
window.addEventListener('resize', this.onResize)
this.onResize()
}
componentWillUnmount () {
if (isClient) {
window.removeEventListener('resize', this.onResize)
}
}
componentDidUpdate (oldProps) {
this.loadSettings(oldProps)
}
onResize = () => {
this.setState({ renderWidth: this.getLayoutWidth() })
}
getLayoutWidth () {
return (document.body && document.body.offsetWidth) || 0
}
loadSettings (oldProps) {
const { settings, request, getNewSettings } = this.props
const { projectSlug: oldProjectSlug, env: oldEnv } = oldProps.match.params
const { projectSlug: newProjectSlug, env: newEnv } = this.props.match.params
if (
(
oldProjectSlug !== newProjectSlug ||
oldEnv !== newEnv
) ||
(
settings === undefined ||
(request.networkStatus === 'ready')
)
) {
getNewSettings()
}
}
render () {
const { settings, request, history, location, match } = this.props
const { renderWidth } = this.state
if (!settings || !request || request.networkStatus === 'loading') {
return <div />
}
if (request.networkStatus === 'failed') {
return <ErrorBlock {...getErrorMessages(match.params, 'settings')[4044]} fullscreen match={match} />
}
return (
<WrappedComponent
settings={settings}
settingsRequest={request}
history={history}
location={location}
match={match}
renderWidth={renderWidth}
/>
)
}
}
hoistNonReactStatic(WithSettings, WrappedComponent)
return connect(mapStateToProps, mapDispatchToProps)(WithSettings)
}
I've managed to narrow it down to the WithSettings HOC that I am using to wrap my route components in. If I don't use the WithSettings HOC (as with the Article route) then my SSR output waits for the async import to complete, and the server generated html includes markup related to the route (good!). If I do use the HOC (as with the Page route) then the module immediately resolves and my SSR output turns into <div id="root"></div because it no longer waits for the dynamic import to complete before rendering. Problem is: I need the WithSettings HOC in all my routes as it fetches required data from the server that I need to render the app.
Can anyone tell me how I can use a HOC and use React Loadable's async component for route components so that it works on the server?
Managed to figure it out.
It was due to my HOC. In the render method it would return <div /> when settings or request where undefined or request.networkStatus is loading. It turns out this tripped up server side rendering. Removing this block was enough to make it work.

Passing property through router

This is for a react-native app
I'm trying to pass a property to a component through a router like so:
I have a button with an onPress like so in separate component:
_onPress() {
this.props.navigator.push(Router.getDealList(this.state.categoryName, this.state.categoryId));
}
With the router defined as:
const Router = {
getDealList(categoryName, categoryId) {
return {
renderScene() {
let DealList = require('./components/DealList').default;
debugger;
return <DealList categoryId={categoryId} />;
},
getTitle() {
return categoryName;
},
};
},
};
However, when I try to access the property in the DealList component, it seems to be undefined. I am attempting to do this like so:
_onFetch(page = 1, callback) {
const categoryId = this.props.categoryId;
...
}
I've attempted to debug this and it seems to have the variables up until the point where I try to call _onFetch
I have used a different approach in my React native app. Please be patient, this might give you a clue or you can adopt the same way altogether.
In main.js I have this structure which takes care of everything smoothly.
Top level views:
var Splash = require('./views/splash');
var Signin = require('./views/signin');
var Signup = require('./views/signup');
var Home = require('./views/home');
Routes:
var ROUTES = {
splash: Splash,
signin: Signin,
signup: Signup,
home: Home,
};
And navigator looks something like this. Observe "data" being passed as a prop to respective Component in renderScene()
render: function(){
return(
//Render first view through initialRoute
<Navigator
style={styles.container}
initialRoute={{ name: 'home', index: 0 }}
renderScene={ this.renderScene }
configureScene={ () => { return Navigator.SceneConfigs.PushFromRight; }}
>
</Navigator>
)
},
renderScene: function(route, navigator){
//ROUTES['signin'] => SignIn
var Component = ROUTES[route.name];
return (
<Component
route={route}
navigator={navigator}
data={route.data}
/>
);
}
When I want to push a new View in my app I simply do this-
onPressNewBooking: function(){
this.props.navigator.push({ name: 'newbooking', data: this.state.rawData })
}

Resources