@jsenv/snapshot 2.6.6 → 2.6.7
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/package.json +7 -5
- package/src/filesystem_well_known_values.js +18 -26
- package/src/get_caller_location.js +22 -0
- package/src/main.js +4 -1
- package/src/replace_fluctuating_values.js +1 -1
- package/src/side_effects/capture_side_effects.js +6 -0
- package/src/side_effects/create_capture_side_effects.js +305 -0
- package/src/side_effects/filesystem/filesystem_side_effects.js +302 -0
- package/src/side_effects/filesystem/group_file_side_effects_per_directory.js +30 -0
- package/src/{function_side_effects → side_effects/filesystem}/spy_filesystem_calls.js +48 -25
- package/src/side_effects/log/group_log_side_effects.js +29 -0
- package/src/side_effects/log/log_side_effects.js +156 -0
- package/src/side_effects/render_logs_gif.js +18 -0
- package/src/side_effects/render_side_effects.js +435 -0
- package/src/side_effects/snapshot_side_effects.js +43 -0
- package/src/side_effects/snapshot_tests.js +115 -0
- package/src/side_effects/utils/group_side_effects.js +89 -0
- package/src/function_side_effects/function_side_effects_collector.js +0 -160
- package/src/function_side_effects/function_side_effects_renderer.js +0 -29
- package/src/function_side_effects/function_side_effects_snapshot.js +0 -302
- package/src/function_side_effects/group_file_side_effects_per_directory.js +0 -114
- package/src/function_side_effects/spy_console_calls.js +0 -89
- package/src/snapshot_scenarios.js +0 -32
- /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.js +0 -0
- /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.test.mjs +0 -0
- /package/src/{function_side_effects → side_effects}/hook_into_method.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/snapshot",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.7",
|
|
4
4
|
"description": "Snapshot testing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -34,12 +34,14 @@
|
|
|
34
34
|
"test": "node --conditions=development ./scripts/test.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/assert": "4.1.
|
|
38
|
-
"@jsenv/ast": "6.2.
|
|
37
|
+
"@jsenv/assert": "4.1.13",
|
|
38
|
+
"@jsenv/ast": "6.2.11",
|
|
39
39
|
"@jsenv/exception": "1.0.1",
|
|
40
|
-
"@jsenv/filesystem": "4.9.
|
|
41
|
-
"@jsenv/
|
|
40
|
+
"@jsenv/filesystem": "4.9.6",
|
|
41
|
+
"@jsenv/terminal-recorder": "1.4.2",
|
|
42
|
+
"@jsenv/urls": "2.5.0",
|
|
42
43
|
"@jsenv/utils": "2.1.2",
|
|
44
|
+
"ansi-regex": "6.0.1",
|
|
43
45
|
"pixelmatch": "6.0.0",
|
|
44
46
|
"prettier": "3.3.3",
|
|
45
47
|
"strip-ansi": "7.1.0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensurePathnameTrailingSlash,
|
|
3
3
|
removePathnameTrailingSlash,
|
|
4
|
+
yieldAncestorUrls,
|
|
4
5
|
} from "@jsenv/urls";
|
|
5
6
|
import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
|
|
6
7
|
import { readFileSync } from "node:fs";
|
|
@@ -114,6 +115,16 @@ export const createReplaceFilesystemWellKnownValues = ({
|
|
|
114
115
|
wellKownUrlArray.push(wellKnownUrl);
|
|
115
116
|
wellKnownPathArray.push(wellKnownPath);
|
|
116
117
|
}
|
|
118
|
+
return () => {
|
|
119
|
+
const urlIndex = wellKownUrlArray.indexOf(wellKnownUrl);
|
|
120
|
+
if (urlIndex > -1) {
|
|
121
|
+
wellKownUrlArray.splice(urlIndex, 1);
|
|
122
|
+
}
|
|
123
|
+
const pathIndex = wellKnownPathArray.indexOf(wellKnownPath);
|
|
124
|
+
if (pathIndex !== -1) {
|
|
125
|
+
wellKnownPathArray.splice(pathIndex, 1);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
117
128
|
};
|
|
118
129
|
if (rootDirectoryUrl) {
|
|
119
130
|
addWellKnownFileUrl(rootDirectoryUrl, WELL_KNOWN_ROOT);
|
|
@@ -136,11 +147,13 @@ export const createReplaceFilesystemWellKnownValues = ({
|
|
|
136
147
|
const ancestorPackages = [];
|
|
137
148
|
const cwd = cwdPath || process.cwd();
|
|
138
149
|
const cwdUrl = ensurePathnameTrailingSlash(pathToFileURL(cwd));
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
for (const ancestorUrl of yieldAncestorUrls(
|
|
151
|
+
cwdUrl,
|
|
152
|
+
ancestorPackagesRootDirectoryUrl,
|
|
153
|
+
{ yieldSelf: true },
|
|
154
|
+
)) {
|
|
155
|
+
const packageFileUrl = new URL("package.json", ancestorUrl);
|
|
156
|
+
const packageDirectoryUrl = ancestorUrl;
|
|
144
157
|
let packageFileContent;
|
|
145
158
|
try {
|
|
146
159
|
packageFileContent = readFileSync(packageFileUrl);
|
|
@@ -226,24 +239,3 @@ export const createReplaceFilesystemWellKnownValues = ({
|
|
|
226
239
|
replaceFilesystemWellKnownValues.addWellKnownFileUrl = addWellKnownFileUrl;
|
|
227
240
|
return replaceFilesystemWellKnownValues;
|
|
228
241
|
};
|
|
229
|
-
|
|
230
|
-
const getParentUrl = (url) => {
|
|
231
|
-
url = String(url);
|
|
232
|
-
// With node.js new URL('../', 'file:///C:/').href
|
|
233
|
-
// returns "file:///C:/" instead of "file:///"
|
|
234
|
-
const resource = url.slice("file://".length);
|
|
235
|
-
const slashLastIndex = resource.lastIndexOf("/");
|
|
236
|
-
if (slashLastIndex === -1) {
|
|
237
|
-
return url;
|
|
238
|
-
}
|
|
239
|
-
const lastCharIndex = resource.length - 1;
|
|
240
|
-
if (slashLastIndex === lastCharIndex) {
|
|
241
|
-
const slashBeforeLastIndex = resource.lastIndexOf("/", slashLastIndex - 1);
|
|
242
|
-
if (slashBeforeLastIndex === -1) {
|
|
243
|
-
return url;
|
|
244
|
-
}
|
|
245
|
-
return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return `file://${resource.slice(0, slashLastIndex + 1)}`;
|
|
249
|
-
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fileSystemPathToUrl, isFileSystemPath } from "@jsenv/urls";
|
|
2
|
+
|
|
3
|
+
export const getCallerLocation = (callIndex = 1) => {
|
|
4
|
+
const { prepareStackTrace } = Error;
|
|
5
|
+
Error.prepareStackTrace = (error, stack) => {
|
|
6
|
+
Error.prepareStackTrace = prepareStackTrace;
|
|
7
|
+
return stack;
|
|
8
|
+
};
|
|
9
|
+
const { stack } = new Error();
|
|
10
|
+
Error.prepareStackTrace = prepareStackTrace;
|
|
11
|
+
const callerCallsite = stack[callIndex];
|
|
12
|
+
const fileName = callerCallsite.getFileName();
|
|
13
|
+
const testCallSite = {
|
|
14
|
+
url:
|
|
15
|
+
fileName && isFileSystemPath(fileName)
|
|
16
|
+
? fileSystemPathToUrl(fileName)
|
|
17
|
+
: fileName,
|
|
18
|
+
line: callerCallsite.getLineNumber(),
|
|
19
|
+
column: callerCallsite.getColumnNumber(),
|
|
20
|
+
};
|
|
21
|
+
return testCallSite;
|
|
22
|
+
};
|
package/src/main.js
CHANGED
|
@@ -3,5 +3,8 @@ export {
|
|
|
3
3
|
takeFileSnapshot,
|
|
4
4
|
} from "./filesystem_snapshot.js";
|
|
5
5
|
export { createReplaceFilesystemWellKnownValues } from "./filesystem_well_known_values.js";
|
|
6
|
-
export {
|
|
6
|
+
export { getCallerLocation } from "./get_caller_location.js";
|
|
7
7
|
export { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
|
|
8
|
+
export { renderLogsGif } from "./side_effects/render_logs_gif.js";
|
|
9
|
+
export { snapshotSideEffects } from "./side_effects/snapshot_side_effects.js";
|
|
10
|
+
export { snapshotTests } from "./side_effects/snapshot_tests.js";
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { parseFunction } from "@jsenv/assert/src/utils/function_parser.js";
|
|
2
|
+
import { createReplaceFilesystemWellKnownValues } from "../filesystem_well_known_values.js";
|
|
3
|
+
import { filesystemSideEffects } from "./filesystem/filesystem_side_effects.js";
|
|
4
|
+
import { logSideEffects } from "./log/log_side_effects.js";
|
|
5
|
+
|
|
6
|
+
export const createCaptureSideEffects = ({
|
|
7
|
+
logEffects = true,
|
|
8
|
+
filesystemEffects = true,
|
|
9
|
+
rootDirectoryUrl,
|
|
10
|
+
replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
|
|
11
|
+
rootDirectoryUrl,
|
|
12
|
+
}),
|
|
13
|
+
} = {}) => {
|
|
14
|
+
const detectors = [];
|
|
15
|
+
if (logEffects) {
|
|
16
|
+
detectors.push(logSideEffects(logEffects === true ? {} : logEffects));
|
|
17
|
+
}
|
|
18
|
+
let filesystemSideEffectsDetector;
|
|
19
|
+
if (filesystemEffects) {
|
|
20
|
+
filesystemSideEffectsDetector = filesystemSideEffects(
|
|
21
|
+
filesystemEffects === true ? {} : filesystemEffects,
|
|
22
|
+
{
|
|
23
|
+
replaceFilesystemWellKnownValues,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
detectors.push(filesystemSideEffectsDetector);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const options = {
|
|
30
|
+
rootDirectoryUrl,
|
|
31
|
+
replaceFilesystemWellKnownValues,
|
|
32
|
+
};
|
|
33
|
+
let functionExecutingCount = 0;
|
|
34
|
+
const capture = (fn, { callSite, baseDirectory } = {}) => {
|
|
35
|
+
if (baseDirectory !== undefined && filesystemSideEffectsDetector) {
|
|
36
|
+
filesystemSideEffectsDetector.setBaseDirectory(baseDirectory);
|
|
37
|
+
}
|
|
38
|
+
const startMs = Date.now();
|
|
39
|
+
const sideEffects = [];
|
|
40
|
+
sideEffects.options = options;
|
|
41
|
+
const sideEffectTypeCounterMap = new Map();
|
|
42
|
+
const onSideEffectAdded = (sideEffect) => {
|
|
43
|
+
let counter = sideEffectTypeCounterMap.get(sideEffect.type) || 0;
|
|
44
|
+
sideEffectTypeCounterMap.set(sideEffect.type, counter + 1);
|
|
45
|
+
sideEffect.counter = counter;
|
|
46
|
+
sideEffect.delay = Date.now() - startMs;
|
|
47
|
+
};
|
|
48
|
+
const onSideEffectRemoved = () => {};
|
|
49
|
+
const addSideEffect = (sideEffect) => {
|
|
50
|
+
sideEffects.push(sideEffect);
|
|
51
|
+
onSideEffectAdded(sideEffect);
|
|
52
|
+
return sideEffect;
|
|
53
|
+
};
|
|
54
|
+
const replaceSideEffect = (existingSideEffect, newSideEffect) => {
|
|
55
|
+
const index = sideEffects.indexOf(existingSideEffect);
|
|
56
|
+
sideEffects[index] = newSideEffect;
|
|
57
|
+
onSideEffectRemoved(existingSideEffect);
|
|
58
|
+
onSideEffectAdded(newSideEffect);
|
|
59
|
+
};
|
|
60
|
+
const removeSideEffect = (sideEffect) => {
|
|
61
|
+
const index = sideEffects.indexOf(sideEffect);
|
|
62
|
+
if (index > -1) {
|
|
63
|
+
sideEffects.splice(index, 1);
|
|
64
|
+
onSideEffectRemoved(sideEffect);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
sideEffects.replaceSideEffect = replaceSideEffect;
|
|
68
|
+
sideEffects.removeSideEffect = removeSideEffect;
|
|
69
|
+
|
|
70
|
+
const sourceCode = parseFunction(fn).body;
|
|
71
|
+
addSideEffect({
|
|
72
|
+
code: "source_code",
|
|
73
|
+
type: "source_code",
|
|
74
|
+
value: { sourceCode, callSite },
|
|
75
|
+
render: {
|
|
76
|
+
md: () => {
|
|
77
|
+
return {
|
|
78
|
+
type: "source_code",
|
|
79
|
+
text: {
|
|
80
|
+
type: "source_code",
|
|
81
|
+
value: { sourceCode, callSite },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const finallyCallbackSet = new Set();
|
|
89
|
+
const addFinallyCallback = (finallyCallback) => {
|
|
90
|
+
finallyCallbackSet.add(finallyCallback);
|
|
91
|
+
};
|
|
92
|
+
const skippableHandlerSet = new Set();
|
|
93
|
+
const addSkippableHandler = (skippableHandler) => {
|
|
94
|
+
skippableHandlerSet.add(skippableHandler);
|
|
95
|
+
};
|
|
96
|
+
addFinallyCallback((sideEffects) => {
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < sideEffects.length) {
|
|
99
|
+
const sideEffect = sideEffects[i];
|
|
100
|
+
i++;
|
|
101
|
+
let skippableHandlerResult;
|
|
102
|
+
for (const skippableHandler of skippableHandlerSet) {
|
|
103
|
+
skippableHandlerResult = skippableHandler(sideEffect);
|
|
104
|
+
if (skippableHandlerResult) {
|
|
105
|
+
// there is no skippable per sideEffect type today
|
|
106
|
+
// so even if the skippable doe not skip in the end
|
|
107
|
+
// we don't have to check if an other skippable handler could
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (skippableHandlerResult) {
|
|
112
|
+
let j = i;
|
|
113
|
+
while (j < sideEffects.length) {
|
|
114
|
+
const afterSideEffect = sideEffects[j];
|
|
115
|
+
j++;
|
|
116
|
+
let stopCalled = false;
|
|
117
|
+
let skipCalled = false;
|
|
118
|
+
const stop = () => {
|
|
119
|
+
stopCalled = true;
|
|
120
|
+
};
|
|
121
|
+
const skip = () => {
|
|
122
|
+
skipCalled = true;
|
|
123
|
+
};
|
|
124
|
+
skippableHandlerResult(afterSideEffect, { skip, stop });
|
|
125
|
+
if (skipCalled) {
|
|
126
|
+
sideEffect.skippable = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (stopCalled) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
addSkippableHandler((sideEffect) => {
|
|
137
|
+
if (sideEffect.type === "return" && sideEffect.value === RETURN_PROMISE) {
|
|
138
|
+
return (nextSideEffect, { skip }) => {
|
|
139
|
+
if (
|
|
140
|
+
nextSideEffect.code === "resolve" ||
|
|
141
|
+
nextSideEffect.code === "reject"
|
|
142
|
+
) {
|
|
143
|
+
skip();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
for (const detector of detectors) {
|
|
151
|
+
const uninstall = detector.install(addSideEffect, {
|
|
152
|
+
addSkippableHandler,
|
|
153
|
+
addFinallyCallback,
|
|
154
|
+
});
|
|
155
|
+
finallyCallbackSet.add(() => {
|
|
156
|
+
uninstall();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (functionExecutingCount) {
|
|
160
|
+
// The reason for this warning:
|
|
161
|
+
// 1. fs side effect detectors is not yet fully compatible with that because
|
|
162
|
+
// callback.oncomplete redefinition might be wrong for open, mkdir etc
|
|
163
|
+
// (at least this is to be tested)
|
|
164
|
+
// 2. It's usually a sign code forgets to put await in front of
|
|
165
|
+
// collectFunctionSideEffects or snapshotFunctionSideEffects
|
|
166
|
+
// 3. collectFunctionSideEffects is meant to collect a function side effect
|
|
167
|
+
// during unit test. So in unit test the function being tested should be analyized
|
|
168
|
+
// and should not in turn analyze an other one
|
|
169
|
+
console.warn(
|
|
170
|
+
`captureSideEffects called while other function(s) side effects are collected`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const onCatch = (valueThrow) => {
|
|
175
|
+
addSideEffect({
|
|
176
|
+
code: "throw",
|
|
177
|
+
type: "throw",
|
|
178
|
+
value: valueThrow,
|
|
179
|
+
render: {
|
|
180
|
+
md: () => {
|
|
181
|
+
return {
|
|
182
|
+
label: "throw",
|
|
183
|
+
text: {
|
|
184
|
+
type: "js_value",
|
|
185
|
+
value: valueThrow,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const onReturn = (valueReturned) => {
|
|
193
|
+
if (valueReturned === RETURN_PROMISE) {
|
|
194
|
+
addSideEffect({
|
|
195
|
+
code: "return",
|
|
196
|
+
type: "return",
|
|
197
|
+
value: valueReturned,
|
|
198
|
+
render: {
|
|
199
|
+
md: () => {
|
|
200
|
+
return {
|
|
201
|
+
label: "return promise",
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
addSideEffect({
|
|
209
|
+
code: "return",
|
|
210
|
+
type: "return",
|
|
211
|
+
value: valueReturned,
|
|
212
|
+
render: {
|
|
213
|
+
md: () => {
|
|
214
|
+
return {
|
|
215
|
+
label: "return",
|
|
216
|
+
text: {
|
|
217
|
+
type: "js_value",
|
|
218
|
+
value: valueReturned,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const onResolve = (value) => {
|
|
226
|
+
addSideEffect({
|
|
227
|
+
code: "resolve",
|
|
228
|
+
type: "resolve",
|
|
229
|
+
value,
|
|
230
|
+
render: {
|
|
231
|
+
md: () => {
|
|
232
|
+
return {
|
|
233
|
+
label: "resolve",
|
|
234
|
+
text: {
|
|
235
|
+
type: "js_value",
|
|
236
|
+
value,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const onReject = (reason) => {
|
|
244
|
+
addSideEffect({
|
|
245
|
+
code: "reject",
|
|
246
|
+
type: "reject",
|
|
247
|
+
value: reason,
|
|
248
|
+
render: {
|
|
249
|
+
md: () => {
|
|
250
|
+
return {
|
|
251
|
+
label: "reject",
|
|
252
|
+
text: {
|
|
253
|
+
type: "js_value",
|
|
254
|
+
value: reason,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const onFinally = () => {
|
|
262
|
+
delete process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS;
|
|
263
|
+
functionExecutingCount--;
|
|
264
|
+
for (const finallyCallback of finallyCallbackSet) {
|
|
265
|
+
finallyCallback(sideEffects);
|
|
266
|
+
}
|
|
267
|
+
finallyCallbackSet.clear();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS = "1";
|
|
271
|
+
functionExecutingCount++;
|
|
272
|
+
let returnedPromise = false;
|
|
273
|
+
try {
|
|
274
|
+
const valueReturned = fn();
|
|
275
|
+
if (valueReturned && typeof valueReturned.then === "function") {
|
|
276
|
+
onReturn(RETURN_PROMISE);
|
|
277
|
+
returnedPromise = valueReturned.then(
|
|
278
|
+
(value) => {
|
|
279
|
+
onResolve(value);
|
|
280
|
+
onFinally();
|
|
281
|
+
return sideEffects;
|
|
282
|
+
},
|
|
283
|
+
(e) => {
|
|
284
|
+
onReject(e);
|
|
285
|
+
onFinally();
|
|
286
|
+
return sideEffects;
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
return returnedPromise;
|
|
290
|
+
}
|
|
291
|
+
onReturn(valueReturned);
|
|
292
|
+
return sideEffects;
|
|
293
|
+
} catch (e) {
|
|
294
|
+
onCatch(e);
|
|
295
|
+
return sideEffects;
|
|
296
|
+
} finally {
|
|
297
|
+
if (!returnedPromise) {
|
|
298
|
+
onFinally();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
return capture;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const RETURN_PROMISE = {};
|