http-snapshotter 0.2.3 → 0.3.0
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 -32
- package/index.d.ts +11 -18
- package/index.js +35 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ There is also a `SNAPSHOT=ignore` option to neither read nor write from snapshot
|
|
|
53
53
|
|
|
54
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.
|
|
55
55
|
|
|
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
|
+
|
|
56
58
|
Once you are done writing your tests, run your test runner on all your tests and then take a look at `<snapshots directory>/unused-snapshots.log` file to see which snapshot files haven't been used by your final test suite. You can delete unused snapshot files.
|
|
57
59
|
|
|
58
60
|
The tests of this library uses this library itself, check the `tests/` directory and try the tests `npm ci; npm test`.
|
|
@@ -116,44 +118,42 @@ There are scenarios where one needs to test varied response for the same call (e
|
|
|
116
118
|
|
|
117
119
|
There are 2 ways to go about this:
|
|
118
120
|
|
|
119
|
-
Method 1: The easy way
|
|
120
|
-
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:
|
|
121
122
|
|
|
122
123
|
```js
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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';
|
|
128
139
|
|
|
129
140
|
test('Test behavior on a free account', async (t) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
JSON.stringify({
|
|
139
|
-
...(await response.clone().json()),
|
|
140
|
-
free_user: true,
|
|
141
|
-
}),
|
|
142
|
-
{
|
|
143
|
-
headers: response.headers
|
|
144
|
-
}
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return response;
|
|
149
|
-
};
|
|
150
|
-
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
|
+
});
|
|
151
149
|
|
|
152
|
-
//
|
|
153
|
-
// assert
|
|
150
|
+
// write the test here
|
|
151
|
+
// t.assert(await enablePaidFeature(), { error: 'Free accounts do not have access to this paid feature' })
|
|
154
152
|
|
|
155
|
-
// cleanup before moving to next test
|
|
156
|
-
|
|
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();
|
|
157
157
|
});
|
|
158
158
|
```
|
|
159
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
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
* Here onwards run test runner without SNAPSHOT env variable or SNAPSHOT=read
|
|
14
14
|
* You can use SNAPSHOT=ignore to neither read not write snapshots, for testing on real
|
|
15
15
|
* network operations.
|
|
16
|
+
*
|
|
17
|
+
* Log read/saved snapshots by setting LOG_SNAPSHOT=1 env variable.
|
|
16
18
|
*
|
|
17
19
|
* Unused snapshot files will be written into a log file named 'unused-snapshots.log'.
|
|
18
20
|
* You can delete those files manually.
|
|
19
|
-
*
|
|
20
|
-
* Log requests with LOG_REQ=1 env variable
|
|
21
|
+
*
|
|
22
|
+
* Log requests with LOG_REQ=1 or LOG_REQ=summary (to just print summary) env variable
|
|
21
23
|
* or node.js built-in NODE_DEBUG=http,http2
|
|
22
24
|
*
|
|
23
25
|
* More docs at the end of this file, find the exported methods.
|
|
@@ -33,7 +35,7 @@ const { resolve } = require('node:path');
|
|
|
33
35
|
|
|
34
36
|
// Environment variable SNAPSHOT = update / ignore / read (default)
|
|
35
37
|
const SNAPSHOT = process.env.SNAPSHOT || 'read';
|
|
36
|
-
const
|
|
38
|
+
const { LOG_REQ, LOG_SNAPSHOT } = process.env;
|
|
37
39
|
const unusedSnapshotsLogFile = 'unused-snapshots.log';
|
|
38
40
|
/**
|
|
39
41
|
* @type {import("node:fs").PathLike | null}
|
|
@@ -77,8 +79,6 @@ let snapshotDirectory = null;
|
|
|
77
79
|
* @typedef {SnapshotText | SnapshotJson} Snapshot
|
|
78
80
|
*/
|
|
79
81
|
|
|
80
|
-
/** @type {(res: any) => any} */
|
|
81
|
-
const identity = (response) => response;
|
|
82
82
|
|
|
83
83
|
const defaultKeyDerivationProps = ['method', 'url', 'body'];
|
|
84
84
|
/**
|
|
@@ -110,10 +110,6 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// Dynamically changeable props
|
|
113
|
-
/**
|
|
114
|
-
* @type {(response: Response, request: Request) => Promise<Response>}
|
|
115
|
-
*/
|
|
116
|
-
let responseTransformer = identity;
|
|
117
113
|
/**
|
|
118
114
|
* @type {(req: Request) => Promise<{ filePrefix: string, fileSuffixKey: string }>}
|
|
119
115
|
*/
|
|
@@ -159,13 +155,16 @@ const readFiles = new Set();
|
|
|
159
155
|
*/
|
|
160
156
|
async function saveSnapshot(request, response) {
|
|
161
157
|
const { absoluteFilePath, fileName, fileSuffixKey } = await getSnapshotFileName(request);
|
|
162
|
-
// console.log(fileName);
|
|
163
158
|
|
|
164
159
|
// Prevent multiple tests from having same snapshot
|
|
165
160
|
if (alreadyWrittenFiles.has(absoluteFilePath)) {
|
|
166
161
|
return /** @type {ReadSnapshotReturnType} */ (alreadyWrittenFiles.get(absoluteFilePath));
|
|
167
162
|
}
|
|
168
163
|
|
|
164
|
+
if (LOG_SNAPSHOT) {
|
|
165
|
+
console.debug('Writing:', fileName);
|
|
166
|
+
}
|
|
167
|
+
|
|
169
168
|
/** @returns {ReadSnapshotReturnType} */
|
|
170
169
|
const saveFreshSnapshot = async () => {
|
|
171
170
|
let requestBody;
|
|
@@ -238,9 +237,11 @@ const snapshotCache = {};
|
|
|
238
237
|
*/
|
|
239
238
|
async function readSnapshot(request) {
|
|
240
239
|
const { absoluteFilePath, fileName, fileSuffixKey } = await getSnapshotFileName(request);
|
|
241
|
-
// console.log(fileName);
|
|
242
240
|
|
|
243
241
|
if (!snapshotCache[absoluteFilePath]) {
|
|
242
|
+
if (LOG_SNAPSHOT) {
|
|
243
|
+
console.debug('Reading:', fileName);
|
|
244
|
+
}
|
|
244
245
|
let json;
|
|
245
246
|
try {
|
|
246
247
|
json = await fs.readFile(absoluteFilePath, 'utf-8');
|
|
@@ -256,8 +257,8 @@ async function readSnapshot(request) {
|
|
|
256
257
|
headers: Object.fromEntries([...request.headers.entries()]),
|
|
257
258
|
body: reqBody,
|
|
258
259
|
},
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
fileName,
|
|
261
|
+
fileSuffixKey,
|
|
261
262
|
});
|
|
262
263
|
throw new Error('Network request not mocked');
|
|
263
264
|
} else {
|
|
@@ -289,7 +290,7 @@ async function sendResponse(request, snapshot) {
|
|
|
289
290
|
},
|
|
290
291
|
} = snapshot;
|
|
291
292
|
|
|
292
|
-
|
|
293
|
+
const newResponse = new Response(
|
|
293
294
|
responseType === 'json'
|
|
294
295
|
? JSON.stringify(body)
|
|
295
296
|
: /** @type {string} */ (body),
|
|
@@ -300,8 +301,6 @@ async function sendResponse(request, snapshot) {
|
|
|
300
301
|
},
|
|
301
302
|
);
|
|
302
303
|
|
|
303
|
-
newResponse = await responseTransformer(newResponse, request);
|
|
304
|
-
|
|
305
304
|
// respondWith is a method added by @mswjs/interceptors
|
|
306
305
|
// @ts-ignore
|
|
307
306
|
request.respondWith(newResponse);
|
|
@@ -395,26 +394,6 @@ function resetSnapshotFilenameGenerator() {
|
|
|
395
394
|
snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
396
395
|
}
|
|
397
396
|
|
|
398
|
-
/**
|
|
399
|
-
* Attach response transformer function.
|
|
400
|
-
*
|
|
401
|
-
* Here is an opportunity to modify the response (loaded from snapshot) on-the-fly right before
|
|
402
|
-
* the response is sent to consumers.
|
|
403
|
-
*
|
|
404
|
-
* WARNING: Attaching a function on a per-test basis may not be concurrent safe. i.e. If you tests
|
|
405
|
-
* run sequentially, then it is safe. But if your test runner runs test suites concurrently,
|
|
406
|
-
* then it is better to attach a function only once ever.
|
|
407
|
-
* @param {(response: Response, request: Request) => Promise<Response>} func
|
|
408
|
-
*/
|
|
409
|
-
function attachResponseTransformer(func) {
|
|
410
|
-
responseTransformer = func;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** Reset response transformer */
|
|
414
|
-
function resetResponseTransformer() {
|
|
415
|
-
responseTransformer = identity;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
397
|
/**
|
|
419
398
|
* Start the interceptor
|
|
420
399
|
* @param {object} opts
|
|
@@ -453,16 +432,26 @@ function start({
|
|
|
453
432
|
async ({ request, response }) => {
|
|
454
433
|
if (LOG_REQ) {
|
|
455
434
|
const { fileName, fileSuffixKey } = await getSnapshotFileName(request);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
435
|
+
const summary = `----------\n${request.method} ${request.url}\nWould use file name: ${fileName}`;
|
|
436
|
+
if (LOG_REQ === 'summary') {
|
|
437
|
+
console.debug(summary);
|
|
438
|
+
} else {
|
|
439
|
+
console.debug(`${summary}\n----------\n`, {
|
|
440
|
+
request: {
|
|
441
|
+
url: request.url,
|
|
442
|
+
method: request.method,
|
|
443
|
+
headers: Object.fromEntries([...request.headers.entries()]),
|
|
444
|
+
body: await request.clone().text(),
|
|
445
|
+
},
|
|
446
|
+
response: {
|
|
447
|
+
status: response.status,
|
|
448
|
+
statusText: response.statusText,
|
|
449
|
+
headers: Object.fromEntries([...response.headers.entries()]),
|
|
450
|
+
body: await response.clone().text(),
|
|
451
|
+
},
|
|
452
|
+
wouldUseFileSuffixKey: fileSuffixKey,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
466
455
|
}
|
|
467
456
|
if (SNAPSHOT === 'update') {
|
|
468
457
|
if (!dirCreatePromise) {
|
|
@@ -489,8 +478,6 @@ module.exports = {
|
|
|
489
478
|
defaultSnapshotFileNameGenerator,
|
|
490
479
|
attachSnapshotFilenameGenerator,
|
|
491
480
|
resetSnapshotFilenameGenerator,
|
|
492
|
-
attachResponseTransformer,
|
|
493
|
-
resetResponseTransformer,
|
|
494
481
|
start,
|
|
495
482
|
stop,
|
|
496
483
|
};
|