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.
Files changed (4) hide show
  1. package/fetch.js +19 -7
  2. package/package.json +2 -1
  3. package/readme.md +183 -62
  4. 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.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
- - Isomorphic fetch(); it works the same way in the browser and server.
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
- ### Advanced promise interface
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
- This library has a couple of aces up its sleeve. First, it [has a better Promise interface](https://www.npmjs.com/swear):
95
+ ### URL
96
+
97
+ This is normally the first argument, though technically you can use both styles:
50
98
 
51
99
  ```js
52
- import api from "fch";
100
+ // All of these methods are valid
101
+ import api from 'fch';
53
102
 
54
- // You can keep performing actions like it was sync
55
- const firstFriendName = await api.get("/friends")[0].name;
56
- console.log(firstFriendName);
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
- ### Define shared options
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
- You can also define values straight away:
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 "fch";
126
+ import api from 'api';
65
127
 
66
- api.baseUrl = "https://pokeapi.co/";
128
+ // Sending plain text
129
+ await api.post('/houses', { body: 'plain text' });
67
130
 
68
- const mew = await api.get("/pokemon/150");
69
- console.log(mew);
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
- If you prefer Axios' style of outputting the whole response, you can do:
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
- // Default, already only returns the data on a successful call
76
- api.output = "data";
77
- const name = await api.get("/users/1").name;
147
+ import api from 'fch';
148
+ api.headers.abc = 'def';
78
149
 
79
- // Axios-like
80
- api.output = "response";
81
- const name = await api.get("/users/1").data.name;
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
- You can also easily add the interceptors `before`, `after` and `error`:
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
- // Perform an action or request transformation before the request is sent
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
- // Perform an action or data transformation after the request is finished
97
- fch.after = async res => {
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
- // Perform an action or data transformation when an error is thrown
104
- fch.error = async err => {
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
- // OR, resolve it as if it didn't fail
110
- return err.response;
169
+ // Return the whole response (with .body):
170
+ const response = await api.get('/data', { output: 'response' });
111
171
 
112
- // OR, resolve it with a custom value
113
- return { message: 'Request failed with a code ' + err.response.status };
114
- };
172
+ // Throws error
173
+ const invalid = await api.get('/data', { output: 'invalid' });
115
174
  ```
116
175
 
117
176
 
118
- ## Dedupe
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
- ## How to
221
+ You can also easily add the interceptors `before`, `after` and `error`:
162
222
 
163
- ### Stop errors from throwing
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
- While you can handle this on a per-request basis, if you want to overwrite the global behavior you can just do:
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 fch from 'fch';
169
- fch.error = error => error.response;
259
+ import api from "fch";
170
260
 
171
- const res = fch('/notfound');
172
- expect(res.status).toBe(404);
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
- Just that, then when there's an error it'll just return as usual, e.g.:
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
- Globally on a per-request basis, for example if you take the value from localStorage:
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
- });