fch 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fetch.js +19 -7
- package/package.json +2 -1
- package/readme.md +183 -62
- package/fetch.test.js +0 -370
package/fetch.js
CHANGED
|
@@ -136,13 +136,6 @@ const fch = swear((url, options = {}) => {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
fch.get = (url, options = {}) => fch(url, { ...options, method: "get" });
|
|
140
|
-
fch.head = (url, options = {}) => fch(url, { ...options, method: "head" });
|
|
141
|
-
fch.post = (url, options = {}) => fch(url, { ...options, method: "post" });
|
|
142
|
-
fch.patch = (url, options = {}) => fch(url, { ...options, method: "patch" });
|
|
143
|
-
fch.put = (url, options = {}) => fch(url, { ...options, method: "put" });
|
|
144
|
-
fch.del = (url, options = {}) => fch(url, { ...options, method: "delete" });
|
|
145
|
-
|
|
146
139
|
// Default values
|
|
147
140
|
fch.method = "get";
|
|
148
141
|
fch.headers = {};
|
|
@@ -156,4 +149,23 @@ fch.before = (request) => request;
|
|
|
156
149
|
fch.after = (response) => response;
|
|
157
150
|
fch.error = (error) => Promise.reject(error);
|
|
158
151
|
|
|
152
|
+
const request = (url, opts = {}) => fch(url, { ...opts });
|
|
153
|
+
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
154
|
+
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
155
|
+
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
156
|
+
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
157
|
+
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
158
|
+
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
159
|
+
|
|
160
|
+
fch.request = request;
|
|
161
|
+
fch.get = get;
|
|
162
|
+
fch.head = head;
|
|
163
|
+
fch.post = post;
|
|
164
|
+
fch.patch = patch;
|
|
165
|
+
fch.put = put;
|
|
166
|
+
fch.del = del;
|
|
167
|
+
|
|
168
|
+
fch.swear = swear;
|
|
169
|
+
|
|
159
170
|
export default fch;
|
|
171
|
+
export { request, get, head, post, patch, put, del, swear };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
5
|
"homepage": "https://github.com/franciscop/fetch",
|
|
6
6
|
"repository": "https://github.com/franciscop/fetch.git",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"ajax"
|
|
23
23
|
],
|
|
24
24
|
"main": "fetch.js",
|
|
25
|
+
"files": [],
|
|
25
26
|
"type": "module",
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=18.0.0"
|
package/readme.md
CHANGED
|
@@ -4,10 +4,7 @@ A library to make API calls easier. Similar to Axios, but tiny size and simpler
|
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import api from "fch";
|
|
7
|
-
|
|
8
|
-
api.baseUrl = "https://pokeapi.co/";
|
|
9
|
-
|
|
10
|
-
const mew = await api("/pokemon/150");
|
|
7
|
+
const mew = await api("https://pokeapi.co/pokemon/150");
|
|
11
8
|
console.log(mew);
|
|
12
9
|
```
|
|
13
10
|
|
|
@@ -15,11 +12,10 @@ console.log(mew);
|
|
|
15
12
|
|
|
16
13
|
- Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
|
|
17
14
|
- Automatically parse server response as json if it includes the headers, or text otherwise.
|
|
18
|
-
-
|
|
15
|
+
- Works the same way in Node.js and the browser.
|
|
19
16
|
- Await/Async Promise interface works as you know and love.
|
|
20
17
|
- Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance.
|
|
21
18
|
- Advanced [Promise interface](https://www.npmjs.com/swear) for better scripting.
|
|
22
|
-
- Import with the shorthand for tighter syntax. `import { get, post } from 'fch';`.
|
|
23
19
|
- Easily define shared options straight on the root `fetch.baseUrl = "https://...";`.
|
|
24
20
|
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
25
21
|
- Deduplicates parallel GET requests.
|
|
@@ -27,6 +23,24 @@ console.log(mew);
|
|
|
27
23
|
- [TODO]: cache engine with "highs" and "lows", great for scrapping
|
|
28
24
|
- [TODO]: rate-limiting of requests (N-second, or N-parallel), great for scrapping
|
|
29
25
|
|
|
26
|
+
These are the available options and their defaults:
|
|
27
|
+
|
|
28
|
+
- `api.baseUrl = null;`: Set an API endpoint
|
|
29
|
+
- `api.method = 'get';`: Default method to use for api()
|
|
30
|
+
- `api.headers = {};`: Merged with the headers on a per-request basis
|
|
31
|
+
- `api.dedupe = true;`: Avoid parallel GET requests to the same path
|
|
32
|
+
- `api.output = 'body';`: Return the body; use 'response' for the full response
|
|
33
|
+
- `api.before = req => req;`: Interceptor executed before sending the request
|
|
34
|
+
- `api.after = res => res;`: Handle the responses before returning them
|
|
35
|
+
- `api.error = err => Promise.reject(err);`: handle any error thrown by fch
|
|
36
|
+
- `api(url, { method, body, headers, ... })`
|
|
37
|
+
- `api.get(url, { headers, ... });`: helper for convenience
|
|
38
|
+
- `api.head(url, { headers, ... });`: helper for convenience
|
|
39
|
+
- `api.post(url, { body, headers, ... });`: helper for convenience
|
|
40
|
+
- `api.patch(url, { body, headers, ... });`: helper for convenience
|
|
41
|
+
- `api.put(url, { body, headers, ... });`: helper for convenience
|
|
42
|
+
- `api.del(url, { body, headers, ... });`: helper for convenience
|
|
43
|
+
|
|
30
44
|
## Getting Started
|
|
31
45
|
|
|
32
46
|
Install it in your project:
|
|
@@ -43,79 +57,124 @@ import api from 'fch';
|
|
|
43
57
|
const data = await api.get('/');
|
|
44
58
|
```
|
|
45
59
|
|
|
60
|
+
## Options
|
|
46
61
|
|
|
47
|
-
|
|
62
|
+
```js
|
|
63
|
+
import api, { get, post, put, ... } from 'fch';
|
|
64
|
+
|
|
65
|
+
// General options with their defaults; most of these are also parameters:
|
|
66
|
+
api.baseUrl = null; // Set an API endpoint
|
|
67
|
+
api.method = 'get'; // Default method to use for api()
|
|
68
|
+
api.headers = {}; // Is merged with the headers on a per-request basis
|
|
69
|
+
|
|
70
|
+
// Control simple variables
|
|
71
|
+
api.dedupe = true; // Avoid parallel GET requests to the same path
|
|
72
|
+
api.output = 'body'; // Return the body; use 'response' for the full response
|
|
73
|
+
|
|
74
|
+
// Interceptors
|
|
75
|
+
api.before = req => req;
|
|
76
|
+
api.after = res => res;
|
|
77
|
+
api.error = err => Promise.reject(err);
|
|
78
|
+
|
|
79
|
+
// Similar API to fetch()
|
|
80
|
+
api(url, { method, body, headers, ... });
|
|
81
|
+
|
|
82
|
+
// Our highly recommended style:
|
|
83
|
+
api.get(url, { headers, ... });
|
|
84
|
+
api.post(url, { body, headers, ... });
|
|
85
|
+
api.put(url, { body, headers, ... });
|
|
86
|
+
// ...
|
|
87
|
+
|
|
88
|
+
// Just import/use the method you need
|
|
89
|
+
get(url, { headers, ... });
|
|
90
|
+
post(url, { body, headers, ... });
|
|
91
|
+
put(url, { body, headers, ... });
|
|
92
|
+
// ...
|
|
93
|
+
```
|
|
48
94
|
|
|
49
|
-
|
|
95
|
+
### URL
|
|
96
|
+
|
|
97
|
+
This is normally the first argument, though technically you can use both styles:
|
|
50
98
|
|
|
51
99
|
```js
|
|
52
|
-
|
|
100
|
+
// All of these methods are valid
|
|
101
|
+
import api from 'fch';
|
|
53
102
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
103
|
+
// We strongly recommend using this style for your normal code:
|
|
104
|
+
await api.post('/hello', { body: '...', headers: {} })
|
|
105
|
+
|
|
106
|
+
// Try to avoid these, but they are also valid:
|
|
107
|
+
await api('/hello', { method: 'post', body: '...', headers: {} });
|
|
108
|
+
await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
|
|
109
|
+
await api.post({ url: '/hello', headers: {}, body: '...' });
|
|
57
110
|
```
|
|
58
111
|
|
|
59
|
-
|
|
112
|
+
It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
|
|
60
113
|
|
|
61
|
-
|
|
114
|
+
```js
|
|
115
|
+
import api from 'fch';
|
|
116
|
+
api.baseUrl = 'https//api.filemon.io/';
|
|
117
|
+
api.get('/hello');
|
|
118
|
+
// Called https//api.filemon.io/hello
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Body
|
|
122
|
+
|
|
123
|
+
The `body` can be a string, a FormData instance or a plain object. If it's an object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
|
|
62
124
|
|
|
63
125
|
```js
|
|
64
|
-
import api from
|
|
126
|
+
import api from 'api';
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
// Sending plain text
|
|
129
|
+
await api.post('/houses', { body: 'plain text' });
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
131
|
+
// Will JSON.stringify it internally, and add the JSON headers
|
|
132
|
+
await api.post('/houses', { body: { id: 1, name: 'Cute Cottage' } });
|
|
133
|
+
|
|
134
|
+
// Send it as FormData
|
|
135
|
+
form.onsubmit = e => {
|
|
136
|
+
await api.post('/houses', { body: new FormData(e.target) });
|
|
137
|
+
};
|
|
70
138
|
```
|
|
71
139
|
|
|
72
|
-
|
|
140
|
+
|
|
141
|
+
### Headers
|
|
142
|
+
|
|
143
|
+
You can define headers globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
|
|
144
|
+
|
|
73
145
|
|
|
74
146
|
```js
|
|
75
|
-
|
|
76
|
-
api.
|
|
77
|
-
const name = await api.get("/users/1").name;
|
|
147
|
+
import api from 'fch';
|
|
148
|
+
api.headers.abc = 'def';
|
|
78
149
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
150
|
+
api.get('/helle', { headers: { ghi: 'jkl' } });
|
|
151
|
+
// Total headers on the request:
|
|
152
|
+
// { abc: 'def', ghi: 'jkl' }
|
|
82
153
|
```
|
|
83
154
|
|
|
84
|
-
### Interceptors
|
|
85
155
|
|
|
86
|
-
|
|
156
|
+
### Output
|
|
157
|
+
|
|
158
|
+
This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
|
|
87
159
|
|
|
88
160
|
```js
|
|
89
|
-
|
|
90
|
-
fch.before = async req => {
|
|
91
|
-
// Normalized request ready to be sent
|
|
92
|
-
...
|
|
93
|
-
return req;
|
|
94
|
-
};
|
|
161
|
+
import api from 'fch';
|
|
95
162
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
// Full response as just after the request is made
|
|
99
|
-
...
|
|
100
|
-
return res;
|
|
101
|
-
};
|
|
163
|
+
// "body" (default) or "response"
|
|
164
|
+
api.output = 'body';
|
|
102
165
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
// Need to re-throw if we want to throw on error
|
|
106
|
-
...
|
|
107
|
-
throw err;
|
|
166
|
+
// Return only the body, this is the default
|
|
167
|
+
const body = await api.get('/data');
|
|
108
168
|
|
|
109
|
-
|
|
110
|
-
|
|
169
|
+
// Return the whole response (with .body):
|
|
170
|
+
const response = await api.get('/data', { output: 'response' });
|
|
111
171
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
};
|
|
172
|
+
// Throws error
|
|
173
|
+
const invalid = await api.get('/data', { output: 'invalid' });
|
|
115
174
|
```
|
|
116
175
|
|
|
117
176
|
|
|
118
|
-
|
|
177
|
+
### Dedupe
|
|
119
178
|
|
|
120
179
|
When you perform a GET request to a given URL, but another GET request *to the same* URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
|
|
121
180
|
|
|
@@ -157,28 +216,90 @@ it("can opt out locally", async () => {
|
|
|
157
216
|
});
|
|
158
217
|
```
|
|
159
218
|
|
|
219
|
+
### Interceptors
|
|
160
220
|
|
|
161
|
-
|
|
221
|
+
You can also easily add the interceptors `before`, `after` and `error`:
|
|
162
222
|
|
|
163
|
-
|
|
223
|
+
```js
|
|
224
|
+
// Perform an action or request transformation before the request is sent
|
|
225
|
+
fch.before = async req => {
|
|
226
|
+
// Normalized request ready to be sent
|
|
227
|
+
...
|
|
228
|
+
return req;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Perform an action or data transformation after the request is finished
|
|
232
|
+
fch.after = async res => {
|
|
233
|
+
// Full response as just after the request is made
|
|
234
|
+
...
|
|
235
|
+
return res;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Perform an action or data transformation when an error is thrown
|
|
239
|
+
fch.error = async err => {
|
|
240
|
+
// Need to re-throw if we want to throw on error
|
|
241
|
+
...
|
|
242
|
+
throw err;
|
|
243
|
+
|
|
244
|
+
// OR, resolve it as if it didn't fail
|
|
245
|
+
return err.response;
|
|
164
246
|
|
|
165
|
-
|
|
247
|
+
// OR, resolve it with a custom value
|
|
248
|
+
return { message: 'Request failed with a code ' + err.response.status };
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
### Advanced promise interface
|
|
255
|
+
|
|
256
|
+
This library uses [the `swear` promise interface](https://www.npmjs.com/swear), which allows you to query operations seamlessly on top of your promise:
|
|
166
257
|
|
|
167
258
|
```js
|
|
168
|
-
import
|
|
169
|
-
fch.error = error => error.response;
|
|
259
|
+
import api from "fch";
|
|
170
260
|
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
// You can keep performing actions like it was sync
|
|
262
|
+
const firstFriendName = await api.get("/friends")[0].name;
|
|
263
|
+
console.log(firstFriendName);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Define shared options
|
|
267
|
+
|
|
268
|
+
You can also define values straight away:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
import api from "fch";
|
|
272
|
+
|
|
273
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
274
|
+
|
|
275
|
+
const mew = await api.get("/pokemon/150");
|
|
276
|
+
console.log(mew);
|
|
173
277
|
```
|
|
174
278
|
|
|
175
|
-
|
|
279
|
+
If you prefer Axios' style of outputting the whole response, you can do:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
// Default, already only returns the data on a successful call
|
|
283
|
+
api.output = "data";
|
|
284
|
+
const name = await api.get("/users/1").name;
|
|
285
|
+
|
|
286
|
+
// Axios-like
|
|
287
|
+
api.output = "response";
|
|
288
|
+
const name = await api.get("/users/1").data.name;
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
## How to
|
|
293
|
+
|
|
294
|
+
### Stop errors from throwing
|
|
295
|
+
|
|
296
|
+
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
|
|
176
297
|
|
|
177
298
|
```js
|
|
178
299
|
import fch from 'fch';
|
|
179
300
|
fch.error = error => error.response;
|
|
180
301
|
|
|
181
|
-
const res = fch('/notfound');
|
|
302
|
+
const res = await fch('/notfound');
|
|
182
303
|
expect(res.status).toBe(404);
|
|
183
304
|
```
|
|
184
305
|
|
|
@@ -214,8 +335,8 @@ There's a configuration parameter for that:
|
|
|
214
335
|
import fch from 'fch';
|
|
215
336
|
fch.baseUrl = 'https://api.filemon.io/';
|
|
216
337
|
|
|
217
|
-
// Calls "https://api.filemon.io/blabla
|
|
218
|
-
const body = await fch.get('/blabla
|
|
338
|
+
// Calls "https://api.filemon.io/blabla"
|
|
339
|
+
const body = await fch.get('/blabla');
|
|
219
340
|
```
|
|
220
341
|
|
|
221
342
|
|
|
@@ -230,7 +351,7 @@ fch.headers.Authorization = 'bearer abc';
|
|
|
230
351
|
const me = await fch('/users/me');
|
|
231
352
|
```
|
|
232
353
|
|
|
233
|
-
|
|
354
|
+
Or globally on a per-request basis, for example if you take the value from localStorage:
|
|
234
355
|
|
|
235
356
|
```js
|
|
236
357
|
import fch from 'fch';
|
package/fetch.test.js
DELETED
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
import fch from "./fetch.js";
|
|
2
|
-
import mock from "jest-fetch-mock";
|
|
3
|
-
|
|
4
|
-
mock.enableMocks();
|
|
5
|
-
|
|
6
|
-
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
|
7
|
-
|
|
8
|
-
describe("fetch()", () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
fetch.resetMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("can create a basic request", async () => {
|
|
14
|
-
fetch.once("hello");
|
|
15
|
-
const data = await fch("/");
|
|
16
|
-
|
|
17
|
-
expect(data).toEqual("hello");
|
|
18
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
19
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
20
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
21
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("accepts Axios syntax as well", async () => {
|
|
25
|
-
fetch.once("hello");
|
|
26
|
-
const data = await fch({ url: "/" });
|
|
27
|
-
|
|
28
|
-
expect(data).toEqual("hello");
|
|
29
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
30
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
31
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
32
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("can receive a full response", async () => {
|
|
36
|
-
fetch.once("hello", { status: 200, headers: { hello: "world" } });
|
|
37
|
-
const res = await fch("/", { output: "response" });
|
|
38
|
-
|
|
39
|
-
expect(res.body).toEqual("hello");
|
|
40
|
-
expect(res.status).toEqual(200);
|
|
41
|
-
expect(res.statusText).toEqual("OK");
|
|
42
|
-
expect(res.headers.hello).toEqual("world");
|
|
43
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("works with JSON", async () => {
|
|
47
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
48
|
-
const data = await fch("https://google.com/");
|
|
49
|
-
|
|
50
|
-
expect(data).toEqual({ secret: "12345" });
|
|
51
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
52
|
-
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("can use the swear interface", async () => {
|
|
56
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
57
|
-
const hello = await fch("/").secret;
|
|
58
|
-
expect(hello).toEqual("12345");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("can use the baseUrl", async () => {
|
|
62
|
-
fetch.once("hi");
|
|
63
|
-
fch.baseUrl = "https://google.com/";
|
|
64
|
-
const data = await fch.get("/hello");
|
|
65
|
-
expect(data).toBe("hi");
|
|
66
|
-
expect(fetch.mock.calls[0][0]).toBe("https://google.com/hello");
|
|
67
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
68
|
-
fch.baseUrl = null;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("can use the `fetch.get()` shorthand", async () => {
|
|
72
|
-
fetch.once("my-data");
|
|
73
|
-
const data = await fch.get("/");
|
|
74
|
-
expect(data).toBe("my-data");
|
|
75
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("can use the `fetch.patch()` shorthand", async () => {
|
|
79
|
-
fetch.once("my-data");
|
|
80
|
-
expect(await fch.patch("/")).toBe("my-data");
|
|
81
|
-
expect(fetch.mock.calls[0][1].method).toEqual("patch");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("can use the `fetch.put()` shorthand", async () => {
|
|
85
|
-
fetch.once("my-data");
|
|
86
|
-
expect(await fch.put("/")).toBe("my-data");
|
|
87
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("can use the `fetch.post()` shorthand", async () => {
|
|
91
|
-
fetch.once("my-data");
|
|
92
|
-
expect(await fch.post("/")).toBe("my-data");
|
|
93
|
-
expect(fetch.mock.calls[0][1].method).toEqual("post");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("can use the `fetch.del()` shorthand", async () => {
|
|
97
|
-
fetch.once("my-data");
|
|
98
|
-
expect(await fch.del("/")).toBe("my-data");
|
|
99
|
-
expect(fetch.mock.calls[0][1].method).toEqual("delete");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("ignores invalid options", async () => {
|
|
103
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
104
|
-
const res = await fch("https://google.com/", 10);
|
|
105
|
-
|
|
106
|
-
expect(res).toEqual({ secret: "12345" });
|
|
107
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
108
|
-
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("will not overwrite if it is FormData", async () => {
|
|
112
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
113
|
-
const res = await fch("/", { method: "post", body: new FormData() });
|
|
114
|
-
|
|
115
|
-
expect(res).toEqual({ secret: "12345" });
|
|
116
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
117
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
118
|
-
expect(opts).toMatchObject({ body: expect.any(FormData) });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("will not overwrite if content-type is set", async () => {
|
|
122
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
123
|
-
const res = await fch("/", {
|
|
124
|
-
method: "POST",
|
|
125
|
-
body: { a: "b" },
|
|
126
|
-
headers: { "Content-Type": "xxx" },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(res).toEqual({ secret: "12345" });
|
|
130
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
131
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
132
|
-
expect(url).toEqual("/");
|
|
133
|
-
expect(opts).toMatchObject({
|
|
134
|
-
method: "post",
|
|
135
|
-
body: { a: "b" },
|
|
136
|
-
headers: { "content-type": "xxx" },
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("will send JSON", async () => {
|
|
141
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
142
|
-
const res = await fch("/", { method: "POST", body: { a: "b" } });
|
|
143
|
-
|
|
144
|
-
expect(res).toEqual({ secret: "12345" });
|
|
145
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
146
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
147
|
-
expect(url).toEqual("/");
|
|
148
|
-
expect(opts).toMatchObject({
|
|
149
|
-
method: "post",
|
|
150
|
-
body: '{"a":"b"}',
|
|
151
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("can run in parallel", async () => {
|
|
156
|
-
fetch.once("a").once("b");
|
|
157
|
-
const res = await Promise.all([fch("/a"), fch("/b")]);
|
|
158
|
-
|
|
159
|
-
expect(res).toEqual(["a", "b"]);
|
|
160
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("can set `accepts` insensitively", async () => {
|
|
164
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
165
|
-
const res = await fch("/", { headers: { Accepts: "text/xml" } });
|
|
166
|
-
|
|
167
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({ accepts: "text/xml" });
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("can accept network rejections", async () => {
|
|
171
|
-
fetch.mockResponseOnce(JSON.stringify("unauthorized"), {
|
|
172
|
-
status: 401,
|
|
173
|
-
ok: false,
|
|
174
|
-
});
|
|
175
|
-
await expect(fch("/")).rejects.toMatchObject({
|
|
176
|
-
message: "Unauthorized",
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("throws with the wrong 'output' option", async () => {
|
|
181
|
-
fetch.once("hello");
|
|
182
|
-
await expect(fch("/", { output: "abc" })).rejects.toMatchObject({
|
|
183
|
-
message: `options.output needs to be either "body" (default) or "response", not "abc"`,
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("can accept rejections", async () => {
|
|
188
|
-
fetch.mockRejectOnce(new Error("fake error message"));
|
|
189
|
-
await expect(fch("/error")).rejects.toMatchObject({
|
|
190
|
-
message: "fake error message",
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("interceptors", () => {
|
|
196
|
-
beforeEach(() => {
|
|
197
|
-
fetch.resetMocks();
|
|
198
|
-
});
|
|
199
|
-
afterEach(() => {
|
|
200
|
-
fetch.resetMocks();
|
|
201
|
-
delete fch.before;
|
|
202
|
-
delete fch.after;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("can create a before interceptor", async () => {
|
|
206
|
-
fetch.once("hello");
|
|
207
|
-
const data = await fch("/", {
|
|
208
|
-
before: (req) => {
|
|
209
|
-
req.url = "/hello";
|
|
210
|
-
req.method = "put";
|
|
211
|
-
return req;
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
expect(data).toEqual("hello");
|
|
216
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
217
|
-
expect(fetch.mock.calls[0][0]).toEqual("/hello");
|
|
218
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
219
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("can create a global before interceptor", async () => {
|
|
223
|
-
fetch.once("hello");
|
|
224
|
-
fch.before = (req) => {
|
|
225
|
-
req.url = "/hello";
|
|
226
|
-
req.method = "put";
|
|
227
|
-
return req;
|
|
228
|
-
};
|
|
229
|
-
const data = await fch("/");
|
|
230
|
-
|
|
231
|
-
expect(data).toEqual("hello");
|
|
232
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
233
|
-
expect(fetch.mock.calls[0][0]).toEqual("/hello");
|
|
234
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
235
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
236
|
-
|
|
237
|
-
delete fch.before;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("can create an after interceptor", async () => {
|
|
241
|
-
fetch.once("hello", { status: 201, headers: { hello: "world" } });
|
|
242
|
-
const res = await fch("/", {
|
|
243
|
-
output: "response",
|
|
244
|
-
after: (res) => {
|
|
245
|
-
res.body = "bye";
|
|
246
|
-
res.status = 200;
|
|
247
|
-
res.headers.hello = "world";
|
|
248
|
-
return res;
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
expect(res.body).toEqual("bye");
|
|
253
|
-
expect(res.status).toEqual(200);
|
|
254
|
-
expect(res.statusText).toEqual("Created");
|
|
255
|
-
expect(res.headers.hello).toEqual("world");
|
|
256
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
257
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
258
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
259
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("can create a global after interceptor", async () => {
|
|
263
|
-
fetch.once("hello", { status: 201, headers: { hello: "world" } });
|
|
264
|
-
fch.after = (res) => {
|
|
265
|
-
res.body = "bye";
|
|
266
|
-
res.status = 200;
|
|
267
|
-
res.headers.hello = "world";
|
|
268
|
-
return res;
|
|
269
|
-
};
|
|
270
|
-
const res = await fch("/", { output: "response" });
|
|
271
|
-
|
|
272
|
-
expect(res.body).toEqual("bye");
|
|
273
|
-
expect(res.status).toEqual(200);
|
|
274
|
-
expect(res.statusText).toEqual("Created");
|
|
275
|
-
expect(res.headers.hello).toEqual("world");
|
|
276
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
277
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
278
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
279
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("dedupe network calls", () => {
|
|
284
|
-
beforeEach(() => {
|
|
285
|
-
fetch.resetMocks();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("dedupes ongoing/parallel calls", async () => {
|
|
289
|
-
fetch.once("a").once("b");
|
|
290
|
-
const res = await Promise.all([fch("/a"), fch("/a")]);
|
|
291
|
-
|
|
292
|
-
expect(res).toEqual(["a", "a"]);
|
|
293
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("dedupes named calls", async () => {
|
|
297
|
-
fetch.once("a").once("b");
|
|
298
|
-
const res = await Promise.all([fch.get("/a"), fch.get("/a")]);
|
|
299
|
-
|
|
300
|
-
expect(res).toEqual(["a", "a"]);
|
|
301
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("can opt out locally", async () => {
|
|
305
|
-
fetch.once("a").once("b");
|
|
306
|
-
const res = await Promise.all([fch("/a"), fch("/a", { dedupe: false })]);
|
|
307
|
-
|
|
308
|
-
expect(res).toEqual(["a", "b"]);
|
|
309
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("cannot reuse an opted out call", async () => {
|
|
313
|
-
fetch.once("a").once("b");
|
|
314
|
-
const res = await Promise.all([fch("/a", { dedupe: false }), fch("/a")]);
|
|
315
|
-
|
|
316
|
-
expect(res).toEqual(["a", "b"]);
|
|
317
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("will reuse the last opt-in call", async () => {
|
|
321
|
-
fetch.once("a").once("b").once("c");
|
|
322
|
-
const res = await Promise.all([
|
|
323
|
-
fch("/a"),
|
|
324
|
-
fch("/a", { dedupe: false }),
|
|
325
|
-
fch("/a"),
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
expect(res).toEqual(["a", "b", "a"]);
|
|
329
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("can opt out globally", async () => {
|
|
333
|
-
fetch.once("a").once("b").once("c");
|
|
334
|
-
fch.dedupe = false;
|
|
335
|
-
const res = await Promise.all([fch("/a"), fch("/a"), fch("/a")]);
|
|
336
|
-
|
|
337
|
-
expect(res).toEqual(["a", "b", "c"]);
|
|
338
|
-
expect(fetch.mock.calls.length).toEqual(3);
|
|
339
|
-
fch.dedupe = true;
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("does NOT dedupe/cache serial requests", async () => {
|
|
343
|
-
fetch.once("a").once("b");
|
|
344
|
-
const resa = await fch("/a");
|
|
345
|
-
const resb = await fch("/a");
|
|
346
|
-
|
|
347
|
-
expect([resa, resb]).toEqual(["a", "b"]);
|
|
348
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("does NOT dedupe/cache other request types", async () => {
|
|
352
|
-
fetch.once("a").once("b");
|
|
353
|
-
const res = await Promise.all([fch.get("/a"), fch.post("/a")]);
|
|
354
|
-
|
|
355
|
-
expect(res).toEqual(["a", "b"]);
|
|
356
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it("NOT deduping are invisible for other request", async () => {
|
|
360
|
-
fetch.once("a").once("b").once("c");
|
|
361
|
-
const res = await Promise.all([
|
|
362
|
-
fch.get("/a"),
|
|
363
|
-
fch.post("/a"),
|
|
364
|
-
fch.get("/a"),
|
|
365
|
-
]);
|
|
366
|
-
|
|
367
|
-
expect(res).toEqual(["a", "b", "a"]);
|
|
368
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
369
|
-
});
|
|
370
|
-
});
|