http-snapshotter 0.5.1 → 0.5.3-beta.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/index.js +88 -3
- package/intercept.js +78 -0
- package/package.json +4 -1
- /package/{test.js → playground.js} +0 -0
package/index.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* You can use SNAPSHOT=ignore to neither read not write snapshots, for testing on real
|
|
17
17
|
* network operations.
|
|
18
18
|
*
|
|
19
|
+
* To ignore specific requests from being snapshotted in SNAPSHOT=update or SNAPSHOT=append mode,
|
|
20
|
+
* use attachSnapshotIgnoreRules() to provide a function that receives the request object and
|
|
21
|
+
* returns true if the request should be ignored from snapshotting.
|
|
22
|
+
*
|
|
19
23
|
* Log read/saved snapshots by setting LOG_SNAPSHOT=1 env variable.
|
|
20
24
|
*
|
|
21
25
|
* Unused snapshot files will be written into a log file named 'unused-snapshots.log'.
|
|
@@ -135,6 +139,15 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
135
139
|
};
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Default snapshot ignore rules - by default no requests are ignored
|
|
144
|
+
* @param {Request} request
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function defaultSnapshotIgnoreRules(request) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
// Dynamically changeable props
|
|
139
152
|
/**
|
|
140
153
|
* @type {(req: Request) => Promise<{ filePrefix: string, fileSuffixKey: string }>}
|
|
@@ -142,6 +155,11 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
142
155
|
let snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
143
156
|
let snapshotSubDirectory = '';
|
|
144
157
|
|
|
158
|
+
/**
|
|
159
|
+
* @type {(req: Request) => boolean}
|
|
160
|
+
*/
|
|
161
|
+
let snapshotIgnoreRules = defaultSnapshotIgnoreRules;
|
|
162
|
+
|
|
145
163
|
/**
|
|
146
164
|
* @typedef SnapshotFileInfo
|
|
147
165
|
* @property {string} absoluteFilePath
|
|
@@ -318,6 +336,7 @@ async function readSnapshot(request, snapshotFileInfo) {
|
|
|
318
336
|
`${colors.yellow}Below is the diff between the two's fileSuffixKey that is used for computing the hash of the file name:${colors.reset}`,
|
|
319
337
|
showColoredDiff(match.differences),
|
|
320
338
|
]),
|
|
339
|
+
'',
|
|
321
340
|
] : []).join('\n');
|
|
322
341
|
console.error(
|
|
323
342
|
`${colors.red}No network snapshot found for following request:${colors.reset}`,
|
|
@@ -594,6 +613,36 @@ function resetSnapshotFilenameGenerator() {
|
|
|
594
613
|
snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
595
614
|
}
|
|
596
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Attach snapshot ignore rules function
|
|
618
|
+
*
|
|
619
|
+
* Here's your opportunity to define custom rules for ignoring requests from being snapshotted.
|
|
620
|
+
* The function receives the Request object and should return true if the request should be ignored.
|
|
621
|
+
*
|
|
622
|
+
* IMPORTANT: Behavior varies by SNAPSHOT mode:
|
|
623
|
+
* - SNAPSHOT=update/append: Ignored requests make real network calls but don't create snapshots
|
|
624
|
+
* - SNAPSHOT=read: Ignored requests throw an error (tests shouldn't make real network calls)
|
|
625
|
+
*
|
|
626
|
+
* Use cases (not limited to):
|
|
627
|
+
* 1. Ignore requests with specific headers (e.g., x-debug-mode: no-snapshot)
|
|
628
|
+
* 2. Ignore requests to specific URLs or domains
|
|
629
|
+
* 3. Ignore requests with specific HTTP methods
|
|
630
|
+
* 4. Ignore requests based on request body content
|
|
631
|
+
*
|
|
632
|
+
* WARNING: Attaching a function on a per-test basis may not be concurrent safe. i.e. If your tests
|
|
633
|
+
* run sequentially, then it is safe. But if your test runner runs test suites concurrently,
|
|
634
|
+
* then it is better to attach a function only once ever.
|
|
635
|
+
* @param {(req: Request) => boolean} func
|
|
636
|
+
*/
|
|
637
|
+
function attachSnapshotIgnoreRules(func) {
|
|
638
|
+
snapshotIgnoreRules = func;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Reset snapshot ignore rules to default (no requests ignored) */
|
|
642
|
+
function resetSnapshotIgnoreRules() {
|
|
643
|
+
snapshotIgnoreRules = defaultSnapshotIgnoreRules;
|
|
644
|
+
}
|
|
645
|
+
|
|
597
646
|
/**
|
|
598
647
|
* Start the interceptor
|
|
599
648
|
* @param {object} opts
|
|
@@ -620,10 +669,35 @@ function start({
|
|
|
620
669
|
});
|
|
621
670
|
|
|
622
671
|
const cache = /** @type {WeakMap<Request, SnapshotFileInfo>} */ (new WeakMap());
|
|
672
|
+
const ignoredRequests = /** @type {WeakSet<Request>} */ (new WeakSet());
|
|
623
673
|
|
|
624
674
|
//@ts-ignore
|
|
625
675
|
interceptor.on('request', async ({ request, controller }) => {
|
|
626
|
-
if
|
|
676
|
+
// Check if request should be ignored from snapshotting using ignore rules
|
|
677
|
+
const shouldIgnoreSnapshot = snapshotIgnoreRules(request);
|
|
678
|
+
|
|
679
|
+
// Track ignored requests
|
|
680
|
+
if (shouldIgnoreSnapshot) {
|
|
681
|
+
ignoredRequests.add(request);
|
|
682
|
+
|
|
683
|
+
// In read mode, we should not allow ignored requests to make real network calls
|
|
684
|
+
if (SNAPSHOT === 'read') {
|
|
685
|
+
console.error(
|
|
686
|
+
`${colors.red}Request ignored by snapshot ignore rules but SNAPSHOT=read mode doesn't allow real network requests:${colors.reset}`,
|
|
687
|
+
{
|
|
688
|
+
request: {
|
|
689
|
+
url: request.url,
|
|
690
|
+
method: request.method,
|
|
691
|
+
headers: Object.fromEntries([...request.headers.entries()]),
|
|
692
|
+
body: await request.clone().text(),
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
throw new Error('Request ignored by snapshot ignore rules but SNAPSHOT=read mode doesn\'t allow real network requests');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (['read', 'append'].includes(SNAPSHOT) && !shouldIgnoreSnapshot) {
|
|
627
701
|
const snapshotFileInfo = await getSnapshotFileInfo(request);
|
|
628
702
|
cache.set(request, snapshotFileInfo);
|
|
629
703
|
await readSnapshotAndSendResponse(request, controller, snapshotFileInfo);
|
|
@@ -634,15 +708,23 @@ function start({
|
|
|
634
708
|
'response',
|
|
635
709
|
/** @type {(params: { request: Request, response: Response }) => Promise<void>} */
|
|
636
710
|
async ({ request, response }) => {
|
|
711
|
+
// Check if this request was marked to ignore snapshots
|
|
712
|
+
const shouldIgnoreSnapshot = ignoredRequests.has(request);
|
|
713
|
+
|
|
637
714
|
const snapshotFileInfo = cache.get(request) || (await getSnapshotFileInfo(request));
|
|
638
715
|
cache.delete(request);
|
|
716
|
+
|
|
639
717
|
const {
|
|
640
718
|
// absoluteFilePath,
|
|
641
719
|
fileName,
|
|
642
720
|
fileSuffixKey,
|
|
643
721
|
} = snapshotFileInfo;
|
|
644
722
|
if (LOG_REQ) {
|
|
645
|
-
const summary = `----------\n${request.method} ${request.url}\
|
|
723
|
+
const summary = `----------\n${request.method} ${request.url}\n${
|
|
724
|
+
shouldIgnoreSnapshot
|
|
725
|
+
? 'ignored from snapshotting by ignore rules'
|
|
726
|
+
: `Would use file name: ${fileName}`
|
|
727
|
+
}`;
|
|
646
728
|
if (LOG_REQ === '1' || LOG_REQ === 'summary') {
|
|
647
729
|
console.debug(summary);
|
|
648
730
|
} else if (LOG_REQ === 'detailed') {
|
|
@@ -663,7 +745,7 @@ function start({
|
|
|
663
745
|
});
|
|
664
746
|
}
|
|
665
747
|
}
|
|
666
|
-
if (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName))) {
|
|
748
|
+
if (!shouldIgnoreSnapshot && (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName)))) {
|
|
667
749
|
if (!dirCreatePromise) {
|
|
668
750
|
dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
|
|
669
751
|
}
|
|
@@ -690,6 +772,9 @@ module.exports = {
|
|
|
690
772
|
defaultSnapshotFileNameGenerator,
|
|
691
773
|
attachSnapshotFilenameGenerator,
|
|
692
774
|
resetSnapshotFilenameGenerator,
|
|
775
|
+
defaultSnapshotIgnoreRules,
|
|
776
|
+
attachSnapshotIgnoreRules,
|
|
777
|
+
resetSnapshotIgnoreRules,
|
|
693
778
|
start,
|
|
694
779
|
stop,
|
|
695
780
|
};
|
package/intercept.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This function was created with a realization that once you override a method for mocking
|
|
3
|
+
* and run a test, you can't undo the override, because shared code (test runner without
|
|
4
|
+
* test isolation) will hold on to closures to the overridden function.
|
|
5
|
+
*
|
|
6
|
+
* So a solution it to intercept once before all tests, mock for a test and "undoMock()" at
|
|
7
|
+
* end of a test will cause the intercept to "proxy" future calls to the original method.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const mocked = new Set();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the keys of an object where the values of the keys are functions.
|
|
14
|
+
* @template T The object type.
|
|
15
|
+
* @typedef {Extract<
|
|
16
|
+
* keyof T,
|
|
17
|
+
* { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
|
|
18
|
+
* >} FunctionKeys
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template T
|
|
23
|
+
* @template {FunctionKeys<T>} K
|
|
24
|
+
* @param {T} object
|
|
25
|
+
* @param {K} methodName
|
|
26
|
+
*/
|
|
27
|
+
function intercept(object, methodName) {
|
|
28
|
+
let mockHandler = null;
|
|
29
|
+
const originalMethod = object[methodName];
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const boundedMethod = object[methodName].bind(object);
|
|
32
|
+
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
// eslint-disable-next-line no-param-reassign
|
|
35
|
+
object[methodName] = function interceptedFunction(...args) {
|
|
36
|
+
return mockHandler
|
|
37
|
+
? mockHandler(boundedMethod, ...args)
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
: originalMethod.apply(object, args);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/**
|
|
44
|
+
* @param {(originalMethod: T[K], ...args: Parameters<T[K]>) => any} mockFunction
|
|
45
|
+
*/
|
|
46
|
+
mock(mockFunction) {
|
|
47
|
+
mockHandler = mockFunction;
|
|
48
|
+
mocked.add(this);
|
|
49
|
+
},
|
|
50
|
+
undoMock() {
|
|
51
|
+
mockHandler = null;
|
|
52
|
+
mocked.delete(this);
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Un-intercepting will not get rid of closures to interceptedFunction
|
|
56
|
+
*/
|
|
57
|
+
destroy() {
|
|
58
|
+
mockHandler = null;
|
|
59
|
+
// eslint-disable-next-line no-param-reassign
|
|
60
|
+
object[methodName] = originalMethod;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function undoAllMocks() {
|
|
66
|
+
[...mocked].forEach((methods) => methods.undoMock());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const interceptAllMethods = (object) => Object.fromEntries(
|
|
70
|
+
Object
|
|
71
|
+
.keys(object)
|
|
72
|
+
.filter((key) => typeof object[key] === 'function')
|
|
73
|
+
.map(
|
|
74
|
+
(key) => [key, intercept(object, key)],
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
module.exports = { intercept, undoAllMocks, interceptAllMethods };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "http-snapshotter",
|
|
3
|
-
"version": "0.5.1",
|
|
3
|
+
"version": "0.5.3-beta.1",
|
|
4
4
|
"description": "Snapshot HTTP requests for tests (node.js)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"require": "./index.js",
|
|
10
10
|
"types": "./index.d.ts"
|
|
11
11
|
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
12
15
|
"scripts": {
|
|
13
16
|
"test": "tape tests/**/*.test.* | tap-arc",
|
|
14
17
|
"tsc": "tsc"
|
|
File without changes
|