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.
Files changed (4) hide show
  1. package/README.md +32 -32
  2. package/index.d.ts +11 -18
  3. package/index.js +35 -48
  4. 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 it to not touch the existing snapshot file, and use `attachResponseTransformer` to
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
- import {
124
- // ...
125
- attachResponseTransformer,
126
- resetResponseTransformer,
127
- } from "http-snapshotter";
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
- * @param {Response} response https://developer.mozilla.org/en-US/docs/Web/API/Response
132
- * @param {Request} request https://developer.mozilla.org/en-US/docs/Web/API/Request
133
- */
134
- const interceptResponse = async (response, request) => {
135
- const url = new URL(request.url);
136
- if (request.method === 'GET' && url.pathname === '/account') {
137
- return new Response(
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
- // make fetch() call here
153
- // assert the test
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
- resetResponseTransformer();
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 LOG_REQ = process.env.LOG_REQ === '1' || process.env.LOG_REQ === 'true';
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
- wouldBeFileSuffixKey: fileSuffixKey,
260
- wouldBeFileName: fileName,
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
- let newResponse = new Response(
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
- console.debug('Request', {
457
- request: {
458
- url: request.url,
459
- method: request.method,
460
- headers: Object.fromEntries([...request.headers.entries()]),
461
- body: await request.clone().text(),
462
- },
463
- wouldBeFileName: fileName,
464
- wouldBeFileSuffixKey: fileSuffixKey,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "http-snapshotter",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Snapshot HTTP requests for tests (node.js)",
5
5
  "main": "index.cjs",
6
6
  "types": "index.d.ts",