@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.
Files changed (26) hide show
  1. package/package.json +7 -5
  2. package/src/filesystem_well_known_values.js +18 -26
  3. package/src/get_caller_location.js +22 -0
  4. package/src/main.js +4 -1
  5. package/src/replace_fluctuating_values.js +1 -1
  6. package/src/side_effects/capture_side_effects.js +6 -0
  7. package/src/side_effects/create_capture_side_effects.js +305 -0
  8. package/src/side_effects/filesystem/filesystem_side_effects.js +302 -0
  9. package/src/side_effects/filesystem/group_file_side_effects_per_directory.js +30 -0
  10. package/src/{function_side_effects → side_effects/filesystem}/spy_filesystem_calls.js +48 -25
  11. package/src/side_effects/log/group_log_side_effects.js +29 -0
  12. package/src/side_effects/log/log_side_effects.js +156 -0
  13. package/src/side_effects/render_logs_gif.js +18 -0
  14. package/src/side_effects/render_side_effects.js +435 -0
  15. package/src/side_effects/snapshot_side_effects.js +43 -0
  16. package/src/side_effects/snapshot_tests.js +115 -0
  17. package/src/side_effects/utils/group_side_effects.js +89 -0
  18. package/src/function_side_effects/function_side_effects_collector.js +0 -160
  19. package/src/function_side_effects/function_side_effects_renderer.js +0 -29
  20. package/src/function_side_effects/function_side_effects_snapshot.js +0 -302
  21. package/src/function_side_effects/group_file_side_effects_per_directory.js +0 -114
  22. package/src/function_side_effects/spy_console_calls.js +0 -89
  23. package/src/snapshot_scenarios.js +0 -32
  24. /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.js +0 -0
  25. /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.test.mjs +0 -0
  26. /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.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.12",
38
- "@jsenv/ast": "6.2.10",
37
+ "@jsenv/assert": "4.1.13",
38
+ "@jsenv/ast": "6.2.11",
39
39
  "@jsenv/exception": "1.0.1",
40
- "@jsenv/filesystem": "4.9.5",
41
- "@jsenv/urls": "2.4.1",
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
- let currentUrl = cwdUrl;
140
- while (currentUrl.href !== ancestorPackagesRootDirectoryUrl) {
141
- const packageFileUrl = new URL("package.json", currentUrl);
142
- const packageDirectoryUrl = currentUrl;
143
- currentUrl = new URL(getParentUrl(currentUrl));
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 { snapshotFunctionSideEffects } from "./function_side_effects/function_side_effects_snapshot.js";
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";
@@ -61,7 +61,7 @@ export const replaceFluctuatingValues = (
61
61
  value = replaceDurations(value);
62
62
  return value;
63
63
  };
64
- if (stringType === "html") {
64
+ if (stringType === "html" || stringType === "svg") {
65
65
  // do parse html
66
66
  const htmlAst =
67
67
  stringType === "svg"
@@ -0,0 +1,6 @@
1
+ import { createCaptureSideEffects } from "./create_capture_side_effects.js";
2
+
3
+ export const captureSideEffects = (fn, options) => {
4
+ const capture = createCaptureSideEffects(options);
5
+ return capture(fn);
6
+ };
@@ -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 = {};