Why is a catch being triggered if my Fetch is succesful? - reactjs

I am testing with component where on component did mount, it gets some data from an api,
I am mocking the response to be succesful, but still the catch block is being triggered, i think it shouldnt.
and i get this error:
TypeError: Network request failed
at XMLHttpRequest.xhr.onerror (C:\Users\jorge\Documents\testingreact\React-Testing-For-Beginners\node_modules\whatwg-fetch\fetch.js:436:16)
This is my component:
class MoviesList extends PureComponent {
state = {
movies: [],
};
async componentDidMount() {
try {
const res = await fetch(
'https://api.themoviedb.org/3/discover/movie?api_key=APIKEY&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1',
)
const movies = await res.json();
if (movies.success) {
this.setState({
movies: movies.results,
})
}
} catch (e) {
console.log(e);
}
}
render() {
...stuff
}
Then in my test i am mocking the result, and setting a property on the response called success to true
So, the catch error should not be triggered, however the console.log in the catch block is being logged.
Here is the test:
global.fetch = require('jest-fetch-mock')
afterEach(() => {
cleanup
})
const movies = {
success: true,
results: [
{
id: 'hi1',
title: 'title1',
poster_path: 'gfdsftg'
},
{
id: 'hi2',
title: 'title2',
poster_path: 'gfdsftg'
}
]
}
const movie = movies.results[0]
test('<MoviesList />', async () => {
fetch.mockResponseOnce(JSON.stringify(movies))
const {getByTestId, queryByTestId, getAllByTestId} = render(<MoviesList />)
...some assertions here that all pass
})
Why is the catch block being triggered?

Related

How can I trigger a GraphQL error to test an ErrorLink on Apollo Client?

I have the following ErrorLink set for Apollo Client.
export const errorLink = onError(
({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
notificationService.notify("An Error Occurred");
},
);
I need to test this implementation in a unit test.
I've the following to test Apollo Links
const MockQuery = gql`
query {
foo
}
`;
interface LinkResult<T> {
operation: Operation;
result: FetchResult<T>;
}
async function executeLink<T = ApolloLink>(
linkToTest: ApolloLink,
request: GraphQLRequest = { query: MockQuery },
) {
const linkResult = {} as LinkResult<T>;
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(ApolloLink.from([linkToTest]), request).subscribe(
(result) => {
linkResult.result = result as FetchResult<T>;
},
(error) => {
reject(error);
},
() => {
resolve(linkResult);
},
);
});
}
it('triggers a notification on error', () => {
const testLink = new ApolloLink(() => {
await waitFor(() => expect(notificationSpy).toBeCalledWith('An Error Occurred'))
return null;
});
const link = ApolloLink.from([errorLink, testLink]);
executeLink(link);
});
These unit test work fine for other links like AuthLink where I test whether the auth token was set to the localStorage. But I cannot test the error link because I cannot trigger a GraphQL error.
You can create a mocked terminating link and provide a GraphQL operation result.
E.g.
errorLink.ts:
import { onError } from '#apollo/client/link/error';
type ErrorResponse = any;
export const errorLink = onError(({ response, graphQLErrors, networkError, operation }: ErrorResponse) => {
console.log('An Error Occurred');
console.log('graphQLErrors: ', graphQLErrors);
});
errorLink.test.ts:
import { ApolloLink, execute, Observable } from '#apollo/client';
import { gql } from 'apollo-server-express';
import { errorLink } from './errorLink';
const MockQuery = gql`
query {
foo
}
`;
describe('68629868', () => {
test('should pass', (done) => {
expect.assertions(1);
const mockLink = new ApolloLink((operation) =>
Observable.of({
errors: [
{
message: 'resolver blew up',
},
],
} as any),
);
const link = errorLink.concat(mockLink);
execute(link, { query: MockQuery }).subscribe((result) => {
expect(result.errors![0].message).toBe('resolver blew up');
done();
});
});
});
test result:
PASS apollo-graphql-tutorial src/stackoverflow/68629868/errorLink.test.ts (5.02s)
68629868
✓ should pass (14ms)
console.log src/stackoverflow/68629868/errorLink.ts:6
An Error Occurred
console.log src/stackoverflow/68629868/errorLink.ts:7
graphQLErrors: [ { message: 'resolver blew up' } ]
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.067s
package version: #apollo/client#3.3.20
I specifically needed to test handling NetworkError with TypeScript and it was a right pain to figure out, so here's how you can do it:
import {
ApolloLink,
execute,
FetchResult,
from,
gql,
GraphQLRequest,
Observable,
Operation,
} from '#apollo/client'
import { errorLink, notificationService } from './'
interface LinkResult<T> {
operation: Operation
result: FetchResult<T>
}
const MockQuery = gql`
query {
foo
}
`
class NetworkError extends Error {
bodyText
statusCode
result
message
response
constructor(networkErrorProps, ...params) {
super(...params)
const {
name,
bodyText,
statusCode,
result,
message,
response,
} = networkErrorProps
this.name = name
this.bodyText = bodyText
this.statusCode = statusCode
this.result = result
this.message = message
this.response = response
}
}
describe('errorLink', () => {
it('should handle error and send notification', async () => {
const mockLink = new ApolloLink((operation, forward) => {
let fetchResult: FetchResult = {
errors: [], // put GraphQLErrors here
data: null,
}
// Thanks https://stackoverflow.com/a/70936974/21217
let linkResult = Observable.of(fetchResult).map(_ => {
throw new NetworkError({
name: 'ServerParseError',
message: 'Unexpected token',
response: {},
bodyText: '<!DOCTYPE html><html><head></head><body>Error</body></html>',
statusCode: 503,
result: {},
})
})
return linkResult
})
async function executeLink<T = any, U = any>(
dataLink: ApolloLink
) {
const linkResult = {} as LinkResult<T>
return new Promise<LinkResult<T>>((resolve, reject) => {
execute(from([errorLink, dataLink]), {
query: MockQuery,
}).subscribe(
result => {
// We don't care
},
error => {
// We can resolve here to skip having a try / catch around the await below
resolve(linkResult)
},
)
})
}
const notificationSpy = jest.spyOn(notificationService, 'notify')
await executeLink(mockLink)
expect(notificationSpy).toHaveBeenCalledWith('An Error Occurred')
})
})

React test on component's fetchdata returns "cannot read property 'catch' of undefined"

I have a component fetchData using Promise.all with a catch on the fetchBloomreachData so the other promise can continue even if fetchBloomreachData fails.
If fetchBloomreachData rejects it should still wait for newSearch to resolve. But basically if newSearch fails fetchBloomreachData is irrelevant. So newSearch should still continue even if fetchBloomreachData fails. If newSearch fails then it shouldn't.
Did I overlook something? First time I've seen this error happening for me.
ProductListing.fetchData = ({ dispatch }, rawParams, location, clubId) => {
const nextQuery = getListingParams(rawParams, location.query);
return Promise.all([
dispatch(newSearch(nextQuery, clubId, location)),
dispatch(fetchBloomreachData(nextQuery, location.pathname)).catch(() => {})
]);
};
After adding the catch this test is failing and the test is saying cannot read property 'catch' of undefined
it("should fetchBloomreachData", () => {
const ProductListing = require("src/components/product-listing").ProductListing;
const bloomreachStub = sinon.stub(listingActions, "fetchBloomreachData").resolves();
const dispatchStub = sinon.stub();
ProductListing.fetchData(
{ dispatch: dispatchStub },
{ searchTerm: "" },
{ pathname: "/b/1234" },
{}
);
expect(bloomreachStub).to.have.been.calledWith({ searchTerm: "" }, "/b/1234");
});
Your catch should be appended to the Promise, right now its inside of it.
Correction:
ProductListing.fetchData = ({ dispatch }, rawParams, location, clubId) => {
const nextQuery = getListingParams(rawParams, location.query);
return Promise.all([
dispatch(newSearch(nextQuery, clubId, location)),
dispatch(fetchBloomreachData(nextQuery, location.pathname)),
]).catch(() => {});
};
Was able to get it working by updating the dispatchStub to const dispatchStub = () => Promise.resolve();
it("should fetchBloomreachData", () => {
const ProductListing = require("src/components/product-listing").ProductListing;
const bloomreachStub = sinon.stub(listingActions, "fetchBloomreachData").resolves();
const dispatchStub = () => Promise.resolve();
ProductListing.fetchData(
{ dispatch: dispatchStub },
{ searchTerm: "" },
{ pathname: "/b/1234" },
{}
);
expect(bloomreachStub).to.have.been.calledWith({ searchTerm: "" }, "/b/1234");
});

TypeError: Cannot read property 'title' of null when running tests

I get the following error when running my tests : TypeError: Cannot read property 'title' of null. However it should not be null but an empty string.
If I start the application I do not get this error. Only when running the tests written in jest typescript and react testing library.
The application:
When clicked on the button, it will call the following api : https://jsonplaceholder.typicode.com/posts/1. Then it shows the title in the react app. That's it.
MultipleFetches.tsx
import * as React from 'react';
import {CallAPI} from '../API/useAPI';
interface State{
postResponse :{
data:{
userId: number,
id: number,
title: string,
body:string
},
success : boolean,
error: null
}
}
interface PostResponse{
}
class MultipleFetches extends React.Component{
state : State = {
postResponse:{
data: {
userId:0,
id:0,
title : "",
body : ""
},
success: false,
error : null
}
}
componentDidMount(){
}
handleOnClick =() =>{
CallAPI("https://jsonplaceholder.typicode.com/posts/1").then(res =>{
this.setState({
postResponse : res
})
})
}
render() : React.ReactNode{
return(
<div>
<h1>Fetch API Multiple Times</h1>
<button data-testid="post-click" onClick={this.handleOnClick}>Click to fetch data</button>
<p><b>Post Title</b></p>
<p data-testid="post-title">{this.state.postResponse.data.title}</p>
</div>
)
}
}
export default MultipleFetches;
MultipleFetches.test.tsx
import React from 'react';
import { render, fireEvent, waitForElement, getByTestId, wait, waitFor } from "#testing-library/react";
import MultipleFetches from "../components/MultipleFetches";
describe("<MultipleFetches/>", () =>{
test("post shows correct title after button click", async () =>{
const utils = render(<MultipleFetches/>)
const data ={
userId: 1,
id: 1,
title: "Mock Title",
body: "Mocked Body"
}
// I don't even know what this does??
global.fetch = jest.fn().mockImplementation(() => data);
jest.spyOn(global,'fetch')
.mockImplementation(() => Promise.resolve({
json: () => Promise.resolve(data)
} as Response))
const button = utils.getByTestId("post-click");
fireEvent.click(button);
const newPostTitle = await waitFor(() => utils.getByTestId("post-title"))
expect(newPostTitle.textContent).toBe("Mock Title");
})
})
useAPI.ts
const returnResponse ={
data: null,
success: false,
error: null
}
export async function CallAPI(url : string){
try{
const response = await fetch(url);
if ( response.status < 200 || response.status >= 300) throw new Error("Failed to fetch");
if(response.status === 200){
const json = await response.json();
returnResponse.data = json;
returnResponse.success = true
}
}
catch(e){
returnResponse.error = e.message
}
return returnResponse;
}
Looks like you missed to set status as part of response while your api code is now checking that. The right mock is supposed to be:
jest.spyOn(global,'fetch')
.mockImplementation(() => Promise.resolve({
status: 200, // add your status here
json: () => Promise.resolve(data)
} as Response))

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer

I'm trying to test a useFetch custom hook. This is the hook:
import React from 'react';
function fetchReducer(state, action) {
if (action.type === `fetch`) {
return {
...state,
loading: true,
};
} else if (action.type === `success`) {
return {
data: action.data,
error: null,
loading: false,
};
} else if (action.type === `error`) {
return {
...state,
error: action.error,
loading: false,
};
} else {
throw new Error(
`Hello! This function doesn't support the action you're trying to do.`
);
}
}
export default function useFetch(url, options) {
const [state, dispatch] = React.useReducer(fetchReducer, {
data: null,
error: null,
loading: true,
});
React.useEffect(() => {
dispatch({ type: 'fetch' });
fetch(url, options)
.then((response) => response.json())
.then((data) => dispatch({ type: 'success', data }))
.catch((error) => {
dispatch({ type: 'error', error });
});
}, [url, options]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
This is the test
import useFetch from "./useFetch";
import { renderHook } from "#testing-library/react-hooks";
import { server, rest } from "../mocks/server";
function getAPIbegin() {
return renderHook(() =>
useFetch(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
{ method: "GET" },
1
)
);
}
test("fetch should return the right data", async () => {
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
// Overwrite mock with failure case
test("shows server error if the request fails", async () => {
server.use(
rest.get(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
async (req, res, ctx) => {
return res(ctx.status(500));
}
)
);
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
console.log(result.current);
expect(result.current.loading).toBe(false);
expect(result.current.error).not.toBe(null);
expect(result.current.data).toBe(null);
});
I keep getting an error only when running the test:
"Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
The error is coming from TestHook: node_modules/#testing-library/react-hooks/lib/index.js:21:23)
at Suspense
I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.
Any ideas?
Try this.
function getAPIbegin(url, options) {
return renderHook(() =>
useFetch(url, options)
);
}
test("fetch should return the right data", async () => {
const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
const options = { method: "GET" };
const { result, waitForNextUpdate } = getAPIbegin(url, options);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.

Still getting "React state update on an unmounted component" error with React Testing Library tests

I was getting a plethora of errors with my testing code so have taken several steps to improve things. This article helped: https://binarapps.com/blog/clean-up-request-in-useeffect-react-hook/woodb
I improved the useEffect that I suspected was the problem by introducing AbortController:
useEffect(() => {
if (companyId && companyId !== -1) {
const abortController = new AbortController();
requests.get(`${requests.API_ROOT()}account_management/roles?company_id=${companyId}`)
.then(response => {
dispatch({type: UPDATE_ROLES, payload: response.data.roles});
})
.catch(error => {
if (error.response) {
throw('Error fetching roles: ', error.response.status);
}
});
return () => {
abortController.abort();
};
}
}, [companyId, dispatch]);
I also refactored my test code like this:
it('Should hide modal if Close is pressed', async () => {
await act(async () => {
let { getByTestId, getByText, queryByTestId, queryByText } = await renderDom(<AddUsersLauncher />);
fireEvent.click(queryByText(/^Add Users/i));
fireEvent.click(getByText(/^Close/i));
expect(queryByTestId('add-users-modal')).toBeNull();
});
});
Note: The renderDom function remains the same as before:
const _initialState = {
session: {
user: {},
isLoading: false,
error: false,
},
};
function renderDom(component, initialState = _initialState) {
const store = configureStore(initialState);
return {
...render(
<Provider store={store}>
<SessionProvider>
<ThemeProvider theme={theme}>
<SystemMessages />
<CustomComponent />
{component}
</ThemeProvider>
</SessionProvider>
</Provider>),
store
};
}
As per the question from oemera, this is the async call:
export const get = async function(url: string, _options: any) {
const options = _options || await _default_options();
return axios.get(url, options);
};
After this refactoring, the errors/warnings have reduced but there's still one appearing:
> Add Users component tests
> ✓ Should display the Add Users modal (119ms)
> ✓ Should cancel w/o confirmation modal if no data is entered (34ms)
> ✓ Should hide modal if Close is pressed (32ms)
> ✓ Should hide modal if Close is pressed (29ms)
>
> console.error
> node_modules/react-dom/cjs/react-dom.development.js:558
> Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your
> application. To fix, cancel all subscriptions and asynchronous tasks
> in a useEffect cleanup function.
> in UsersProvider (at AddUsersModal.js:10)
> in div (at AddUsersModal.js:9)
> in AddUsersModal (at AddUsersLauncher.js:16)
>
> Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total
> Snapshots: 0 total Time: 4.999s Ran all test suites matching
> /AddUsers.test/i.
I've checked all of the code AddUsers... files mentioned but there are no useEffect instances there.
Any advice on what I should do to remove this warning?
When you are using axios instead of fetch the abort controller won't work. This is how you do it in axios:
import React, { Component } from 'react';
import axios from 'axios';
class Example extends Component {
  signal = axios.CancelToken.source();
  state = {
    isLoading: false,
    user: {},
  }
  componentDidMount() {
    this.onLoadUser();
  }
  componentWillUnmount() {
    this.signal.cancel('Api is being canceled');
  }
  onLoadUser = async () => {
    try {
      this.setState({ isLoading: true });
      const response = await axios.get('https://randomuser.me/api/', {
        cancelToken: this.signal.token,
      })
      this.setState({ user: response.data, isLoading: true });
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log('Error: ', err.message); // => prints: Api is being canceled
      }
else {
        this.setState({ isLoading: false });
      }
    }
   }
   
    render() {
      return (
        <div>
          <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
        </div>
      )
    }
}
source: https://gist.github.com/adeelibr/d8f3f8859e2929f3f1adb80992f1dc09
Note how you pass in a cancelToken instead of the whole signal. Also you get the signal with axios.CancelToken.source() and you call signal.cancel instead or abortController.abort
So for your example, this should work like this or at least lead you in the right direction:
useEffect(() => {
if (companyId && companyId !== -1) {
const signal = axios.CancelToken.source();
requests.get(`${requests.API_ROOT()}account_management/roles?company_id=${companyId}`,
{ cancelToken: signal.token})
.then(response => {
dispatch({type: UPDATE_ROLES,
payload: response.data.roles}); })
.catch(error => {
if (error.response) {
throw('Error fetching roles: ' error.response.status);
}
});
return () => { signal.cancel('Cancelling request...');};
} }, [companyId, dispatch]);

Resources