http-snapshotter 0.5.2 → 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 +87 -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
|
|
@@ -595,6 +613,36 @@ function resetSnapshotFilenameGenerator() {
|
|
|
595
613
|
snapshotFileNameGenerator = defaultSnapshotFileNameGenerator;
|
|
596
614
|
}
|
|
597
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
|
+
|
|
598
646
|
/**
|
|
599
647
|
* Start the interceptor
|
|
600
648
|
* @param {object} opts
|
|
@@ -621,10 +669,35 @@ function start({
|
|
|
621
669
|
});
|
|
622
670
|
|
|
623
671
|
const cache = /** @type {WeakMap<Request, SnapshotFileInfo>} */ (new WeakMap());
|
|
672
|
+
const ignoredRequests = /** @type {WeakSet<Request>} */ (new WeakSet());
|
|
624
673
|
|
|
625
674
|
//@ts-ignore
|
|
626
675
|
interceptor.on('request', async ({ request, controller }) => {
|
|
627
|
-
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) {
|
|
628
701
|
const snapshotFileInfo = await getSnapshotFileInfo(request);
|
|
629
702
|
cache.set(request, snapshotFileInfo);
|
|
630
703
|
await readSnapshotAndSendResponse(request, controller, snapshotFileInfo);
|
|
@@ -635,15 +708,23 @@ function start({
|
|
|
635
708
|
'response',
|
|
636
709
|
/** @type {(params: { request: Request, response: Response }) => Promise<void>} */
|
|
637
710
|
async ({ request, response }) => {
|
|
711
|
+
// Check if this request was marked to ignore snapshots
|
|
712
|
+
const shouldIgnoreSnapshot = ignoredRequests.has(request);
|
|
713
|
+
|
|
638
714
|
const snapshotFileInfo = cache.get(request) || (await getSnapshotFileInfo(request));
|
|
639
715
|
cache.delete(request);
|
|
716
|
+
|
|
640
717
|
const {
|
|
641
718
|
// absoluteFilePath,
|
|
642
719
|
fileName,
|
|
643
720
|
fileSuffixKey,
|
|
644
721
|
} = snapshotFileInfo;
|
|
645
722
|
if (LOG_REQ) {
|
|
646
|
-
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
|
+
}`;
|
|
647
728
|
if (LOG_REQ === '1' || LOG_REQ === 'summary') {
|
|
648
729
|
console.debug(summary);
|
|
649
730
|
} else if (LOG_REQ === 'detailed') {
|
|
@@ -664,7 +745,7 @@ function start({
|
|
|
664
745
|
});
|
|
665
746
|
}
|
|
666
747
|
}
|
|
667
|
-
if (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName))) {
|
|
748
|
+
if (!shouldIgnoreSnapshot && (SNAPSHOT === 'update' || (SNAPSHOT === 'append' && !readFiles.has(fileName)))) {
|
|
668
749
|
if (!dirCreatePromise) {
|
|
669
750
|
dirCreatePromise = fs.mkdir( /** @type {string} */(snapshotDirectory), { recursive: true });
|
|
670
751
|
}
|
|
@@ -691,6 +772,9 @@ module.exports = {
|
|
|
691
772
|
defaultSnapshotFileNameGenerator,
|
|
692
773
|
attachSnapshotFilenameGenerator,
|
|
693
774
|
resetSnapshotFilenameGenerator,
|
|
775
|
+
defaultSnapshotIgnoreRules,
|
|
776
|
+
attachSnapshotIgnoreRules,
|
|
777
|
+
resetSnapshotIgnoreRules,
|
|
694
778
|
start,
|
|
695
779
|
stop,
|
|
696
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.
|
|
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
|