@jsenv/snapshot 2.8.8 → 2.8.10

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.8.8",
3
+ "version": "2.8.10",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -33,9 +33,9 @@
33
33
  "test": "node --conditions=development ./scripts/test.mjs"
34
34
  },
35
35
  "dependencies": {
36
- "@jsenv/assert": "4.1.15",
37
- "@jsenv/ast": "6.2.15",
38
- "@jsenv/exception": "1.0.2",
36
+ "@jsenv/assert": "4.2.0",
37
+ "@jsenv/ast": "6.2.16",
38
+ "@jsenv/exception": "1.1.0",
39
39
  "@jsenv/filesystem": "4.10.0",
40
40
  "@jsenv/terminal-recorder": "1.4.4",
41
41
  "@jsenv/urls": "2.5.2",
@@ -38,8 +38,8 @@ export const replaceFluctuatingValues = (
38
38
  const extension = urlToExtension(fileUrl);
39
39
  if (extension === ".html") {
40
40
  stringType = "html";
41
- } else if (extension === ".svg") {
42
- stringType = "svg";
41
+ } else if (extension === ".svg" || extension === ".xml") {
42
+ stringType = "xml";
43
43
  } else if (extension === ".json" || CONTENT_TYPE.isJson(contentType)) {
44
44
  stringType = "json";
45
45
  }
@@ -92,10 +92,10 @@ export const replaceFluctuatingValues = (
92
92
  const replaced = replaceInObject(jsValue, { replace: replaceThings });
93
93
  return JSON.stringify(replaced, null, " ");
94
94
  }
95
- if (stringType === "html" || stringType === "svg") {
95
+ if (stringType === "html" || stringType === "xml") {
96
96
  // do parse html
97
97
  const htmlAst =
98
- stringType === "svg"
98
+ stringType === "xml"
99
99
  ? parseSvgString(value)
100
100
  : parseHtml({
101
101
  html: value,
@@ -112,7 +112,14 @@ export const replaceFluctuatingValues = (
112
112
  const attributes = getHtmlNodeAttributes(node);
113
113
  if (attributes) {
114
114
  for (const name of Object.keys(attributes)) {
115
- attributes[name] = replaceThings(attributes[name]);
115
+ const attributeValue = attributes[name];
116
+ if (name === "timestamp") {
117
+ attributes[name] = "[timestamp]";
118
+ } else if (name === "time") {
119
+ attributes[name] = "[time]";
120
+ } else {
121
+ attributes[name] = replaceThings(attributeValue);
122
+ }
116
123
  }
117
124
  setHtmlNodeAttributes(node, attributes);
118
125
  }
@@ -5,7 +5,31 @@ import { groupLogSideEffects } from "./group_log_side_effects.js";
5
5
  const logSideEffectsOptionsDefault = {
6
6
  prevent: true,
7
7
  group: true,
8
- ignore: false,
8
+ level: "info", // "debug", "trace", "info", "warn", "error", "off"
9
+ onlyIfLevel: "debug",
10
+ };
11
+
12
+ export const isLogSideEffect = (sideEffect) => {
13
+ const { type } = sideEffect;
14
+ return typeof typeToLevelMap[type] === "number";
15
+ };
16
+ const typeToLevelMap = {
17
+ "console.debug": 0,
18
+ "console.trace": 1,
19
+ "console.info": 2,
20
+ "console.log": 2,
21
+ "process.stdout": 2,
22
+ "console.warn": 3,
23
+ "console.error": 4,
24
+ "process.stderr": 4,
25
+ };
26
+ const levelNumberMap = {
27
+ debug: 0,
28
+ trace: 1,
29
+ info: 2,
30
+ warn: 3,
31
+ error: 4,
32
+ off: 5,
9
33
  };
10
34
 
11
35
  export const logSideEffects = (logSideEffectsOptions) => {
@@ -16,7 +40,30 @@ export const logSideEffects = (logSideEffectsOptions) => {
16
40
  return {
17
41
  name: "console",
18
42
  install: (addSideEffect, { addFinallyCallback }) => {
19
- const { prevent, group, ignore } = logSideEffectsOptions;
43
+ const { level, prevent, group, onlyIfLevel } = logSideEffectsOptions;
44
+ const levelNumber = levelNumberMap[level];
45
+ if (onlyIfLevel && onlyIfLevel !== "debug") {
46
+ const onlyIfLevelNumber = levelNumberMap[onlyIfLevel];
47
+ addFinallyCallback((sideEffects) => {
48
+ const logSideEffects = [];
49
+ let hasOneOfLevelOrAbove;
50
+ for (const sideEffect of sideEffects) {
51
+ if (!isLogSideEffect(sideEffect)) {
52
+ continue;
53
+ }
54
+ logSideEffects.push(sideEffect);
55
+ if (!hasOneOfLevelOrAbove) {
56
+ const sideEffectLevel = typeToLevelMap[sideEffect.type];
57
+ hasOneOfLevelOrAbove = sideEffectLevel >= onlyIfLevelNumber;
58
+ }
59
+ }
60
+ if (!hasOneOfLevelOrAbove) {
61
+ for (const logSideEffect of logSideEffects) {
62
+ sideEffects.removeSideEffect(logSideEffect);
63
+ }
64
+ }
65
+ });
66
+ }
20
67
  if (group) {
21
68
  addFinallyCallback((sideEffects) => {
22
69
  groupLogSideEffects(sideEffects, {
@@ -60,8 +107,23 @@ export const logSideEffects = (logSideEffectsOptions) => {
60
107
  });
61
108
  });
62
109
  }
110
+
63
111
  const addLogSideEffect = (type, message) => {
64
- if (ignore) {
112
+ const sideEffectLevel = typeToLevelMap[type];
113
+ if (sideEffectLevel < levelNumber) {
114
+ return;
115
+ }
116
+ message = String(message);
117
+ // some messages are flaky by definition we don't want to
118
+ // fail on thoose
119
+ if (
120
+ message.includes(
121
+ "GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels",
122
+ )
123
+ ) {
124
+ return;
125
+ }
126
+ if (message.includes("task queue exceeded allotted deadline by")) {
65
127
  return;
66
128
  }
67
129
  addSideEffect({
@@ -1,12 +1,13 @@
1
1
  import { writeFileSync } from "@jsenv/filesystem";
2
2
  import { startTerminalRecording } from "@jsenv/terminal-recorder";
3
+ import { isLogSideEffect } from "./log/log_side_effects.js";
3
4
 
4
5
  export const renderLogsGif = async (sideEffects, gitFileUrl) => {
5
6
  const terminalRecorder = await startTerminalRecording({
6
7
  gif: true,
7
8
  });
8
9
  for (const sideEffect of sideEffects) {
9
- if (sideEffect.type === "console.log") {
10
+ if (isLogSideEffect(sideEffect)) {
10
11
  await terminalRecorder.write(sideEffect.value, {
11
12
  delay: sideEffect.delay,
12
13
  });
@@ -1,4 +1,4 @@
1
- import { createException } from "@jsenv/exception";
1
+ import { createException, stringifyException } from "@jsenv/exception";
2
2
  import { writeFileSync } from "@jsenv/filesystem";
3
3
  import { renderTerminalSvg } from "@jsenv/terminal-recorder";
4
4
  import { urlToExtension, urlToRelativeUrl } from "@jsenv/urls";
@@ -37,9 +37,11 @@ export const createBigSizeEffect =
37
37
  export const renderSideEffects = (
38
38
  sideEffects,
39
39
  {
40
+ sourceFileUrl,
40
41
  sideEffectMdFileUrl,
41
42
  generateOutFileUrl,
42
43
  generatedBy = true,
44
+ title,
43
45
  titleLevel = 1,
44
46
  getBigSizeEffect = createBigSizeEffect({
45
47
  details: { line: 15, length: 2000 },
@@ -48,8 +50,7 @@ export const renderSideEffects = (
48
50
  // and in that case we might want to move it to an other file
49
51
  dedicatedFile: { line: 50, length: 5000 },
50
52
  }),
51
- errorStackHidden,
52
- errorMessageTransform,
53
+ errorTransform,
53
54
  } = {},
54
55
  ) => {
55
56
  const { rootDirectoryUrl, replaceFilesystemWellKnownValues } =
@@ -64,6 +65,8 @@ export const renderSideEffects = (
64
65
  };
65
66
 
66
67
  let markdown = "";
68
+ markdown += `# [${title}](${urlToRelativeUrl(sourceFileUrl, sideEffectMdFileUrl, { preferRelativeNotation: true })})`;
69
+ markdown += "\n\n";
67
70
  let sideEffectNumber = 0;
68
71
  for (const sideEffect of sideEffects) {
69
72
  if (sideEffect.skippable) {
@@ -77,41 +80,59 @@ export const renderSideEffects = (
77
80
  }
78
81
  const lastSideEffectNumber = sideEffectNumber;
79
82
 
83
+ let sideEffectMd = "";
80
84
  for (const sideEffect of sideEffects) {
81
85
  if (sideEffect.skippable) {
82
86
  continue;
83
87
  }
84
- if (markdown) {
85
- markdown += "\n\n";
88
+ if (sideEffectMd) {
89
+ sideEffectMd += "\n\n";
86
90
  }
87
- markdown += renderOneSideEffect(sideEffect, {
91
+ sideEffectMd += renderOneSideEffect(sideEffect, {
88
92
  sideEffectMdFileUrl,
89
93
  generateOutFileUrl,
90
94
  rootDirectoryUrl,
91
95
  titleLevel,
92
96
  getBigSizeEffect,
93
97
  replace,
94
- errorStackHidden,
95
- errorMessageTransform,
98
+ errorTransform,
96
99
  lastSideEffectNumber,
97
100
  });
98
101
  }
102
+ markdown += sideEffectMd;
99
103
  if (generatedBy) {
100
- let generatedByLink = renderSmallLink(
104
+ markdown += "\n";
105
+ markdown += "---";
106
+ markdown += "\n\n";
107
+ markdown += renderSmallLink(
101
108
  {
102
109
  text: "@jsenv/snapshot",
103
110
  href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
104
111
  },
105
- {
106
- prefix: "Generated by ",
107
- },
112
+ { prefix: "Generated by " },
108
113
  );
109
- markdown += "\n\n";
110
- markdown += generatedByLink;
111
114
  }
112
115
  return markdown;
113
116
  };
114
117
 
118
+ export const renderInfosTableMd = (infos) => {
119
+ const infoKeys = Object.keys(infos);
120
+ if (infoKeys.length === 0) {
121
+ return "";
122
+ }
123
+ let infoTableMd = "";
124
+ infoTableMd += "\n";
125
+ infoTableMd += `Infos | &nbsp;`;
126
+ infoTableMd += "\n";
127
+ infoTableMd += `----- | ------`;
128
+ for (const key of infoKeys) {
129
+ infoTableMd += "\n";
130
+ infoTableMd += `${key} | ${infos[key]}`;
131
+ }
132
+ infoTableMd += "\n";
133
+ return infoTableMd;
134
+ };
135
+
115
136
  export const renderSmallLink = (
116
137
  link,
117
138
  { prefix = "", suffix = "", indent } = {},
@@ -139,8 +160,7 @@ const renderOneSideEffect = (
139
160
  titleLevel,
140
161
  getBigSizeEffect,
141
162
  replace,
142
- errorStackHidden,
143
- errorMessageTransform,
163
+ errorTransform,
144
164
  lastSideEffectNumber,
145
165
  },
146
166
  ) => {
@@ -175,8 +195,7 @@ const renderOneSideEffect = (
175
195
  generateOutFileUrl,
176
196
  replace,
177
197
  rootDirectoryUrl,
178
- errorStackHidden,
179
- errorMessageTransform,
198
+ errorTransform,
180
199
  });
181
200
  }
182
201
  if (sideEffect.code === "source_code") {
@@ -212,8 +231,7 @@ const renderText = (
212
231
  generateOutFileUrl,
213
232
  replace,
214
233
  rootDirectoryUrl,
215
- errorStackHidden,
216
- errorMessageTransform,
234
+ errorTransform,
217
235
  },
218
236
  ) => {
219
237
  if (text && typeof text === "object") {
@@ -253,11 +271,9 @@ const renderText = (
253
271
  // return renderMarkdownBlock(text.value.stack);
254
272
  const exception = createException(jsValue, {
255
273
  rootDirectoryUrl,
256
- errorMessageTransform,
274
+ errorTransform,
257
275
  });
258
- const exceptionText = errorStackHidden
259
- ? `${exception.name}: ${exception.message}`
260
- : exception.stack || exception.message || exception;
276
+ const exceptionText = stringifyException(exception);
261
277
  return renderPotentialAnsi(exceptionText, {
262
278
  stringType: "error",
263
279
  sideEffect,
@@ -32,7 +32,7 @@ export const snapshotSideEffects = (
32
32
  {
33
33
  sideEffectMdFileUrl,
34
34
  outFilePattern = "_[source_filename]/[filename]",
35
- errorStackHidden,
35
+ errorTransform,
36
36
  throwWhenDiff,
37
37
  ...captureOptions
38
38
  } = {},
@@ -60,9 +60,11 @@ export const snapshotSideEffects = (
60
60
  const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl);
61
61
  const onSideEffects = (sideEffects) => {
62
62
  const sideEffectFileContent = renderSideEffects(sideEffects, {
63
+ sourceFileUrl,
64
+ title: urlToFilename(sourceFileUrl),
63
65
  sideEffectMdFileUrl,
64
66
  generateOutFileUrl,
65
- errorStackHidden,
67
+ errorTransform,
66
68
  });
67
69
  writeFileSync(sideEffectMdFileUrl, sideEffectFileContent);
68
70
  outDirectorySnapshot.compare(throwWhenDiff);
@@ -37,10 +37,7 @@ export const snapshotTests = async (
37
37
  },
38
38
  rootDirectoryUrl,
39
39
  generatedBy = true,
40
- linkToSource = true,
41
- linkToEachSource,
42
- errorStackHidden,
43
- errorMessageTransform,
40
+ errorTransform,
44
41
  logEffects,
45
42
  filesystemEffects,
46
43
  throwWhenDiff = process.env.CI,
@@ -48,7 +45,7 @@ export const snapshotTests = async (
48
45
  ) => {
49
46
  filesystemActions = {
50
47
  ...filesystemActions,
51
- "**/*.svg": "presence_only",
48
+ "**/*.svg": "compare_presence_only",
52
49
  };
53
50
 
54
51
  const sourceName = urlToBasename(sourceFileUrl, true);
@@ -68,42 +65,32 @@ export const snapshotTests = async (
68
65
 
69
66
  const dirUrlMap = new Map();
70
67
  const sideEffectsMap = new Map();
71
- const testMap = new Map();
72
- const onlyTestMap = new Map();
68
+ const testArray = [];
69
+ let index = 0;
70
+ let hasOnly = false;
73
71
  const test = (scenario, fn, options) => {
74
- if (testMap.has(scenario) || onlyTestMap.has(scenario)) {
75
- console.warn(`test override "${scenario}"`);
76
- }
77
- testMap.set(scenario, { fn, options, callSite: getCallerLocation(2) });
72
+ testArray.push({
73
+ index,
74
+ scenario,
75
+ fn,
76
+ options,
77
+ callSite: getCallerLocation(2),
78
+ });
79
+ index++;
78
80
  };
79
81
  test.ONLY = (scenario, fn, options) => {
80
- if (testMap.has(scenario) || onlyTestMap.has(scenario)) {
81
- console.warn(`test override "${scenario}"`);
82
- }
83
- onlyTestMap.set(scenario, { fn, options, callSite: getCallerLocation(2) });
82
+ hasOnly = true;
83
+ testArray.push({
84
+ index,
85
+ scenario,
86
+ fn,
87
+ options,
88
+ callSite: getCallerLocation(2),
89
+ only: true,
90
+ });
91
+ index++;
84
92
  };
85
93
  const fnReturnValue = await fnRegisteringTest({ test });
86
-
87
- let activeTestMap;
88
- const toIgnoreActions = {};
89
- if (onlyTestMap.size) {
90
- activeTestMap = onlyTestMap;
91
- for (const [scenario] of testMap) {
92
- const testScenario = asValidFilename(scenario);
93
- const generateScenarioOutFileUrl = (outfilename) => {
94
- return generateOutFileUrl(`${testScenario}/${outfilename}`);
95
- };
96
- const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
97
- toIgnoreActions[scenarioOutDirectoryUrl] = "ignore";
98
- }
99
- } else {
100
- activeTestMap = testMap;
101
- }
102
- // ignore tout ceux aui sont désactivé
103
- const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl, {
104
- ...filesystemActions,
105
- ...toIgnoreActions,
106
- });
107
94
  const captureSideEffects = createCaptureSideEffects({
108
95
  sourceFileUrl,
109
96
  rootDirectoryUrl,
@@ -111,51 +98,79 @@ export const snapshotTests = async (
111
98
  filesystemEffects,
112
99
  filesystemActions,
113
100
  });
101
+ const sourceRelativeUrl = urlToRelativeUrl(
102
+ sourceFileUrl,
103
+ sideEffectMdFileUrl,
104
+ {
105
+ preferRelativeNotation: true,
106
+ },
107
+ );
114
108
  let markdown = "";
115
- markdown += `# ${sourceName}`;
116
- if (generatedBy) {
117
- let generatedByLink = renderSmallLink(
118
- {
119
- text: "@jsenv/snapshot",
120
- href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
121
- },
122
- {
123
- prefix: "Generated by ",
124
- suffix:
125
- linkToSource && sourceFileUrl
126
- ? generateExecutingLink(sourceFileUrl, sideEffectMdFileUrl)
127
- : "",
128
- },
109
+ markdown += `# [${urlToFilename(sourceFileUrl)}](${sourceRelativeUrl})`;
110
+ markdown += `\n\n`;
111
+ let testMd = "";
112
+ testMd += `\n\n`;
113
+ let outDirectorySnapshot;
114
+ if (testArray.length === 0) {
115
+ outDirectorySnapshot = takeDirectorySnapshot(
116
+ outDirectoryUrl,
117
+ filesystemActions,
129
118
  );
130
- markdown += "\n\n";
131
- markdown += generatedByLink;
132
- }
133
- const scenarioDirs = [];
134
- for (const [scenario, { fn, callSite }] of activeTestMap) {
135
- markdown += "\n\n";
136
- markdown += `## ${scenario}`;
137
- markdown += "\n\n";
138
- const sideEffects = await captureSideEffects(fn, {
139
- callSite: linkToEachSource ? callSite : undefined,
140
- baseDirectory: String(new URL("./", callSite.url)),
141
- });
142
- sideEffectsMap.set(scenario, sideEffects);
143
- const testScenario = asValidFilename(scenario);
144
- scenarioDirs.push(testScenario);
145
- const generateScenarioOutFileUrl = (outfilename) => {
146
- return generateOutFileUrl(`${testScenario}/${outfilename}`);
147
- };
148
- const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
149
- dirUrlMap.set(scenario, scenarioOutDirectoryUrl);
150
- const sideEffectsMarkdown = renderSideEffects(sideEffects, {
151
- sideEffectMdFileUrl,
152
- generateOutFileUrl: generateScenarioOutFileUrl,
153
- generatedBy: false,
154
- titleLevel: 3,
155
- errorStackHidden,
156
- errorMessageTransform,
119
+ testMd += "No test";
120
+ testMd += "\n";
121
+ } else {
122
+ let allTestMd = "";
123
+ const scenarioDirs = [];
124
+ const scenarioIgnoreActions = {};
125
+ const testToExecuteArray = [];
126
+ for (const testRegistered of testArray) {
127
+ const { scenario, only } = testRegistered;
128
+ const scenarioFilename = asValidFilename(scenario);
129
+ scenarioDirs.push(scenarioFilename);
130
+ const generateScenarioOutFileUrl = (outFilename) => {
131
+ return generateOutFileUrl(`${scenarioFilename}/${outFilename}`);
132
+ };
133
+ const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
134
+ dirUrlMap.set(scenario, scenarioOutDirectoryUrl);
135
+ const scenarioMdFileUrl = generateScenarioOutFileUrl(
136
+ `${scenarioFilename}.md`,
137
+ );
138
+ allTestMd += `- [${scenario}](${urlToRelativeUrl(scenarioMdFileUrl, sideEffectMdFileUrl)})`;
139
+ allTestMd += "\n";
140
+ if (hasOnly && !only) {
141
+ scenarioIgnoreActions[scenarioOutDirectoryUrl] = "ignore";
142
+ continue;
143
+ }
144
+ testRegistered.generateScenarioOutFileUrl = generateScenarioOutFileUrl;
145
+ testRegistered.scenarioMdFileUrl = scenarioMdFileUrl;
146
+ testToExecuteArray.push(testRegistered);
147
+ }
148
+ outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl, {
149
+ ...filesystemActions,
150
+ ...scenarioIgnoreActions,
157
151
  });
158
- markdown += sideEffectsMarkdown;
152
+ for (const testToExecute of testToExecuteArray) {
153
+ const {
154
+ scenario,
155
+ fn,
156
+ callSite,
157
+ scenarioMdFileUrl,
158
+ generateScenarioOutFileUrl,
159
+ } = testToExecute;
160
+ const sideEffects = await captureSideEffects(fn, {
161
+ baseDirectory: String(new URL("./", callSite.url)),
162
+ });
163
+ sideEffectsMap.set(scenario, sideEffects);
164
+ const sideEffectsMarkdown = renderSideEffects(sideEffects, {
165
+ sourceFileUrl: `${callSite.url}#L${callSite.line}`,
166
+ sideEffectMdFileUrl: scenarioMdFileUrl,
167
+ generateOutFileUrl: generateScenarioOutFileUrl,
168
+ title: scenario,
169
+ errorTransform,
170
+ });
171
+ writeFileSync(scenarioMdFileUrl, sideEffectsMarkdown);
172
+ }
173
+ testMd += allTestMd;
159
174
  }
160
175
  if (typeof fnReturnValue === "function") {
161
176
  await fnReturnValue();
@@ -176,21 +191,25 @@ export const snapshotTests = async (
176
191
  // });
177
192
  // }
178
193
  // }
194
+ markdown += testMd;
195
+ if (generatedBy) {
196
+ markdown += "\n";
197
+ markdown += "---";
198
+ markdown += "\n\n";
199
+ markdown += renderSmallLink(
200
+ {
201
+ text: "@jsenv/snapshot",
202
+ href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
203
+ },
204
+ { prefix: "Generated by " },
205
+ );
206
+ }
179
207
  writeFileSync(sideEffectMdFileUrl, markdown);
180
208
  outDirectorySnapshot.compare(throwWhenDiff);
181
209
 
182
210
  return { dirUrlMap, sideEffectsMap };
183
211
  };
184
212
 
185
- const generateExecutingLink = (sourceFileUrl, sideEffectFileUrl) => {
186
- const relativeUrl = urlToRelativeUrl(sourceFileUrl, sideEffectFileUrl, {
187
- preferRelativeNotation: true,
188
- });
189
- const href = `${relativeUrl}`;
190
- const text = `${relativeUrl}`;
191
- return ` executing <a href="${href}">${text}</a>`;
192
- };
193
-
194
213
  // see https://github.com/parshap/node-sanitize-filename/blob/master/index.js
195
214
  const asValidFilename = (string) => {
196
215
  return string