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.
Files changed (4) hide show
  1. package/README.md +32 -34
  2. package/index.d.ts +11 -18
  3. package/index.js +13 -37
  4. 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. This could be useful while writing a new test.
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 it to not touch the existing snapshot file, and use `attachResponseTransformer` to
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
- import {
126
- // ...
127
- attachResponseTransformer,
128
- resetResponseTransformer,
129
- } 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';
130
139
 
131
140
  test('Test behavior on a free account', async (t) => {
132
- /**
133
- * @param {Response} response https://developer.mozilla.org/en-US/docs/Web/API/Response
134
- * @param {Request} request https://developer.mozilla.org/en-US/docs/Web/API/Request
135
- */
136
- const interceptResponse = async (response, request) => {
137
- const url = new URL(request.url);
138
- if (request.method === 'GET' && url.pathname === '/account') {
139
- return new Response(
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
- // make fetch() call here
155
- // assert the test
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
- 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();
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
- let newResponse = new Response(
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
- return sendResponse(request, snapshot);
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 (SNAPSHOT === 'read') {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "http-snapshotter",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Snapshot HTTP requests for tests (node.js)",
5
5
  "main": "index.cjs",
6
6
  "types": "index.d.ts",