Skip to content

Commit bdcfe0b

Browse files
author
nebarf
committed
use-http-client tests
1 parent 6732863 commit bdcfe0b

17 files changed

+392
-45
lines changed

jest.config.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@ module.exports = {
1212
// Runs special logic, such as cleaning up components
1313
// when using React Testing Library and adds special
1414
// extended assertions to Jest
15-
setupFilesAfterEnv: [
16-
// '@testing-library/react/cleanup-after-each',
17-
// '@testing-library/jest-dom/extend-expect',
18-
],
15+
setupFilesAfterEnv: ['./setup-jest.ts'],
1916

2017
// Test spec file resolution pattern
2118
// Matches parent folder `__tests__` and filename

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"eslint-plugin-react-hooks": "^4.2.0",
3535
"husky": "^7.0.0",
3636
"jest": "^27.2.4",
37+
"jest-fetch-mock": "^3.0.3",
3738
"lint-staged": "^11.2.0",
3839
"prettier": "^2.4.1",
3940
"react": "^17.0.2",
@@ -60,8 +61,8 @@
6061
"release:patch": "release-it patch",
6162
"release:minor": "release-it minor",
6263
"release:major": "release-it major",
63-
"test": "jest -c ./jest.config.js",
64-
"test:watch": "jest -c ./jest.config.js --watch"
64+
"test": "jest -c ./jest.config.js --env=jsdom",
65+
"test:watch": "jest -c ./jest.config.js --watch --env=jsdom"
6566
},
6667
"peerDependencies": {
6768
"react": "^17.0.2",

setup-jest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { enableFetchMocks } from 'jest-fetch-mock';
2+
3+
enableFetchMocks();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { ReactElement } from 'react';
2+
import { HttpClientConfigProvider, HttpClientProviderConfig } from '../..';
3+
4+
export class HttpClientProviderConfigFixture {
5+
static defaultOptions: Partial<HttpClientProviderConfig> = {};
6+
7+
static create(
8+
options?: Partial<HttpClientProviderConfig>
9+
): ({ children }: { children: ReactElement }) => ReactElement {
10+
const fallenbackOptions: Partial<HttpClientProviderConfig> =
11+
options || HttpClientProviderConfigFixture.defaultOptions;
12+
13+
return ({ children }: { children: ReactElement }): ReactElement => {
14+
const Provider = (
15+
<HttpClientConfigProvider config={fallenbackOptions}>{children}</HttpClientConfigProvider>
16+
);
17+
18+
return Provider;
19+
};
20+
}
21+
}

src/__tests__/use-http-client.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { cleanup } from '@testing-library/react';
2+
import { renderHook } from '@testing-library/react-hooks';
3+
import { FetchMock } from 'jest-fetch-mock';
4+
import {
5+
defaultHttpReqConfig,
6+
HttpError,
7+
HttpMethod,
8+
HttpRequest,
9+
RequestErroredEvent,
10+
RequestStartedEvent,
11+
RequestSuccededEvent,
12+
useHttpClient,
13+
useHttpEvent,
14+
} from '..';
15+
import { HttpClientProviderConfigFixture } from './fixtures/http-client-config-provider.fixture';
16+
17+
const fetch = global.fetch as FetchMock;
18+
19+
describe('use-http-client', () => {
20+
const fetchResponse = {
21+
name: 'Phelony',
22+
role: 'Admin',
23+
};
24+
25+
beforeEach(() => {
26+
jest.useFakeTimers();
27+
jest.spyOn(global, 'setTimeout');
28+
});
29+
30+
afterEach((): void => {
31+
cleanup();
32+
fetch.resetMocks();
33+
jest.runAllTimers();
34+
});
35+
36+
test('should perform a http request', async () => {
37+
fetch.mockResponseOnce(JSON.stringify(fetchResponse));
38+
39+
const { result } = renderHook(() => useHttpClient(), {
40+
wrapper: HttpClientProviderConfigFixture.create(),
41+
});
42+
43+
const { request } = result.current;
44+
const baseUrlOverride = 'https://phelony.com';
45+
const relativeUrl = 'todos/1';
46+
const httpMethod = HttpMethod.Get;
47+
48+
const res = await request({
49+
baseUrlOverride,
50+
relativeUrl,
51+
requestOptions: {
52+
method: httpMethod,
53+
},
54+
});
55+
56+
expect(res).toEqual(fetchResponse);
57+
expect(fetch.mock.calls.length).toEqual(1);
58+
59+
const [[fetchUrl, fetchParams]] = fetch.mock.calls;
60+
const { method: fetchMethod, headers: fetchHeaders } = fetchParams || {};
61+
expect(fetchUrl).toBe(`${baseUrlOverride}/${relativeUrl}`);
62+
expect(fetchMethod).toBe(httpMethod);
63+
expect(fetchHeaders).toEqual(defaultHttpReqConfig.reqOptions.headers);
64+
});
65+
66+
test('should allow to override provider request params', async () => {
67+
fetch.mockResponseOnce(JSON.stringify(fetchResponse));
68+
69+
const { result } = renderHook(() => useHttpClient(), {
70+
wrapper: HttpClientProviderConfigFixture.create(),
71+
});
72+
73+
const { request } = result.current;
74+
const baseUrlOverride = 'https://phelony.com';
75+
const relativeUrl = 'todos/1';
76+
const httpMethod = HttpMethod.Get;
77+
const headers = {
78+
'Content-Type': 'multipart/form-data;',
79+
};
80+
81+
const res = await request({
82+
baseUrlOverride,
83+
relativeUrl,
84+
requestOptions: {
85+
method: httpMethod,
86+
headers,
87+
},
88+
});
89+
90+
expect(res).toEqual(fetchResponse);
91+
expect(fetch.mock.calls.length).toEqual(1);
92+
93+
const [[fetchUrl, fetchParams]] = fetch.mock.calls;
94+
const { method: fetchMethod, headers: fetchHeaders } = fetchParams || {};
95+
expect(fetchUrl).toBe(`${baseUrlOverride}/${relativeUrl}`);
96+
expect(fetchMethod).toBe(httpMethod);
97+
expect(fetchHeaders).toEqual(headers);
98+
});
99+
100+
test('should return a http error if the response has errors', async () => {
101+
const fetchError = new Error('Fetch error');
102+
fetch.mockRejectOnce(fetchError);
103+
104+
const { result } = renderHook(() => useHttpClient(), {
105+
wrapper: HttpClientProviderConfigFixture.create(),
106+
});
107+
108+
const { request } = result.current;
109+
110+
try {
111+
await request({});
112+
} catch (error) {
113+
expect(error).toBeInstanceOf(HttpError);
114+
expect(error.message).toBe(fetchError.message);
115+
expect(error.status).toBeUndefined();
116+
expect(error.statusText).toBeUndefined();
117+
expect(error.response).toBeUndefined();
118+
expect(error.request.url).toBe('/');
119+
expect(error.nativeError).toBe(fetchError);
120+
}
121+
122+
expect(fetch.mock.calls.length).toEqual(1);
123+
const [[fetchUrl, fetchParams]] = fetch.mock.calls;
124+
expect(fetchUrl).toBe('/');
125+
expect(fetchParams?.headers).toEqual(defaultHttpReqConfig.reqOptions.headers);
126+
expect(fetchParams?.method).toBe(defaultHttpReqConfig.reqOptions.method);
127+
});
128+
129+
test('should dispatch events when the request starts and succeeds', async () => {
130+
fetch.mockResponseOnce(JSON.stringify(fetchResponse));
131+
132+
const { result: useHttpClientResult } = renderHook(() => useHttpClient(), {
133+
wrapper: HttpClientProviderConfigFixture.create(),
134+
});
135+
136+
const requestStartedEventHandler = jest.fn();
137+
renderHook(() => useHttpEvent(RequestStartedEvent, requestStartedEventHandler));
138+
139+
const requestSuccededEventHandler = jest.fn();
140+
renderHook(() => useHttpEvent(RequestSuccededEvent, requestSuccededEventHandler));
141+
142+
const requestErroredEventHandler = jest.fn();
143+
renderHook(() => useHttpEvent(RequestErroredEvent, requestSuccededEventHandler));
144+
145+
await useHttpClientResult.current.request({});
146+
147+
function checkHttpEventHandlerReqParam(httpRequest: HttpRequest) {
148+
expect(httpRequest.baseUrl).toBe('');
149+
expect(httpRequest.body).toBeNull();
150+
expect(httpRequest.credentials).toBeUndefined();
151+
expect(httpRequest.headers).toEqual(defaultHttpReqConfig.reqOptions.headers);
152+
expect(httpRequest.maxAge).toBe(0);
153+
expect(httpRequest.method).toBe(defaultHttpReqConfig.reqOptions.method);
154+
expect(httpRequest.queryParams).toBeUndefined();
155+
expect(httpRequest.relativeUrl).toBe('');
156+
expect(httpRequest.signal).toBeUndefined();
157+
expect(httpRequest.url).toBe('/');
158+
expect(httpRequest.serializedQueryParams).toBe('');
159+
expect(httpRequest.urlWithParams).toBe('/');
160+
}
161+
162+
const [[startedEventRequestParam]] = requestStartedEventHandler.mock.calls;
163+
expect(requestStartedEventHandler.mock.calls.length).toBe(1);
164+
checkHttpEventHandlerReqParam(startedEventRequestParam);
165+
166+
const [[{ request: succededEventRequestParam, response: succededEventResponseParam }]] =
167+
requestSuccededEventHandler.mock.calls;
168+
expect(requestSuccededEventHandler.mock.calls.length).toBe(1);
169+
expect(succededEventResponseParam).toEqual(fetchResponse);
170+
checkHttpEventHandlerReqParam(succededEventRequestParam);
171+
172+
expect(requestErroredEventHandler.mock.calls.length).toBe(0);
173+
});
174+
175+
test('should dispatch events when the request starts and goes in error', async () => {
176+
const fetchError = new Error('Fetch error');
177+
fetch.mockRejectOnce(fetchError);
178+
179+
const queryParams = { orderBy: 'age+' };
180+
181+
const { result: useHttpClientResult } = renderHook(() => useHttpClient(), {
182+
wrapper: HttpClientProviderConfigFixture.create(),
183+
});
184+
185+
const requestStartedEventHandler = jest.fn();
186+
renderHook(() => useHttpEvent(RequestStartedEvent, requestStartedEventHandler));
187+
188+
const requestSuccededEventHandler = jest.fn();
189+
renderHook(() => useHttpEvent(RequestSuccededEvent, requestSuccededEventHandler));
190+
191+
const requestErroredEventHandler = jest.fn();
192+
renderHook(() => useHttpEvent(RequestErroredEvent, requestErroredEventHandler));
193+
194+
try {
195+
await useHttpClientResult.current.request({
196+
requestOptions: { queryParams },
197+
});
198+
} catch (error) {}
199+
200+
function checkHttpEventHandlerReqParam(httpRequest: HttpRequest) {
201+
expect(httpRequest.baseUrl).toBe('');
202+
expect(httpRequest.body).toBeNull();
203+
expect(httpRequest.credentials).toBeUndefined();
204+
expect(httpRequest.headers).toEqual(defaultHttpReqConfig.reqOptions.headers);
205+
expect(httpRequest.maxAge).toBe(0);
206+
expect(httpRequest.method).toBe(defaultHttpReqConfig.reqOptions.method);
207+
expect(httpRequest.queryParams).toEqual(queryParams);
208+
expect(httpRequest.relativeUrl).toBe('');
209+
expect(httpRequest.signal).toBeUndefined();
210+
expect(httpRequest.url).toBe('/');
211+
expect(httpRequest.serializedQueryParams).toBe('orderBy=age%2B');
212+
expect(httpRequest.urlWithParams).toBe('/?orderBy=age%2B');
213+
}
214+
215+
const [[startedEventRequestParam]] = requestStartedEventHandler.mock.calls;
216+
expect(requestStartedEventHandler.mock.calls.length).toBe(1);
217+
checkHttpEventHandlerReqParam(startedEventRequestParam);
218+
219+
expect(requestErroredEventHandler.mock.calls.length).toBe(1);
220+
const [[httpError]] = requestErroredEventHandler.mock.calls;
221+
expect(httpError).toBeInstanceOf(HttpError);
222+
checkHttpEventHandlerReqParam(httpError.request);
223+
expect(httpError.message).toEqual(fetchError.message);
224+
expect(httpError.status).toBeUndefined();
225+
expect(httpError.statusText).toBeUndefined();
226+
expect(httpError.response).toBeUndefined();
227+
228+
expect(requestSuccededEventHandler.mock.calls.length).toBe(0);
229+
});
230+
231+
test('should put the request response in the cache', async () => {
232+
fetch.mockResponseOnce(JSON.stringify(fetchResponse));
233+
234+
const { result } = renderHook(() => useHttpClient(), {
235+
wrapper: HttpClientProviderConfigFixture.create(),
236+
});
237+
238+
const { request } = result.current;
239+
240+
const res1 = await request({
241+
requestOptions: { maxAge: 6000 },
242+
});
243+
244+
expect(res1).toEqual(fetchResponse);
245+
expect(fetch.mock.calls.length).toBe(1);
246+
247+
const res2 = await request({});
248+
expect(res2).toEqual(fetchResponse);
249+
expect(fetch.mock.calls.length).toBe(1);
250+
});
251+
252+
test('should allow to abort a request', async () => {
253+
fetch.mockAbortOnce();
254+
255+
const { result } = renderHook(() => useHttpClient(), {
256+
wrapper: HttpClientProviderConfigFixture.create(),
257+
});
258+
259+
const { abortableRequest } = result.current;
260+
261+
const [requestPromise, abortController] = abortableRequest({
262+
relativeUrl: 'posts',
263+
});
264+
265+
try {
266+
abortController.abort();
267+
await requestPromise;
268+
} catch (error) {
269+
expect(error).toBeInstanceOf(HttpError);
270+
expect(error.status).toBeUndefined();
271+
expect(error.statsuText).toBeUndefined();
272+
expect(error.response).toBeUndefined();
273+
expect(error.message.trim()).toBe('The operation was aborted.');
274+
expect(error.request.url).toBe('/posts');
275+
expect(error.nativeError.name).toBe('AbortError');
276+
expect(fetch.mock.calls.length).toBe(1);
277+
}
278+
});
279+
280+
test('should perform a post request', async () => {
281+
fetch.mockResponseOnce(JSON.stringify(fetchResponse));
282+
283+
const { result } = renderHook(() => useHttpClient(), {
284+
wrapper: HttpClientProviderConfigFixture.create(),
285+
});
286+
287+
const { post } = result.current;
288+
289+
const res1 = await post({
290+
requestOptions: { body: JSON.stringify({ name: 'Rico' }) },
291+
});
292+
293+
expect(res1).toEqual(fetchResponse);
294+
expect(fetch.mock.calls.length).toBe(1);
295+
});
296+
297+
test('should perform an abortable get request', async () => {
298+
fetch.mockAbortOnce();
299+
300+
const { result } = renderHook(() => useHttpClient(), {
301+
wrapper: HttpClientProviderConfigFixture.create(),
302+
});
303+
304+
const { abortableGet } = result.current;
305+
const [requestPromise, abortController] = abortableGet({
306+
relativeUrl: 'posts',
307+
});
308+
309+
try {
310+
abortController.abort();
311+
await requestPromise;
312+
} catch (error) {
313+
expect(error).toBeInstanceOf(HttpError);
314+
expect(error.status).toBeUndefined();
315+
expect(error.statusText).toBeUndefined();
316+
expect(error.response).toBeUndefined();
317+
expect(error.message.trim()).toBe('The operation was aborted.');
318+
expect(error.request.url).toBe('/posts');
319+
expect(error.nativeError.name).toBe('AbortError');
320+
expect(fetch.mock.calls.length).toBe(1);
321+
}
322+
});
323+
});

src/__tests__/use-http-delete.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/__tests__/use-http-event.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)