@jsenv/snapshot 2.7.5 → 2.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/snapshot",
3
- "version": "2.7.5",
3
+ "version": "2.8.1",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@jsenv/assert": "4.1.14",
37
- "@jsenv/ast": "6.2.13",
37
+ "@jsenv/ast": "6.2.14",
38
38
  "@jsenv/exception": "1.0.1",
39
39
  "@jsenv/filesystem": "4.9.9",
40
40
  "@jsenv/terminal-recorder": "1.4.3",
@@ -58,6 +58,7 @@ export const createWellKnown = (name, replacement = name) => {
58
58
 
59
59
  export const createReplaceFilesystemWellKnownValues = ({
60
60
  rootDirectoryUrl,
61
+ localhostUrl,
61
62
  // for unit tests
62
63
  isWindows = process.platform === "win32",
63
64
  ancestorPackagesDisabled,
@@ -126,9 +127,32 @@ export const createReplaceFilesystemWellKnownValues = ({
126
127
  }
127
128
  };
128
129
  };
130
+ const addWellKnownUrl = (url, replacement) => {
131
+ const wellKnownUrl = {
132
+ url,
133
+ replace: (string) => {
134
+ return string.replaceAll(url, replacement);
135
+ },
136
+ };
137
+ wellKownUrlArray.push(wellKnownUrl);
138
+ return () => {
139
+ const urlIndex = wellKownUrlArray.indexOf(wellKnownUrl);
140
+ if (urlIndex > -1) {
141
+ wellKownUrlArray.splice(urlIndex, 1);
142
+ }
143
+ };
144
+ };
129
145
  if (rootDirectoryUrl) {
130
146
  addWellKnownFileUrl(rootDirectoryUrl, WELL_KNOWN_ROOT);
131
147
  }
148
+ if (localhostUrl) {
149
+ addWellKnownUrl(
150
+ localhostUrl,
151
+ localhostUrl.startsWith("https")
152
+ ? "https://localhost"
153
+ : "http://localhost",
154
+ );
155
+ }
132
156
  /*
133
157
  * When running code inside a node project ancestor packages
134
158
  * should make things super predictible because
@@ -203,23 +227,17 @@ export const createReplaceFilesystemWellKnownValues = ({
203
227
 
204
228
  const replaceFileUrls = (string, { willBeWrittenOnFilesystem }) => {
205
229
  for (const wellKownUrl of wellKownUrlArray) {
206
- const replaceResult = wellKownUrl.replace(string, {
230
+ string = wellKownUrl.replace(string, {
207
231
  willBeWrittenOnFilesystem,
208
232
  });
209
- if (replaceResult !== string) {
210
- return replaceResult;
211
- }
212
233
  }
213
234
  return string;
214
235
  };
215
236
  const replaceFilePaths = (string, { willBeWrittenOnFilesystem }) => {
216
237
  for (const wellKownPath of wellKnownPathArray) {
217
- const replaceResult = wellKownPath.replace(string, {
238
+ string = wellKownPath.replace(string, {
218
239
  willBeWrittenOnFilesystem,
219
240
  });
220
- if (replaceResult !== string) {
221
- return replaceResult;
222
- }
223
241
  }
224
242
  return string;
225
243
  };
@@ -12,119 +12,205 @@ import {
12
12
  visitHtmlNodes,
13
13
  } from "@jsenv/ast";
14
14
  import { urlToExtension } from "@jsenv/urls";
15
+ import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
15
16
  import stripAnsi from "strip-ansi";
16
17
  import { createReplaceFilesystemWellKnownValues } from "./filesystem_well_known_values.js";
17
18
 
18
19
  export const replaceFluctuatingValues = (
19
- string,
20
+ value,
20
21
  {
21
22
  stringType,
22
23
  rootDirectoryUrl,
24
+ localhostUrl,
23
25
  fileUrl,
24
26
  preserveAnsi,
25
27
  // for unit test
26
28
  replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
27
29
  rootDirectoryUrl,
30
+ localhostUrl,
28
31
  }),
29
32
  } = {},
30
33
  ) => {
31
- if (fileUrl && stringType === undefined) {
32
- const extension = urlToExtension(fileUrl);
33
- if (extension === ".html") {
34
- stringType = "html";
35
- } else if (extension === ".svg") {
36
- stringType = "svg";
34
+ if (fileUrl) {
35
+ const contentType = CONTENT_TYPE.fromUrlExtension(fileUrl);
36
+ if (Buffer.isBuffer(value) && CONTENT_TYPE.isTextual(contentType)) {
37
+ value = String(value);
38
+ }
39
+ if (stringType === undefined) {
40
+ const extension = urlToExtension(fileUrl);
41
+ if (extension === ".html") {
42
+ stringType = "html";
43
+ } else if (extension === ".svg") {
44
+ stringType = "svg";
45
+ } else if (extension === ".json" || CONTENT_TYPE.isJson(contentType)) {
46
+ stringType = "json";
47
+ }
37
48
  }
38
49
  }
39
- const replaceDurations = (value) => {
50
+ const replaceDurations = (string) => {
40
51
  // https://stackoverflow.com/a/59202307/24573072
41
- value = value.replace(
52
+ string = string.replace(
42
53
  /(?<!\d|\.)\d+(?:\.\d+)?(\s*)(seconds|second|s)\b/g,
43
54
  (match, space, unit) => {
44
55
  if (unit === "seconds") unit = "second";
45
56
  return `<X>${space}${unit}`;
46
57
  },
47
58
  );
48
- return value;
59
+ return string;
49
60
  };
50
- const replaceSizes = (value) => {
61
+ const replaceSizes = (string) => {
51
62
  // the size of files might slighly differ from an OS to an other
52
63
  // we round the floats to make them predictable
53
64
  // (happens for HTML files where one char is added on linux)
54
- value = value.replace(
55
- /(?<!\d|\.)(\d+(?:\.\d+)?)(\s*)(B|kB|MB)\b/g,
56
- (match, size, space, unit) => {
57
- return `${Math.round(parseFloat(size))}${space}${unit}`;
58
- },
59
- );
60
- return value;
65
+ // string = string.replace(
66
+ // /(?<!\d|\.)(\d+(?:\.\d+)?)(\s*)(B|kB|MB)\b/g,
67
+ // (match, size, space, unit) => {
68
+ // return `${Math.round(parseFloat(size))}${space}${unit}`;
69
+ // },
70
+ // );
71
+ return string;
61
72
  };
62
- const replaceThings = (value) => {
73
+ const replaceThings = (string) => {
63
74
  if (stringType === "filesystem") {
64
- return replaceFilesystemWellKnownValues(value);
75
+ return replaceFilesystemWellKnownValues(string);
65
76
  }
66
77
  if (!preserveAnsi) {
67
- value = stripAnsi(value);
78
+ string = stripAnsi(string);
68
79
  }
69
- value = replaceFilesystemWellKnownValues(value, {
80
+ string = replaceFilesystemWellKnownValues(string, {
70
81
  willBeWrittenOnFilesystem: false,
71
82
  });
72
- value = replaceHttpUrls(value);
73
- value = replaceDurations(value);
74
- value = replaceSizes(value);
75
- return value;
83
+ string = replaceHttpUrls(string);
84
+ string = replaceDurations(string);
85
+ string = replaceSizes(string);
86
+ return string;
76
87
  };
77
- if (stringType === "html" || stringType === "svg") {
78
- // do parse html
79
- const htmlAst =
80
- stringType === "svg"
81
- ? parseSvgString(string)
82
- : parseHtml({
83
- html: string,
84
- storeOriginalPositions: false,
85
- });
86
- // for each attribute value
87
- // and each text node content
88
- visitHtmlNodes(htmlAst, {
89
- "*": (node) => {
90
- const htmlNodeText = getHtmlNodeText(node);
91
- if (htmlNodeText) {
92
- setHtmlNodeText(node, replaceThings(htmlNodeText));
93
- }
94
- const attributes = getHtmlNodeAttributes(node);
95
- if (attributes) {
96
- for (const name of Object.keys(attributes)) {
97
- attributes[name] = replaceThings(attributes[name]);
88
+ if (value === null) {
89
+ return null;
90
+ }
91
+ if (typeof value === "string") {
92
+ if (stringType === "json") {
93
+ const jsValue = JSON.parse(value);
94
+ const replaced = replaceInObject(jsValue, { replace: replaceThings });
95
+ return JSON.stringify(replaced, null, " ");
96
+ }
97
+ if (stringType === "html" || stringType === "svg") {
98
+ // do parse html
99
+ const htmlAst =
100
+ stringType === "svg"
101
+ ? parseSvgString(value)
102
+ : parseHtml({
103
+ html: value,
104
+ storeOriginalPositions: false,
105
+ });
106
+ // for each attribute value
107
+ // and each text node content
108
+ visitHtmlNodes(htmlAst, {
109
+ "*": (node) => {
110
+ const htmlNodeText = getHtmlNodeText(node);
111
+ if (htmlNodeText) {
112
+ setHtmlNodeText(node, replaceThings(htmlNodeText));
98
113
  }
99
- setHtmlNodeAttributes(node, attributes);
100
- }
101
- },
102
- });
103
- return stringifyHtmlAst(htmlAst);
114
+ const attributes = getHtmlNodeAttributes(node);
115
+ if (attributes) {
116
+ for (const name of Object.keys(attributes)) {
117
+ attributes[name] = replaceThings(attributes[name]);
118
+ }
119
+ setHtmlNodeAttributes(node, attributes);
120
+ }
121
+ },
122
+ });
123
+ return stringifyHtmlAst(htmlAst);
124
+ }
125
+ return replaceThings(value);
104
126
  }
105
- return replaceThings(string);
127
+ if (typeof value === "object") {
128
+ if (Buffer.isBuffer(value)) {
129
+ return value;
130
+ }
131
+ const jsValueReplaced = replaceInObject(value, { replace: replaceThings });
132
+ return JSON.stringify(jsValueReplaced, null, " ");
133
+ }
134
+ return value;
106
135
  };
107
136
 
108
- const replaceHttpUrls = (source) => {
109
- return source.replace(/(?:https?|ftp):\/\/\S+[\w/]/g, (match) => {
110
- const lastChar = match[match.length - 1];
111
- // hotfix because our url regex sucks a bit
112
- const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
113
- if (endsWithSeparationChar) {
114
- match = match.slice(0, -1);
137
+ const replaceInObject = (object, { replace }) => {
138
+ const deepCopy = (
139
+ value,
140
+ { shouldReplaceStrings, shouldReplaceNumbers } = {},
141
+ ) => {
142
+ if (value === null) {
143
+ return null;
115
144
  }
116
- try {
117
- const urlObject = new URL(match);
118
- if (urlObject.hostname === "www.w3.org") {
119
- return match;
145
+ if (Array.isArray(value)) {
146
+ const copy = [];
147
+ let i = 0;
148
+ while (i < value.length) {
149
+ copy[i] = deepCopy(value[i], {
150
+ shouldReplaceStrings,
151
+ shouldReplaceNumbers,
152
+ });
153
+ i++;
120
154
  }
121
- if (urlObject.port) {
122
- urlObject.port = 9999;
155
+ return copy;
156
+ }
157
+ if (typeof value === "object") {
158
+ const copy = {};
159
+ const keysToVisit = Object.keys(value);
160
+ for (const keyToVisit of keysToVisit) {
161
+ const nestedValue = value[keyToVisit];
162
+ copy[keyToVisit] = deepCopy(nestedValue, {
163
+ shouldReplaceStrings: shouldReplaceStrings || keyToVisit === "os",
164
+ shouldReplaceNumbers:
165
+ shouldReplaceNumbers ||
166
+ keyToVisit === "timings" ||
167
+ keyToVisit === "performance" ||
168
+ keyToVisit === "memoryUsage" ||
169
+ keyToVisit === "cpuUsage" ||
170
+ keyToVisit === "os",
171
+ });
123
172
  }
124
- const url = urlObject.href;
125
- return url;
126
- } catch (e) {
127
- return match;
173
+ return copy;
128
174
  }
129
- });
175
+ if (typeof value === "string") {
176
+ if (shouldReplaceStrings) {
177
+ return "<X>";
178
+ }
179
+ return replace(value);
180
+ }
181
+ if (typeof value === "number") {
182
+ if (shouldReplaceNumbers) {
183
+ return "<X>";
184
+ }
185
+ return value;
186
+ }
187
+ return value;
188
+ };
189
+ const copy = deepCopy(object);
190
+ return copy;
191
+ };
192
+
193
+ const replaceHttpUrls = (source) => {
194
+ return source;
195
+ // return source.replace(/(?:https?|ftp):\/\/\S+[\w/]/g, (match) => {
196
+ // const lastChar = match[match.length - 1];
197
+ // // hotfix because our url regex sucks a bit
198
+ // const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
199
+ // if (endsWithSeparationChar) {
200
+ // match = match.slice(0, -1);
201
+ // }
202
+ // try {
203
+ // const urlObject = new URL(match);
204
+ // if (urlObject.hostname === "www.w3.org") {
205
+ // return match;
206
+ // }
207
+ // if (urlObject.port) {
208
+ // urlObject.port = 9999;
209
+ // }
210
+ // const url = urlObject.href;
211
+ // return url;
212
+ // } catch (e) {
213
+ // return match;
214
+ // }
215
+ // });
130
216
  };
@@ -160,6 +160,16 @@ const renderOneSideEffect = (
160
160
  lastSideEffectNumber,
161
161
  });
162
162
  if (text) {
163
+ if (
164
+ sideEffect.number === 1 &&
165
+ lastSideEffectNumber === 1 &&
166
+ (sideEffect.code === "return" ||
167
+ sideEffect.code === "throw" ||
168
+ sideEffect.code === "resolve" ||
169
+ sideEffect.code === "reject")
170
+ ) {
171
+ label = null;
172
+ }
163
173
  text = renderText(text, {
164
174
  sideEffect,
165
175
  sideEffectFileUrl,
@@ -167,11 +177,6 @@ const renderOneSideEffect = (
167
177
  replace,
168
178
  rootDirectoryUrl,
169
179
  errorStackHidden,
170
- onRenderError: () => {
171
- if (sideEffect.number === 1 && lastSideEffectNumber === 1) {
172
- label = null;
173
- }
174
- },
175
180
  });
176
181
  }
177
182
  if (sideEffect.code === "source_code") {
@@ -208,7 +213,6 @@ const renderText = (
208
213
  replace,
209
214
  rootDirectoryUrl,
210
215
  errorStackHidden,
211
- onRenderError = () => {},
212
216
  },
213
217
  ) => {
214
218
  if (text && typeof text === "object") {
@@ -233,21 +237,20 @@ const renderText = (
233
237
  return sourceMd;
234
238
  }
235
239
  if (text.type === "js_value") {
236
- const value = text.value;
237
- if (value === undefined) {
240
+ const jsValue = text.value;
241
+ if (jsValue === undefined) {
238
242
  return renderMarkdownBlock("undefined", "js");
239
243
  }
240
244
  if (
241
- value instanceof Error ||
242
- (value &&
243
- value.constructor &&
244
- value.constructor.name.includes("Error") &&
245
- value.stack &&
246
- typeof value.stack === "string")
245
+ jsValue instanceof Error ||
246
+ (jsValue &&
247
+ jsValue.constructor &&
248
+ jsValue.constructor.name.includes("Error") &&
249
+ jsValue.stack &&
250
+ typeof jsValue.stack === "string")
247
251
  ) {
248
- onRenderError();
249
252
  // return renderMarkdownBlock(text.value.stack);
250
- const exception = createException(text.value, { rootDirectoryUrl });
253
+ const exception = createException(jsValue, { rootDirectoryUrl });
251
254
  const exceptionText = errorStackHidden
252
255
  ? `${exception.name}: ${exception.message}`
253
256
  : exception.stack || exception.message || exception;
@@ -259,14 +262,7 @@ const renderText = (
259
262
  replace,
260
263
  });
261
264
  }
262
-
263
- return renderMarkdownBlock(
264
- replace(
265
- typeof value === "string" ? value : JSON.stringify(value, null, " "),
266
- { stringType: "json" },
267
- ),
268
- "js",
269
- );
265
+ return renderMarkdownBlock(replace(jsValue), "js");
270
266
  }
271
267
  if (text.type === "console") {
272
268
  return renderConsole(text.value, {
@@ -279,7 +275,6 @@ const renderText = (
279
275
  if (text.type === "file_content") {
280
276
  return renderFileContent(text, {
281
277
  sideEffect,
282
- sideEffectFileUrl,
283
278
  replace,
284
279
  });
285
280
  }
@@ -341,16 +336,18 @@ const renderPotentialAnsi = (
341
336
 
342
337
  export const renderFileContent = (text, { sideEffect, replace }) => {
343
338
  const { url, buffer, outDirectoryReason } = sideEffect.value;
339
+ const { value } = text;
340
+ let content = value;
344
341
  if (outDirectoryReason) {
345
- const { value, outRelativeUrl, urlInsideOutDirectory } = text;
346
- writeFileSync(urlInsideOutDirectory, buffer);
342
+ const { outRelativeUrl, urlInsideOutDirectory } = text;
343
+ writeFileSync(urlInsideOutDirectory, replace(buffer, { fileUrl: url }));
347
344
  let md = "";
348
345
  if (
349
346
  outDirectoryReason === "lot_of_chars" ||
350
347
  outDirectoryReason === "lot_of_lines"
351
348
  ) {
352
349
  md += "\n";
353
- md += renderMarkdownBlock(escapeMarkdownBlockContent(replace(value)));
350
+ md += renderMarkdownBlock(escapeMarkdownBlockContent(replace(content)));
354
351
  const fileLink = renderLinkMarkdown(
355
352
  {
356
353
  text: outRelativeUrl,
@@ -371,13 +368,14 @@ export const renderFileContent = (text, { sideEffect, replace }) => {
371
368
  );
372
369
  return md;
373
370
  }
374
- const { value } = text;
375
- let content = value;
376
- const extension = urlToExtension(url).slice(1);
377
- if (extension === "md") {
371
+ const extension = urlToExtension(url);
372
+ if (extension === ".md") {
378
373
  content = escapeMarkdownBlockContent(content);
379
374
  }
380
- return renderMarkdownBlock(replace(content, { fileUrl: url }), extension);
375
+ return renderMarkdownBlock(
376
+ replace(content, { fileUrl: url }),
377
+ extension.slice(1),
378
+ );
381
379
  };
382
380
 
383
381
  const escapeMarkdownBlockContent = (content) => {