http-snapshotter 0.2.4 → 0.3.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/README.md +32 -34
- package/index.d.ts +11 -18
- package/index.js +13 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,9 +49,9 @@ You will see a file named `get-xkcd-com-info-0-arAlFb5gfcr9aCN.json` created in
|
|
|
49
49
|
Then onwards running: `node test.js` or `SNAPSHOT=read node test.js` will ensure HTTP network calls are all read from a snapshot file.
|
|
50
50
|
In this mode, http-snapshotter will prevent any real HTTP calls from happening by failing the test (if it didn't have a snapshot file) and print out the request details and the snapshot file name it should have had.
|
|
51
51
|
|
|
52
|
-
There is also a `SNAPSHOT=ignore` option to neither read nor write from snapshot files and do real network requests instead.
|
|
52
|
+
For adding new snapshots without touching existing snapshots use `SNAPSHOT=append`. There is also a `SNAPSHOT=ignore` option to neither read nor write from snapshot files and do real network requests instead. These could be useful while writing a new test.
|
|
53
53
|
|
|
54
|
-
Tip: When you do `SNAPSHOT=update` to create snapshots, run it against a single test, so you know what exact snapshots that one test created/updated.
|
|
54
|
+
Tip: When you do `SNAPSHOT=update` or `SNAPHOT=append` to create snapshots, run it against a single test, so you know what exact snapshots that one test created/updated.
|
|
55
55
|
|
|
56
56
|
Log read/saved snapshots by setting LOG_SNAPSHOT=1 env variable. Log requests with LOG_REQ=1 or LOG_REQ=summary (to just print request HTTP method, url and snapshot file that it would use).
|
|
57
57
|
|
|
@@ -118,44 +118,42 @@ There are scenarios where one needs to test varied response for the same call (e
|
|
|
118
118
|
|
|
119
119
|
There are 2 ways to go about this:
|
|
120
120
|
|
|
121
|
-
Method 1: The easy way
|
|
122
|
-
change the response on runtime for the specific test:
|
|
121
|
+
Method 1: The easy way is to [intercept the function](https://gist.github.com/Munawwar/c1d024d20b78f19b3714ab09b62a0e1f) with your other test utilities:
|
|
123
122
|
|
|
124
123
|
```js
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
// setupIntercepts.js
|
|
125
|
+
// Using intercept.js (https://gist.github.com/Munawwar/c1d024d20b78f19b3714ab09b62a0e1f)
|
|
126
|
+
// Write all your intercepts in a single file for all tests.
|
|
127
|
+
// This is safe because the default behavior of an intercept is
|
|
128
|
+
// to call the original function.
|
|
129
|
+
import { intercept } from "./intercept.js";
|
|
130
|
+
import methods from './account.js';
|
|
131
|
+
// intercept the get() method
|
|
132
|
+
export const accountGet = intercept(methods, 'get');
|
|
133
|
+
|
|
134
|
+
// test.js
|
|
135
|
+
import { accountGet } from './setupIntercepts.js';
|
|
136
|
+
// Next import the root function that you want to test, which
|
|
137
|
+
// internally calls get() function from './account.js'
|
|
138
|
+
import { enablePaidFeature } from './routes.js';
|
|
130
139
|
|
|
131
140
|
test('Test behavior on a free account', async (t) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
JSON.stringify({
|
|
141
|
-
...(await response.clone().json()),
|
|
142
|
-
free_user: true,
|
|
143
|
-
}),
|
|
144
|
-
{
|
|
145
|
-
headers: response.headers
|
|
146
|
-
}
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return response;
|
|
151
|
-
};
|
|
152
|
-
attachResponseTransformer(interceptResponse);
|
|
141
|
+
// Setup mock to simulate a free user
|
|
142
|
+
accountGet.mock(async (originalAccountGetFunction, ...args) => {
|
|
143
|
+
const result = await originalAccountGetFunction(...args); // this will use the existing http snapshot
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
free_user: true,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
153
149
|
|
|
154
|
-
//
|
|
155
|
-
// assert
|
|
150
|
+
// write the test here
|
|
151
|
+
// t.assert(await enablePaidFeature(), { error: 'Free accounts do not have access to this paid feature' })
|
|
156
152
|
|
|
157
|
-
// cleanup before moving to next test
|
|
158
|
-
|
|
153
|
+
// cleanup before moving to next test by calling undoMock()
|
|
154
|
+
// This won't destroy the intercept, but will revert the account get()
|
|
155
|
+
// function to call the original account get() function
|
|
156
|
+
accountGet.undoMock();
|
|
159
157
|
});
|
|
160
158
|
```
|
|
161
159
|
|
package/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export type SnapshotText = {
|
|
2
|
-
responseType: 'text';
|
|
3
2
|
fileSuffixKey: string;
|
|
3
|
+
requestType: 'json' | 'text';
|
|
4
4
|
request: {
|
|
5
5
|
method: string;
|
|
6
6
|
url: string;
|
|
7
7
|
headers: string[][];
|
|
8
|
-
body: string | undefined;
|
|
8
|
+
body: string | object | undefined;
|
|
9
9
|
};
|
|
10
|
+
responseType: 'text';
|
|
10
11
|
response: {
|
|
11
12
|
status: number;
|
|
12
13
|
statusText: string;
|
|
@@ -15,14 +16,15 @@ export type SnapshotText = {
|
|
|
15
16
|
};
|
|
16
17
|
};
|
|
17
18
|
export type SnapshotJson = {
|
|
18
|
-
responseType: 'json';
|
|
19
19
|
fileSuffixKey: string;
|
|
20
|
+
requestType: 'json' | 'text';
|
|
20
21
|
request: {
|
|
21
22
|
method: string;
|
|
22
23
|
url: string;
|
|
23
24
|
headers: string[][];
|
|
24
|
-
body: string | undefined;
|
|
25
|
+
body: string | object | undefined;
|
|
25
26
|
};
|
|
27
|
+
responseType: 'json';
|
|
26
28
|
response: {
|
|
27
29
|
status: number;
|
|
28
30
|
statusText: string;
|
|
@@ -31,6 +33,11 @@ export type SnapshotJson = {
|
|
|
31
33
|
};
|
|
32
34
|
};
|
|
33
35
|
export type Snapshot = SnapshotText | SnapshotJson;
|
|
36
|
+
export type ReadSnapshotReturnType = Promise<{
|
|
37
|
+
snapshot: Snapshot;
|
|
38
|
+
absoluteFilePath: string;
|
|
39
|
+
fileName: string;
|
|
40
|
+
}>;
|
|
34
41
|
export type ClientRequestInterceptorType = import('@mswjs/interceptors/ClientRequest').ClientRequestInterceptor;
|
|
35
42
|
export type FetchInterceptorType = import('@mswjs/interceptors/fetch').FetchInterceptor;
|
|
36
43
|
/**
|
|
@@ -66,20 +73,6 @@ export function attachSnapshotFilenameGenerator(func: (req: Request) => Promise<
|
|
|
66
73
|
}>): void;
|
|
67
74
|
/** Reset snapshot filename generator to default */
|
|
68
75
|
export function resetSnapshotFilenameGenerator(): void;
|
|
69
|
-
/**
|
|
70
|
-
* Attach response transformer function.
|
|
71
|
-
*
|
|
72
|
-
* Here is an opportunity to modify the response (loaded from snapshot) on-the-fly right before
|
|
73
|
-
* the response is sent to consumers.
|
|
74
|
-
*
|
|
75
|
-
* WARNING: Attaching a function on a per-test basis may not be concurrent safe. i.e. If you tests
|
|
76
|
-
* run sequentially, then it is safe. But if your test runner runs test suites concurrently,
|
|
77
|
-
* then it is better to attach a function only once ever.
|
|
78
|
-
* @param {(response: Response, request: Request) => Promise<Response>} func
|
|
79
|
-
*/
|
|
80
|
-
export function attachResponseTransformer(func: (response: Response, request: Request) => Promise<Response>): void;
|
|
81
|
-
/** Reset response transformer */
|
|
82
|
-
export function resetResponseTransformer(): void;
|
|
83
76
|
/**
|
|
84
77
|
* Start the interceptor
|
|
85
78
|
* @param {object} opts
|
package/index.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* SNAPSHOT=update <test runner command>
|
|
11
11
|
* e.g. SNAPSHOT=update tape tests/**\/*.js | tap-diff
|
|
12
12
|
*
|
|
13
|
-
* Here onwards run test runner without SNAPSHOT env variable or SNAPSHOT=read
|
|
13
|
+
* Here onwards run test runner without SNAPSHOT env variable or SNAPSHOT=read.
|
|
14
|
+
* For adding new snapshots without touching existing snapshots use SNAPSHOT=append.
|
|
15
|
+
*
|
|
14
16
|
* You can use SNAPSHOT=ignore to neither read not write snapshots, for testing on real
|
|
15
17
|
* network operations.
|
|
16
18
|
*
|
|
@@ -33,7 +35,7 @@ const { createHash } = require('node:crypto');
|
|
|
33
35
|
const { promises: fs } = require('node:fs');
|
|
34
36
|
const { resolve } = require('node:path');
|
|
35
37
|
|
|
36
|
-
// Environment variable SNAPSHOT = update / ignore / read (default)
|
|
38
|
+
// Environment variable SNAPSHOT = update / append / ignore / read (default)
|
|
37
39
|
const SNAPSHOT = process.env.SNAPSHOT || 'read';
|
|
38
40
|
const { LOG_REQ, LOG_SNAPSHOT } = process.env;
|
|
39
41
|
const unusedSnapshotsLogFile = 'unused-snapshots.log';
|
|
@@ -79,8 +81,6 @@ let snapshotDirectory = null;
|
|
|
79
81
|
* @typedef {SnapshotText | SnapshotJson} Snapshot
|
|
80
82
|
*/
|
|
81
83
|
|
|
82
|
-
/** @type {(res: any) => any} */
|
|
83
|
-
const identity = (response) => response;
|
|
84
84
|
|
|
85
85
|
const defaultKeyDerivationProps = ['method', 'url', 'body'];
|
|
86
86
|
/**
|
|
@@ -112,10 +112,6 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Dynamically changeable props
|
|
115
|
-
/**
|
|
116
|
-
* @type {(response: Response, request: Request) => Promise<Response>}
|
|
117
|
-
*/
|
|
118
|
-
let responseTransformer = identity;
|
|
119
115
|
/**
|
|
120
116
|
* @type {(req: Request) => Promise<{ filePrefix: string, fileSuffixKey: string }>}
|
|
121
117
|
*/
|
|
@@ -255,6 +251,7 @@ async function readSnapshot(request) {
|
|
|
255
251
|
// Fail any test that fires a real network request (without snapshot)
|
|
256
252
|
// @ts-ignore
|
|
257
253
|
if (err.code === 'ENOENT') {
|
|
254
|
+
if (SNAPSHOT === 'append') return {};
|
|
258
255
|
const reqBody = await request.clone().text();
|
|
259
256
|
console.error('No network snapshot found for request with cache keys:', {
|
|
260
257
|
request: {
|
|
@@ -296,7 +293,7 @@ async function sendResponse(request, snapshot) {
|
|
|
296
293
|
},
|
|
297
294
|
} = snapshot;
|
|
298
295
|
|
|
299
|
-
|
|
296
|
+
const newResponse = new Response(
|
|
300
297
|
responseType === 'json'
|
|
301
298
|
? JSON.stringify(body)
|
|
302
299
|
: /** @type {string} */ (body),
|
|
@@ -307,8 +304,6 @@ async function sendResponse(request, snapshot) {
|
|
|
307
304
|
},
|
|
308
305
|
);
|
|
309
306
|
|
|
310
|
-
newResponse = await responseTransformer(newResponse, request);
|
|
311
|
-
|
|
312
307
|
// respondWith is a method added by @mswjs/interceptors
|
|
313
308
|
// @ts-ignore
|
|
314
309
|
request.respondWith(newResponse);
|
|
@@ -320,7 +315,10 @@ async function sendResponse(request, snapshot) {
|
|
|
320
315
|
*/
|
|
321
316
|
async function readSnapshotAndSendResponse(request) {
|
|
322
317
|
const { snapshot } = await readSnapshot(request);
|
|
323
|
-
|
|
318
|
+
if (snapshot) {
|
|
319
|
+
return sendResponse(request, snapshot);
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
/**
|
|
@@ -402,26 +400,6 @@ function resetSnapshotFilenameGenerator() {
|
|
|
402
400
|
snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
403
401
|
}
|
|
404
402
|
|
|
405
|
-
/**
|
|
406
|
-
* Attach response transformer function.
|
|
407
|
-
*
|
|
408
|
-
* Here is an opportunity to modify the response (loaded from snapshot) on-the-fly right before
|
|
409
|
-
* the response is sent to consumers.
|
|
410
|
-
*
|
|
411
|
-
* WARNING: Attaching a function on a per-test basis may not be concurrent safe. i.e. If you tests
|
|
412
|
-
* run sequentially, then it is safe. But if your test runner runs test suites concurrently,
|
|
413
|
-
* then it is better to attach a function only once ever.
|
|
414
|
-
* @param {(response: Response, request: Request) => Promise<Response>} func
|
|
415
|
-
*/
|
|
416
|
-
function attachResponseTransformer(func) {
|
|
417
|
-
responseTransformer = func;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/** Reset response transformer */
|
|
421
|
-
function resetResponseTransformer() {
|
|
422
|
-
responseTransformer = identity;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
403
|
/**
|
|
426
404
|
* Start the interceptor
|
|
427
405
|
* @param {object} opts
|
|
@@ -449,7 +427,7 @@ function start({
|
|
|
449
427
|
|
|
450
428
|
// @ts-ignore
|
|
451
429
|
interceptor.on('request', async ({ request }) => {
|
|
452
|
-
if (
|
|
430
|
+
if (['read', 'append'].includes(SNAPSHOT)) {
|
|
453
431
|
await readSnapshotAndSendResponse(request);
|
|
454
432
|
}
|
|
455
433
|
});
|
|
@@ -458,8 +436,8 @@ function start({
|
|
|
458
436
|
'response',
|
|
459
437
|
/** @type {(params: { request: Request, response: Response }) => Promise<void>} */
|
|
460
438
|
async ({ request, response }) => {
|
|
439
|
+
const { fileName, fileSuffixKey } = await getSnapshotFileName(request);
|
|
461
440
|
if (LOG_REQ) {
|
|
462
|
-
const { fileName, fileSuffixKey } = await getSnapshotFileName(request);
|
|
463
441
|
const summary = `----------\n${request.method} ${request.url}\nWould use file name: ${fileName}`;
|
|
464
442
|
if (LOG_REQ === 'summary') {
|
|
465
443
|
console.debug(summary);
|
|
@@ -481,7 +459,7 @@ function start({
|
|
|
481
459
|
});
|
|
482
460
|
}
|
|
483
461
|
}
|
|
484
|
-
if (SNAPSHOT === 'update') {
|
|
462
|
+
if (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName))) {
|
|
485
463
|
if (!dirCreatePromise) {
|
|
486
464
|
dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
|
|
487
465
|
}
|
|
@@ -506,8 +484,6 @@ module.exports = {
|
|
|
506
484
|
defaultSnapshotFileNameGenerator,
|
|
507
485
|
attachSnapshotFilenameGenerator,
|
|
508
486
|
resetSnapshotFilenameGenerator,
|
|
509
|
-
attachResponseTransformer,
|
|
510
|
-
resetResponseTransformer,
|
|
511
487
|
start,
|
|
512
488
|
stop,
|
|
513
489
|
};
|