@jsenv/snapshot 2.6.5 → 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/{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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { createException } from "@jsenv/exception";
|
|
2
|
+
import { writeFileSync } from "@jsenv/filesystem";
|
|
3
|
+
import { renderTerminalSvg } from "@jsenv/terminal-recorder";
|
|
4
|
+
import { urlToBasename, urlToExtension, urlToRelativeUrl } from "@jsenv/urls";
|
|
5
|
+
import ansiRegex from "ansi-regex";
|
|
6
|
+
import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
|
|
7
|
+
|
|
8
|
+
export const createBigSizeEffect =
|
|
9
|
+
({ details, dedicatedFile }) =>
|
|
10
|
+
(sideEffect, text) => {
|
|
11
|
+
if (text.length > details.length) {
|
|
12
|
+
return {
|
|
13
|
+
type: "details",
|
|
14
|
+
open: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (text.length > dedicatedFile.length) {
|
|
18
|
+
return {
|
|
19
|
+
type: "dedicated_file",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const lineCount = text.split("\n").length;
|
|
23
|
+
if (lineCount > details.lines) {
|
|
24
|
+
return {
|
|
25
|
+
type: "details",
|
|
26
|
+
open: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (lineCount > dedicatedFile.lines) {
|
|
30
|
+
return {
|
|
31
|
+
type: "dedicated_file",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const renderSideEffects = (
|
|
38
|
+
sideEffects,
|
|
39
|
+
{
|
|
40
|
+
sideEffectFileUrl,
|
|
41
|
+
outDirectoryUrl,
|
|
42
|
+
generatedBy = true,
|
|
43
|
+
titleLevel = 1,
|
|
44
|
+
getBigSizeEffect = createBigSizeEffect({
|
|
45
|
+
details: { line: 15, length: 2000 },
|
|
46
|
+
// dedicated_file not implemented yet
|
|
47
|
+
// the idea is that some values like the return value can be big
|
|
48
|
+
// and in that case we might want to move it to an other file
|
|
49
|
+
dedicatedFile: { line: 50, length: 5000 },
|
|
50
|
+
}),
|
|
51
|
+
errorStackHidden,
|
|
52
|
+
} = {},
|
|
53
|
+
) => {
|
|
54
|
+
const { rootDirectoryUrl, replaceFilesystemWellKnownValues } =
|
|
55
|
+
sideEffects.options;
|
|
56
|
+
|
|
57
|
+
const replace = (value, options) => {
|
|
58
|
+
return replaceFluctuatingValues(value, {
|
|
59
|
+
replaceFilesystemWellKnownValues,
|
|
60
|
+
rootDirectoryUrl,
|
|
61
|
+
...options,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let markdown = "";
|
|
66
|
+
let sideEffectNumber = 0;
|
|
67
|
+
for (const sideEffect of sideEffects) {
|
|
68
|
+
if (sideEffect.skippable) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (sideEffect.code === "source_code") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
sideEffectNumber++;
|
|
75
|
+
sideEffect.number = sideEffectNumber;
|
|
76
|
+
}
|
|
77
|
+
const lastSideEffectNumber = sideEffectNumber;
|
|
78
|
+
|
|
79
|
+
for (const sideEffect of sideEffects) {
|
|
80
|
+
if (sideEffect.skippable) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (markdown) {
|
|
84
|
+
markdown += "\n\n";
|
|
85
|
+
}
|
|
86
|
+
markdown += renderOneSideEffect(sideEffect, {
|
|
87
|
+
sideEffectFileUrl,
|
|
88
|
+
outDirectoryUrl,
|
|
89
|
+
rootDirectoryUrl,
|
|
90
|
+
titleLevel,
|
|
91
|
+
getBigSizeEffect,
|
|
92
|
+
replace,
|
|
93
|
+
errorStackHidden,
|
|
94
|
+
lastSideEffectNumber,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (generatedBy) {
|
|
98
|
+
let generatedByLink = renderSmallLink(
|
|
99
|
+
{
|
|
100
|
+
text: "@jsenv/snapshot",
|
|
101
|
+
href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
prefix: "Generated by ",
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
markdown += "\n\n";
|
|
108
|
+
markdown += generatedByLink;
|
|
109
|
+
}
|
|
110
|
+
return markdown;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const renderSmallLink = (
|
|
114
|
+
link,
|
|
115
|
+
{ prefix = "", suffix = "", indent } = {},
|
|
116
|
+
) => {
|
|
117
|
+
return renderSubMarkdown(
|
|
118
|
+
`${prefix}<a href="${link.href}">${link.text}</a>${suffix}`,
|
|
119
|
+
{
|
|
120
|
+
indent,
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const renderSubMarkdown = (content, { indent = 0 }) => {
|
|
126
|
+
return `${" ".repeat(indent)}<sub>
|
|
127
|
+
${" ".repeat(indent + 1)}${content}
|
|
128
|
+
${" ".repeat(indent)}</sub>`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const renderOneSideEffect = (
|
|
132
|
+
sideEffect,
|
|
133
|
+
{
|
|
134
|
+
sideEffectFileUrl,
|
|
135
|
+
outDirectoryUrl,
|
|
136
|
+
rootDirectoryUrl,
|
|
137
|
+
titleLevel,
|
|
138
|
+
getBigSizeEffect,
|
|
139
|
+
replace,
|
|
140
|
+
errorStackHidden,
|
|
141
|
+
lastSideEffectNumber,
|
|
142
|
+
},
|
|
143
|
+
) => {
|
|
144
|
+
const { render } = sideEffect;
|
|
145
|
+
if (typeof render !== "object") {
|
|
146
|
+
throw new TypeError(
|
|
147
|
+
`sideEffect.render should be an object, got ${render} on side effect with type "${sideEffect.type}"`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const { md } = sideEffect.render;
|
|
151
|
+
let { label, text } = md({
|
|
152
|
+
sideEffectFileUrl,
|
|
153
|
+
outDirectoryUrl,
|
|
154
|
+
replace,
|
|
155
|
+
rootDirectoryUrl,
|
|
156
|
+
lastSideEffectNumber,
|
|
157
|
+
});
|
|
158
|
+
if (text) {
|
|
159
|
+
text = renderText(text, {
|
|
160
|
+
sideEffect,
|
|
161
|
+
sideEffectFileUrl,
|
|
162
|
+
outDirectoryUrl,
|
|
163
|
+
replace,
|
|
164
|
+
rootDirectoryUrl,
|
|
165
|
+
errorStackHidden,
|
|
166
|
+
onRenderError: () => {
|
|
167
|
+
if (sideEffect.number === 1 && lastSideEffectNumber === 1) {
|
|
168
|
+
label = null;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (sideEffect.code === "source_code") {
|
|
174
|
+
return text;
|
|
175
|
+
}
|
|
176
|
+
if (!label) {
|
|
177
|
+
return text;
|
|
178
|
+
}
|
|
179
|
+
const stepTitle = `${"#".repeat(titleLevel)} ${sideEffect.number}/${lastSideEffectNumber} ${replace(label)}`;
|
|
180
|
+
if (!text) {
|
|
181
|
+
return stepTitle;
|
|
182
|
+
}
|
|
183
|
+
const bigSizeEffect = getBigSizeEffect(sideEffect, text);
|
|
184
|
+
if (!bigSizeEffect) {
|
|
185
|
+
return `${stepTitle}
|
|
186
|
+
|
|
187
|
+
${text}`;
|
|
188
|
+
}
|
|
189
|
+
// for now we'll use details
|
|
190
|
+
const { open } = bigSizeEffect;
|
|
191
|
+
return `${stepTitle}
|
|
192
|
+
${renderMarkdownDetails(text, {
|
|
193
|
+
open,
|
|
194
|
+
summary: "details",
|
|
195
|
+
})}`;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const renderText = (
|
|
199
|
+
text,
|
|
200
|
+
{
|
|
201
|
+
sideEffect,
|
|
202
|
+
sideEffectFileUrl,
|
|
203
|
+
outDirectoryUrl,
|
|
204
|
+
replace,
|
|
205
|
+
rootDirectoryUrl,
|
|
206
|
+
errorStackHidden,
|
|
207
|
+
onRenderError = () => {},
|
|
208
|
+
},
|
|
209
|
+
) => {
|
|
210
|
+
if (text && typeof text === "object") {
|
|
211
|
+
if (text.type === "source_code") {
|
|
212
|
+
const { sourceCode, callSite } = text.value;
|
|
213
|
+
let sourceMd = renderMarkdownBlock(sourceCode, "js");
|
|
214
|
+
if (!callSite) {
|
|
215
|
+
return sourceMd;
|
|
216
|
+
}
|
|
217
|
+
const callSiteRelativeUrl = urlToRelativeUrl(
|
|
218
|
+
callSite.url,
|
|
219
|
+
sideEffectFileUrl,
|
|
220
|
+
{ preferRelativeNotation: true },
|
|
221
|
+
);
|
|
222
|
+
const sourceCodeLinkText = `${callSiteRelativeUrl}:${callSite.line}:${callSite.column}`;
|
|
223
|
+
const sourceCodeLinkHref = `${callSiteRelativeUrl}#L${callSite.line}`;
|
|
224
|
+
sourceMd += "\n";
|
|
225
|
+
sourceMd += renderSmallLink({
|
|
226
|
+
text: sourceCodeLinkText,
|
|
227
|
+
href: sourceCodeLinkHref,
|
|
228
|
+
});
|
|
229
|
+
return sourceMd;
|
|
230
|
+
}
|
|
231
|
+
if (text.type === "js_value") {
|
|
232
|
+
const value = text.value;
|
|
233
|
+
if (value === undefined) {
|
|
234
|
+
return renderMarkdownBlock("undefined", "js");
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
value instanceof Error ||
|
|
238
|
+
(value &&
|
|
239
|
+
value.constructor &&
|
|
240
|
+
value.constructor.name.includes("Error") &&
|
|
241
|
+
value.stack &&
|
|
242
|
+
typeof value.stack === "string")
|
|
243
|
+
) {
|
|
244
|
+
onRenderError();
|
|
245
|
+
const exception = createException(text.value, { rootDirectoryUrl });
|
|
246
|
+
const exceptionText = errorStackHidden
|
|
247
|
+
? `${exception.name}: ${exception.message}`
|
|
248
|
+
: exception.stack || exception.message || exception;
|
|
249
|
+
const potentialAnsi = renderPotentialAnsi(exceptionText, {
|
|
250
|
+
sideEffect,
|
|
251
|
+
sideEffectFileUrl,
|
|
252
|
+
outDirectoryUrl,
|
|
253
|
+
replace,
|
|
254
|
+
});
|
|
255
|
+
if (potentialAnsi) {
|
|
256
|
+
return potentialAnsi;
|
|
257
|
+
}
|
|
258
|
+
return renderMarkdownBlock(
|
|
259
|
+
replace(exceptionText, { stringType: "error" }),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return renderMarkdownBlock(
|
|
263
|
+
replace(JSON.stringify(value, null, " "), { stringType: "json" }),
|
|
264
|
+
"js",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (text.type === "console") {
|
|
268
|
+
return renderConsole(text.value, {
|
|
269
|
+
sideEffect,
|
|
270
|
+
sideEffectFileUrl,
|
|
271
|
+
outDirectoryUrl,
|
|
272
|
+
replace,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (text.type === "file_content") {
|
|
276
|
+
return renderFileContent(text, {
|
|
277
|
+
sideEffect,
|
|
278
|
+
sideEffectFileUrl,
|
|
279
|
+
replace,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (text.type === "link") {
|
|
283
|
+
return renderLinkMarkdown(text.value, { replace });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return replace(text);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export const renderConsole = (
|
|
290
|
+
string,
|
|
291
|
+
{ sideEffect, sideEffectFileUrl, outDirectoryUrl, replace },
|
|
292
|
+
) => {
|
|
293
|
+
const potentialAnsi = renderPotentialAnsi(string, {
|
|
294
|
+
sideEffect,
|
|
295
|
+
sideEffectFileUrl,
|
|
296
|
+
outDirectoryUrl,
|
|
297
|
+
replace,
|
|
298
|
+
});
|
|
299
|
+
if (potentialAnsi) {
|
|
300
|
+
return potentialAnsi;
|
|
301
|
+
}
|
|
302
|
+
return renderMarkdownBlock(
|
|
303
|
+
replace(string, { stringType: "console" }),
|
|
304
|
+
"console",
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const renderPotentialAnsi = (
|
|
309
|
+
string,
|
|
310
|
+
{ sideEffect, sideEffectFileUrl, outDirectoryUrl, replace },
|
|
311
|
+
) => {
|
|
312
|
+
if (!ansiRegex().test(string)) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
let svgFilename = urlToBasename(outDirectoryUrl);
|
|
316
|
+
svgFilename += `_${sideEffect.code}`;
|
|
317
|
+
if (sideEffect.counter) {
|
|
318
|
+
svgFilename += `_${sideEffect.counter}`;
|
|
319
|
+
}
|
|
320
|
+
svgFilename += ".svg";
|
|
321
|
+
const svgFileUrl = new URL(`./${svgFilename}`, outDirectoryUrl);
|
|
322
|
+
let svgFileContent = renderTerminalSvg(string, {
|
|
323
|
+
head: false,
|
|
324
|
+
paddingTop: 10,
|
|
325
|
+
paddingBottom: 10,
|
|
326
|
+
});
|
|
327
|
+
svgFileContent = replace(svgFileContent, { fileUrl: svgFileUrl });
|
|
328
|
+
writeFileSync(svgFileUrl, svgFileContent);
|
|
329
|
+
const svgFileRelativeUrl = urlToRelativeUrl(svgFileUrl, sideEffectFileUrl);
|
|
330
|
+
return ``;
|
|
331
|
+
// we will write a svg file
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const renderFileContent = (text, { sideEffect, replace }) => {
|
|
335
|
+
const { url, buffer, outDirectoryReason } = sideEffect.value;
|
|
336
|
+
if (outDirectoryReason) {
|
|
337
|
+
const { value, outRelativeUrl, urlInsideOutDirectory } = text;
|
|
338
|
+
writeFileSync(urlInsideOutDirectory, buffer);
|
|
339
|
+
let md = "";
|
|
340
|
+
if (
|
|
341
|
+
outDirectoryReason === "lot_of_chars" ||
|
|
342
|
+
outDirectoryReason === "lot_of_lines"
|
|
343
|
+
) {
|
|
344
|
+
md += "\n";
|
|
345
|
+
md += renderMarkdownBlock(escapeMarkdownBlockContent(replace(value)));
|
|
346
|
+
const fileLink = renderLinkMarkdown(
|
|
347
|
+
{
|
|
348
|
+
text: outRelativeUrl,
|
|
349
|
+
href: outRelativeUrl,
|
|
350
|
+
},
|
|
351
|
+
{ replace },
|
|
352
|
+
);
|
|
353
|
+
md += `\nsee ${fileLink} for more`;
|
|
354
|
+
return md;
|
|
355
|
+
}
|
|
356
|
+
md += `see `;
|
|
357
|
+
md += renderLinkMarkdown(
|
|
358
|
+
{
|
|
359
|
+
text: outRelativeUrl,
|
|
360
|
+
href: outRelativeUrl,
|
|
361
|
+
},
|
|
362
|
+
{ replace },
|
|
363
|
+
);
|
|
364
|
+
return md;
|
|
365
|
+
}
|
|
366
|
+
const { value } = text;
|
|
367
|
+
let content = value;
|
|
368
|
+
const extension = urlToExtension(url).slice(1);
|
|
369
|
+
if (extension === "md") {
|
|
370
|
+
content = escapeMarkdownBlockContent(content);
|
|
371
|
+
}
|
|
372
|
+
return renderMarkdownBlock(replace(content, { fileUrl: url }), extension);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const escapeMarkdownBlockContent = (content) => {
|
|
376
|
+
let escaped = "";
|
|
377
|
+
for (const char of content.split("")) {
|
|
378
|
+
if (["`"].includes(char)) {
|
|
379
|
+
escaped += `\\${char}`;
|
|
380
|
+
} else {
|
|
381
|
+
escaped += char;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return escaped;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// const escapeMarkdown = (content) => {
|
|
388
|
+
// let escaped = "";
|
|
389
|
+
// for (const char of content.split("")) {
|
|
390
|
+
// if (
|
|
391
|
+
// [
|
|
392
|
+
// "`",
|
|
393
|
+
// "*",
|
|
394
|
+
// "_",
|
|
395
|
+
// "{",
|
|
396
|
+
// "}",
|
|
397
|
+
// "[",
|
|
398
|
+
// "]",
|
|
399
|
+
// "(",
|
|
400
|
+
// ")",
|
|
401
|
+
// "#",
|
|
402
|
+
// "+",
|
|
403
|
+
// "-",
|
|
404
|
+
// ".",
|
|
405
|
+
// "!",
|
|
406
|
+
// ].includes(char)
|
|
407
|
+
// ) {
|
|
408
|
+
// escaped += `\\${char}`;
|
|
409
|
+
// } else {
|
|
410
|
+
// escaped += char;
|
|
411
|
+
// }
|
|
412
|
+
// }
|
|
413
|
+
// return escaped;
|
|
414
|
+
// };
|
|
415
|
+
|
|
416
|
+
export const renderLinkMarkdown = ({ href, text }, { replace }) => {
|
|
417
|
+
return `[${replace(text)}](${replace(href)})`;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const renderMarkdownDetails = (text, { open, summary, indent = 0 }) => {
|
|
421
|
+
return `${" ".repeat(indent)}<details${open ? " open" : ""}>
|
|
422
|
+
${" ".repeat(indent + 1)}<summary>${summary}</summary>
|
|
423
|
+
|
|
424
|
+
${text}
|
|
425
|
+
|
|
426
|
+
${" ".repeat(indent)}</details>`;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
export const renderMarkdownBlock = (value, blockName = "") => {
|
|
430
|
+
const start = "```";
|
|
431
|
+
const end = "```";
|
|
432
|
+
return `${start}${blockName}
|
|
433
|
+
${value}
|
|
434
|
+
${end}`;
|
|
435
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { urlToBasename } from "@jsenv/urls";
|
|
2
|
+
import {
|
|
3
|
+
takeDirectorySnapshot,
|
|
4
|
+
takeFileSnapshot,
|
|
5
|
+
} from "../filesystem_snapshot.js";
|
|
6
|
+
import { createCaptureSideEffects } from "./create_capture_side_effects.js";
|
|
7
|
+
import { renderSideEffects } from "./render_side_effects.js";
|
|
8
|
+
|
|
9
|
+
export const snapshotSideEffects = (
|
|
10
|
+
fn,
|
|
11
|
+
sideEffectFileUrl,
|
|
12
|
+
{ outDirectoryUrl, errorStackHidden, ...captureOptions } = {},
|
|
13
|
+
) => {
|
|
14
|
+
const captureSideEffects = createCaptureSideEffects(captureOptions);
|
|
15
|
+
if (outDirectoryUrl === undefined) {
|
|
16
|
+
outDirectoryUrl = new URL(
|
|
17
|
+
`./${urlToBasename(sideEffectFileUrl)}/`,
|
|
18
|
+
sideEffectFileUrl,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
|
|
22
|
+
const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl);
|
|
23
|
+
const onSideEffects = (sideEffects) => {
|
|
24
|
+
const sideEffectFileContent = renderSideEffects(sideEffects, {
|
|
25
|
+
sideEffectFileUrl,
|
|
26
|
+
outDirectoryUrl,
|
|
27
|
+
errorStackHidden,
|
|
28
|
+
});
|
|
29
|
+
sideEffectFileSnapshot.update(sideEffectFileContent, {
|
|
30
|
+
mockFluctuatingValues: false,
|
|
31
|
+
});
|
|
32
|
+
outDirectorySnapshot.compare();
|
|
33
|
+
};
|
|
34
|
+
const returnValue = captureSideEffects(fn);
|
|
35
|
+
if (returnValue && typeof returnValue.then === "function") {
|
|
36
|
+
return returnValue.then((sideEffects) => {
|
|
37
|
+
onSideEffects(sideEffects);
|
|
38
|
+
return sideEffects;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
onSideEffects(returnValue);
|
|
42
|
+
return returnValue;
|
|
43
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { urlToFilename, urlToRelativeUrl } from "@jsenv/urls";
|
|
2
|
+
import {
|
|
3
|
+
takeDirectorySnapshot,
|
|
4
|
+
takeFileSnapshot,
|
|
5
|
+
} from "../filesystem_snapshot.js";
|
|
6
|
+
import { getCallerLocation } from "../get_caller_location.js";
|
|
7
|
+
import { createCaptureSideEffects } from "./create_capture_side_effects.js";
|
|
8
|
+
import { renderSideEffects, renderSmallLink } from "./render_side_effects.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a markdown file describing all test side effects. When executed in CI throw if there is a diff.
|
|
12
|
+
* @param {Function} fnRegisteringTest
|
|
13
|
+
* @param {URL} snapshotFileUrl
|
|
14
|
+
* @param {Object} snapshotTestsOptions
|
|
15
|
+
* @param {string|url} snapshotTestsOptions.sourceDirectoryUrl
|
|
16
|
+
* @return {Array.<Object>} sideEffects
|
|
17
|
+
*/
|
|
18
|
+
export const snapshotTests = async (
|
|
19
|
+
fnRegisteringTest,
|
|
20
|
+
snapshotFileUrl,
|
|
21
|
+
{
|
|
22
|
+
rootDirectoryUrl,
|
|
23
|
+
generatedBy = true,
|
|
24
|
+
linkToSource = true,
|
|
25
|
+
sourceFileUrl,
|
|
26
|
+
linkToEachSource,
|
|
27
|
+
errorStackHidden,
|
|
28
|
+
logEffects,
|
|
29
|
+
filesystemEffects,
|
|
30
|
+
throwWhenDiff = process.env.CI,
|
|
31
|
+
} = {},
|
|
32
|
+
) => {
|
|
33
|
+
const testMap = new Map();
|
|
34
|
+
const onlyTestMap = new Map();
|
|
35
|
+
const test = (scenario, fn, options) => {
|
|
36
|
+
testMap.set(scenario, { fn, options, callSite: getCallerLocation(2) });
|
|
37
|
+
};
|
|
38
|
+
test.ONLY = (scenario, fn, options) => {
|
|
39
|
+
onlyTestMap.set(scenario, { fn, options, callSite: getCallerLocation(2) });
|
|
40
|
+
};
|
|
41
|
+
fnRegisteringTest({ test });
|
|
42
|
+
|
|
43
|
+
const activeTestMap = onlyTestMap.size ? onlyTestMap : testMap;
|
|
44
|
+
const captureSideEffects = createCaptureSideEffects({
|
|
45
|
+
rootDirectoryUrl,
|
|
46
|
+
logEffects,
|
|
47
|
+
filesystemEffects,
|
|
48
|
+
});
|
|
49
|
+
let markdown = "";
|
|
50
|
+
markdown += `# ${urlToFilename(snapshotFileUrl)}`;
|
|
51
|
+
if (generatedBy) {
|
|
52
|
+
let generatedByLink = renderSmallLink(
|
|
53
|
+
{
|
|
54
|
+
text: "@jsenv/snapshot",
|
|
55
|
+
href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
prefix: "Generated by ",
|
|
59
|
+
suffix:
|
|
60
|
+
linkToSource && sourceFileUrl
|
|
61
|
+
? generateExecutingLink(sourceFileUrl, snapshotFileUrl)
|
|
62
|
+
: "",
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
markdown += "\n\n";
|
|
66
|
+
markdown += generatedByLink;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const [scenario, { fn, callSite }] of activeTestMap) {
|
|
70
|
+
markdown += "\n\n";
|
|
71
|
+
markdown += `## ${scenario}`;
|
|
72
|
+
markdown += "\n\n";
|
|
73
|
+
const sideEffects = await captureSideEffects(fn, {
|
|
74
|
+
callSite: linkToEachSource ? callSite : undefined,
|
|
75
|
+
baseDirectory: String(new URL("./", callSite.url)),
|
|
76
|
+
});
|
|
77
|
+
const outDirectoryUrl = new URL(
|
|
78
|
+
`./${asValidFilename(scenario)}/`,
|
|
79
|
+
snapshotFileUrl,
|
|
80
|
+
);
|
|
81
|
+
const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl);
|
|
82
|
+
const sideEffectsMarkdown = renderSideEffects(sideEffects, {
|
|
83
|
+
sideEffectFileUrl: snapshotFileUrl,
|
|
84
|
+
outDirectoryUrl,
|
|
85
|
+
generatedBy: false,
|
|
86
|
+
titleLevel: 3,
|
|
87
|
+
errorStackHidden,
|
|
88
|
+
});
|
|
89
|
+
outDirectorySnapshot.compare(throwWhenDiff);
|
|
90
|
+
markdown += sideEffectsMarkdown;
|
|
91
|
+
}
|
|
92
|
+
const sideEffectFileSnapshot = takeFileSnapshot(snapshotFileUrl);
|
|
93
|
+
sideEffectFileSnapshot.update(markdown, {
|
|
94
|
+
mockFluctuatingValues: false,
|
|
95
|
+
throwWhenDiff,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const generateExecutingLink = (sourceFileUrl, snapshotFileUrl) => {
|
|
100
|
+
const relativeUrl = urlToRelativeUrl(sourceFileUrl, snapshotFileUrl, {
|
|
101
|
+
preferRelativeNotation: true,
|
|
102
|
+
});
|
|
103
|
+
const href = `${relativeUrl}`;
|
|
104
|
+
const text = `${relativeUrl}`;
|
|
105
|
+
return ` executing <a href="${href}">${text}</a>`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// see https://github.com/parshap/node-sanitize-filename/blob/master/index.js
|
|
109
|
+
const asValidFilename = (string) => {
|
|
110
|
+
return string
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/[ ,.]/g, "_")
|
|
114
|
+
.replace(/["/?<>\\:*|]/g, "");
|
|
115
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const groupSideEffectsPer = (
|
|
2
|
+
allSideEffects,
|
|
3
|
+
isInsideGroup,
|
|
4
|
+
{ createGroupSideEffect },
|
|
5
|
+
) => {
|
|
6
|
+
const groupArray = groupBy(allSideEffects, (sideEffect) => {
|
|
7
|
+
if (sideEffect.skippable) {
|
|
8
|
+
return IGNORE;
|
|
9
|
+
}
|
|
10
|
+
const isInsideResult = isInsideGroup(sideEffect);
|
|
11
|
+
if (isInsideResult === IGNORE) {
|
|
12
|
+
return IGNORE;
|
|
13
|
+
}
|
|
14
|
+
return isInsideResult ? "YES" : "NO";
|
|
15
|
+
});
|
|
16
|
+
for (const group of groupArray) {
|
|
17
|
+
if (group.id !== "YES") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const sideEffectArray = group.values;
|
|
21
|
+
if (sideEffectArray.length < 2) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const groupSideEffect = createGroupSideEffect(sideEffectArray);
|
|
25
|
+
const firstEffect = sideEffectArray[0];
|
|
26
|
+
allSideEffects.replaceSideEffect(firstEffect, groupSideEffect);
|
|
27
|
+
for (const sideEffect of sideEffectArray.slice(1)) {
|
|
28
|
+
allSideEffects.removeSideEffect(sideEffect);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const IGNORE = {};
|
|
34
|
+
|
|
35
|
+
const groupBy = (array, groupCallback) => {
|
|
36
|
+
let i = 0;
|
|
37
|
+
const groupArray = [];
|
|
38
|
+
let currentGroup = null;
|
|
39
|
+
while (i < array.length) {
|
|
40
|
+
const value = array[i];
|
|
41
|
+
i++;
|
|
42
|
+
const groupId = groupCallback(value);
|
|
43
|
+
if (groupId === IGNORE) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (currentGroup === null) {
|
|
47
|
+
currentGroup = {
|
|
48
|
+
id: groupId,
|
|
49
|
+
values: [value],
|
|
50
|
+
};
|
|
51
|
+
groupArray.push(currentGroup);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (groupId === currentGroup.id) {
|
|
55
|
+
currentGroup.values.push(value);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
currentGroup = {
|
|
59
|
+
id: groupId,
|
|
60
|
+
values: [value],
|
|
61
|
+
};
|
|
62
|
+
groupArray.push(currentGroup);
|
|
63
|
+
}
|
|
64
|
+
return groupArray;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// const groups = groupFileTogether([
|
|
68
|
+
// {
|
|
69
|
+
// name: "a",
|
|
70
|
+
// type: "fs:write_file",
|
|
71
|
+
// },
|
|
72
|
+
// {
|
|
73
|
+
// name: "b",
|
|
74
|
+
// type: "fs:write_directory",
|
|
75
|
+
// },
|
|
76
|
+
// {
|
|
77
|
+
// name: "c",
|
|
78
|
+
// type: "fs:write_file",
|
|
79
|
+
// },
|
|
80
|
+
// {
|
|
81
|
+
// name: "d",
|
|
82
|
+
// type: "other",
|
|
83
|
+
// },
|
|
84
|
+
// {
|
|
85
|
+
// name: "e",
|
|
86
|
+
// type: "fs:write_file",
|
|
87
|
+
// },
|
|
88
|
+
// ]);
|
|
89
|
+
// debugger;
|