http-snapshotter 0.5.3 → 0.6.0-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/README.md +12 -9
- package/index.js +22 -1
- package/package.json +6 -1
- package/intercept.js +0 -78
- package/playground.js +0 -41
package/README.md
CHANGED
|
@@ -20,24 +20,25 @@ Example (test.js):
|
|
|
20
20
|
|
|
21
21
|
```js
|
|
22
22
|
import test from "tape";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
import { resolve, dirname } from "node:path";
|
|
25
23
|
import { start, startTestCase, endTestCase } from "http-snapshotter";
|
|
24
|
+
import { resolve } from "node:path";
|
|
26
25
|
|
|
27
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
26
|
// if you are using an isolated test runner, then use a different directory per test (e.g. http-snapshots/test-case-1)
|
|
29
|
-
|
|
27
|
+
// you can run this only once per process
|
|
28
|
+
start({ snapshotDirectory: resolve(import.meta.dirname, "http-snapshots") });
|
|
30
29
|
|
|
31
30
|
test("Latest XKCD comic (ESM)", async (t) => {
|
|
32
31
|
// if you are *not* using an isolated test runner (e.g. tape), then `startTestCase` adds snapshots to separate directory
|
|
33
32
|
// Remove this line if it doesn't apply to your test runner
|
|
34
33
|
startTestCase('test-case-1');
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch("https://xkcd.com/info.0.json");
|
|
37
|
+
const json = await res.json();
|
|
38
|
+
t.deepEquals(json.title, "Iceberg Efficiency", "must be equal");
|
|
39
|
+
} finally {
|
|
40
|
+
endTestCase();
|
|
41
|
+
}
|
|
41
42
|
});
|
|
42
43
|
```
|
|
43
44
|
|
|
@@ -68,6 +69,8 @@ The tests of this library uses this library itself, check the `tests/` directory
|
|
|
68
69
|
|
|
69
70
|
2. Stripe SDK uses node http module in a way MSW interceptor cannot mock - [reference](https://github.com/mswjs/msw/issues/2259#issuecomment-2379379566). Solution mentioned in a comment there is to use Stripe SDK with `fetch` client - [reference](https://github.com/mswjs/msw/issues/2259#issuecomment-2422672039). Another workaround is to mock Stripe SDK's methods with your testing library of choice.
|
|
70
71
|
|
|
72
|
+
3. You can't use MSW along with this library, because there can only be one MSW interceptor running.
|
|
73
|
+
|
|
71
74
|
## About snapshot files and its names
|
|
72
75
|
|
|
73
76
|
A snapshot file name uniquely identifies a request. By default it is a combination of HTTP method + URL + body that makes a request unique (headers are ignored).
|
package/index.js
CHANGED
|
@@ -98,6 +98,19 @@ let snapshotDirectory = null;
|
|
|
98
98
|
|
|
99
99
|
const dynamodbHostNameRegex = /^(?:dynamodb|\d+\.ddb)\.([^.]+)\.amazonaws\.com$/;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Stable JSON stringify with sorted keys for deterministic hashing
|
|
103
|
+
* @param {any} val
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function stableStringify(val) {
|
|
107
|
+
if (typeof val !== 'object' || val === null) return JSON.stringify(val);
|
|
108
|
+
if (Array.isArray(val)) return '[' + val.map(stableStringify).join(',') + ']';
|
|
109
|
+
return '{' + Object.keys(val).sort().map(k =>
|
|
110
|
+
JSON.stringify(k) + ':' + stableStringify(val[k])
|
|
111
|
+
).join(',') + '}';
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
const defaultKeyDerivationProps = ['method', 'url', 'body'];
|
|
102
115
|
/**
|
|
103
116
|
* @param {Request} request
|
|
@@ -124,8 +137,16 @@ async function defaultSnapshotFileNameGenerator(request) {
|
|
|
124
137
|
|
|
125
138
|
// Input data
|
|
126
139
|
const dataList = await Promise.all(
|
|
127
|
-
defaultKeyDerivationProps.map((key) => {
|
|
140
|
+
defaultKeyDerivationProps.map(async (key) => {
|
|
128
141
|
if (key === 'body') {
|
|
142
|
+
const contentType = request.headers.get('content-type') || '';
|
|
143
|
+
if (contentType.includes('application/json') || contentType.includes('application/x-amz-json')) {
|
|
144
|
+
try {
|
|
145
|
+
return stableStringify(await request.clone().json());
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Not valid JSON, fall back to text
|
|
148
|
+
}
|
|
149
|
+
}
|
|
129
150
|
return request.clone().text();
|
|
130
151
|
}
|
|
131
152
|
//@ts-ignore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "http-snapshotter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-beta.1",
|
|
4
4
|
"description": "Snapshot HTTP requests for tests (node.js)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"index.mjs",
|
|
18
|
+
"index.d.ts"
|
|
19
|
+
],
|
|
15
20
|
"scripts": {
|
|
16
21
|
"test": "tape tests/**/*.test.* | tap-arc",
|
|
17
22
|
"tsc": "tsc"
|
package/intercept.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
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/playground.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const { diffChars } = require('diff');
|
|
2
|
-
|
|
3
|
-
function findMostSimilar(target, candidates) {
|
|
4
|
-
// Function to calculate similarity score based on diff
|
|
5
|
-
function getSimilarityScore(str1, str2) {
|
|
6
|
-
const changes = diffChars(str1, str2);
|
|
7
|
-
let totalChanges = 0;
|
|
8
|
-
let totalLength = 0;
|
|
9
|
-
|
|
10
|
-
changes.forEach(change => {
|
|
11
|
-
if (change.added || change.removed) {
|
|
12
|
-
totalChanges += change.value.length;
|
|
13
|
-
}
|
|
14
|
-
totalLength += change.value.length;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// Return similarity as a percentage (100 = identical, 0 = completely different)
|
|
18
|
-
return 100 * (1 - totalChanges / totalLength);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Calculate scores for all candidates
|
|
22
|
-
const scores = candidates.map(candidate => ({
|
|
23
|
-
string: candidate,
|
|
24
|
-
score: getSimilarityScore(target, candidate)
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
// Sort by score descending and return the highest scoring match
|
|
28
|
-
return scores.sort((a, b) => b.score - a.score)[0];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Example usage
|
|
32
|
-
const target = "hello world";
|
|
33
|
-
const candidates = [
|
|
34
|
-
"hello word",
|
|
35
|
-
"hello there",
|
|
36
|
-
"hi world",
|
|
37
|
-
"completely different"
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const result = findMostSimilar(target, candidates);
|
|
41
|
-
console.log(`Most similar to "${target}" is "${result.string}" with score ${result.score.toFixed(2)}%`);
|