@jsenv/snapshot 2.8.9 → 2.9.0

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.9",
3
+ "version": "2.9.0",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -33,7 +33,7 @@
33
33
  "test": "node --conditions=development ./scripts/test.mjs"
34
34
  },
35
35
  "dependencies": {
36
- "@jsenv/assert": "4.1.15",
36
+ "@jsenv/assert": "4.2.1",
37
37
  "@jsenv/ast": "6.2.16",
38
38
  "@jsenv/exception": "1.1.0",
39
39
  "@jsenv/filesystem": "4.10.0",
@@ -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
  }
@@ -8,6 +8,7 @@ export const createCaptureSideEffects = ({
8
8
  logEffects = true,
9
9
  filesystemEffects = true,
10
10
  filesystemActions,
11
+ executionEffects = {},
11
12
  rootDirectoryUrl,
12
13
  replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
13
14
  rootDirectoryUrl,
@@ -285,6 +286,12 @@ export const createCaptureSideEffects = ({
285
286
  return sideEffects;
286
287
  },
287
288
  (e) => {
289
+ if (executionEffects.catch === false) {
290
+ throw e;
291
+ }
292
+ if (typeof executionEffects.catch === "function") {
293
+ executionEffects.catch(e);
294
+ }
288
295
  onReject(e);
289
296
  onFinally();
290
297
  return sideEffects;
@@ -295,6 +302,12 @@ export const createCaptureSideEffects = ({
295
302
  onReturn(valueReturned);
296
303
  return sideEffects;
297
304
  } catch (e) {
305
+ if (executionEffects.catch === false) {
306
+ throw e;
307
+ }
308
+ if (typeof executionEffects.catch === "function") {
309
+ executionEffects.catch(e);
310
+ }
298
311
  onCatch(e);
299
312
  return sideEffects;
300
313
  } finally {
@@ -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
  });
@@ -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 },
@@ -63,6 +65,8 @@ export const renderSideEffects = (
63
65
  };
64
66
 
65
67
  let markdown = "";
68
+ markdown += `# [${title}](${urlToRelativeUrl(sourceFileUrl, sideEffectMdFileUrl, { preferRelativeNotation: true })})`;
69
+ markdown += "\n\n";
66
70
  let sideEffectNumber = 0;
67
71
  for (const sideEffect of sideEffects) {
68
72
  if (sideEffect.skippable) {
@@ -76,14 +80,15 @@ export const renderSideEffects = (
76
80
  }
77
81
  const lastSideEffectNumber = sideEffectNumber;
78
82
 
83
+ let sideEffectMd = "";
79
84
  for (const sideEffect of sideEffects) {
80
85
  if (sideEffect.skippable) {
81
86
  continue;
82
87
  }
83
- if (markdown) {
84
- markdown += "\n\n";
88
+ if (sideEffectMd) {
89
+ sideEffectMd += "\n\n";
85
90
  }
86
- markdown += renderOneSideEffect(sideEffect, {
91
+ sideEffectMd += renderOneSideEffect(sideEffect, {
87
92
  sideEffectMdFileUrl,
88
93
  generateOutFileUrl,
89
94
  rootDirectoryUrl,
@@ -94,22 +99,40 @@ export const renderSideEffects = (
94
99
  lastSideEffectNumber,
95
100
  });
96
101
  }
102
+ markdown += sideEffectMd;
97
103
  if (generatedBy) {
98
- let generatedByLink = renderSmallLink(
104
+ markdown += "\n";
105
+ markdown += "---";
106
+ markdown += "\n\n";
107
+ markdown += renderSmallLink(
99
108
  {
100
109
  text: "@jsenv/snapshot",
101
110
  href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
102
111
  },
103
- {
104
- prefix: "Generated by ",
105
- },
112
+ { prefix: "Generated by " },
106
113
  );
107
- markdown += "\n\n";
108
- markdown += generatedByLink;
109
114
  }
110
115
  return markdown;
111
116
  };
112
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
+
113
136
  export const renderSmallLink = (
114
137
  link,
115
138
  { prefix = "", suffix = "", indent } = {},
@@ -32,7 +32,6 @@ export const snapshotSideEffects = (
32
32
  {
33
33
  sideEffectMdFileUrl,
34
34
  outFilePattern = "_[source_filename]/[filename]",
35
- errorTransform,
36
35
  throwWhenDiff,
37
36
  ...captureOptions
38
37
  } = {},
@@ -60,9 +59,10 @@ export const snapshotSideEffects = (
60
59
  const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl);
61
60
  const onSideEffects = (sideEffects) => {
62
61
  const sideEffectFileContent = renderSideEffects(sideEffects, {
62
+ sourceFileUrl,
63
+ title: urlToFilename(sourceFileUrl),
63
64
  sideEffectMdFileUrl,
64
65
  generateOutFileUrl,
65
- errorTransform,
66
66
  });
67
67
  writeFileSync(sideEffectMdFileUrl, sideEffectFileContent);
68
68
  outDirectorySnapshot.compare(throwWhenDiff);
@@ -37,9 +37,7 @@ export const snapshotTests = async (
37
37
  },
38
38
  rootDirectoryUrl,
39
39
  generatedBy = true,
40
- linkToSource = true,
41
- linkToEachSource,
42
- errorTransform,
40
+ executionEffects,
43
41
  logEffects,
44
42
  filesystemEffects,
45
43
  throwWhenDiff = process.env.CI,
@@ -47,7 +45,7 @@ export const snapshotTests = async (
47
45
  ) => {
48
46
  filesystemActions = {
49
47
  ...filesystemActions,
50
- "**/*.svg": "presence_only",
48
+ "**/*.svg": "compare_presence_only",
51
49
  };
52
50
 
53
51
  const sourceName = urlToBasename(sourceFileUrl, true);
@@ -67,93 +65,112 @@ export const snapshotTests = async (
67
65
 
68
66
  const dirUrlMap = new Map();
69
67
  const sideEffectsMap = new Map();
70
- const testMap = new Map();
71
- const onlyTestMap = new Map();
68
+ const testArray = [];
69
+ let index = 0;
70
+ let hasOnly = false;
72
71
  const test = (scenario, fn, options) => {
73
- if (testMap.has(scenario) || onlyTestMap.has(scenario)) {
74
- console.warn(`test override "${scenario}"`);
75
- }
76
- 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++;
77
80
  };
78
81
  test.ONLY = (scenario, fn, options) => {
79
- if (testMap.has(scenario) || onlyTestMap.has(scenario)) {
80
- console.warn(`test override "${scenario}"`);
81
- }
82
- 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++;
83
92
  };
84
93
  const fnReturnValue = await fnRegisteringTest({ test });
85
-
86
- let activeTestMap;
87
- const toIgnoreActions = {};
88
- if (onlyTestMap.size) {
89
- activeTestMap = onlyTestMap;
90
- for (const [scenario] of testMap) {
91
- const testScenario = asValidFilename(scenario);
92
- const generateScenarioOutFileUrl = (outfilename) => {
93
- return generateOutFileUrl(`${testScenario}/${outfilename}`);
94
- };
95
- const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
96
- toIgnoreActions[scenarioOutDirectoryUrl] = "ignore";
97
- }
98
- } else {
99
- activeTestMap = testMap;
100
- }
101
- // ignore tout ceux aui sont désactivé
102
- const outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl, {
103
- ...filesystemActions,
104
- ...toIgnoreActions,
105
- });
106
94
  const captureSideEffects = createCaptureSideEffects({
107
95
  sourceFileUrl,
108
96
  rootDirectoryUrl,
97
+ executionEffects,
109
98
  logEffects,
110
99
  filesystemEffects,
111
100
  filesystemActions,
112
101
  });
102
+ const sourceRelativeUrl = urlToRelativeUrl(
103
+ sourceFileUrl,
104
+ sideEffectMdFileUrl,
105
+ {
106
+ preferRelativeNotation: true,
107
+ },
108
+ );
113
109
  let markdown = "";
114
- markdown += `# ${sourceName}`;
115
- if (generatedBy) {
116
- let generatedByLink = renderSmallLink(
117
- {
118
- text: "@jsenv/snapshot",
119
- href: "https://github.com/jsenv/core/tree/main/packages/independent/snapshot",
120
- },
121
- {
122
- prefix: "Generated by ",
123
- suffix:
124
- linkToSource && sourceFileUrl
125
- ? generateExecutingLink(sourceFileUrl, sideEffectMdFileUrl)
126
- : "",
127
- },
110
+ markdown += `# [${urlToFilename(sourceFileUrl)}](${sourceRelativeUrl})`;
111
+ markdown += `\n\n`;
112
+ let testMd = "";
113
+ testMd += `\n\n`;
114
+ let outDirectorySnapshot;
115
+ if (testArray.length === 0) {
116
+ outDirectorySnapshot = takeDirectorySnapshot(
117
+ outDirectoryUrl,
118
+ filesystemActions,
128
119
  );
129
- markdown += "\n\n";
130
- markdown += generatedByLink;
131
- }
132
- const scenarioDirs = [];
133
- for (const [scenario, { fn, callSite }] of activeTestMap) {
134
- markdown += "\n\n";
135
- markdown += `## ${scenario}`;
136
- markdown += "\n\n";
137
- const sideEffects = await captureSideEffects(fn, {
138
- callSite: linkToEachSource ? callSite : undefined,
139
- baseDirectory: String(new URL("./", callSite.url)),
140
- });
141
- sideEffectsMap.set(scenario, sideEffects);
142
- const testScenario = asValidFilename(scenario);
143
- scenarioDirs.push(testScenario);
144
- const generateScenarioOutFileUrl = (outfilename) => {
145
- return generateOutFileUrl(`${testScenario}/${outfilename}`);
146
- };
147
- const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
148
- dirUrlMap.set(scenario, scenarioOutDirectoryUrl);
149
- const sideEffectsMarkdown = renderSideEffects(sideEffects, {
150
- sideEffectMdFileUrl,
151
- generateOutFileUrl: generateScenarioOutFileUrl,
152
- generatedBy: false,
153
- titleLevel: 3,
154
- errorTransform,
120
+ testMd += "No test";
121
+ testMd += "\n";
122
+ } else {
123
+ let allTestMd = "";
124
+ const scenarioDirs = [];
125
+ const scenarioIgnoreActions = {};
126
+ const testToExecuteArray = [];
127
+ for (const testRegistered of testArray) {
128
+ const { scenario, only } = testRegistered;
129
+ const scenarioFilename = asValidFilename(scenario);
130
+ scenarioDirs.push(scenarioFilename);
131
+ const generateScenarioOutFileUrl = (outFilename) => {
132
+ return generateOutFileUrl(`${scenarioFilename}/${outFilename}`);
133
+ };
134
+ const scenarioOutDirectoryUrl = generateScenarioOutFileUrl("");
135
+ dirUrlMap.set(scenario, scenarioOutDirectoryUrl);
136
+ const scenarioMdFileUrl = generateScenarioOutFileUrl(
137
+ `${scenarioFilename}.md`,
138
+ );
139
+ allTestMd += `- [${scenario}](${urlToRelativeUrl(scenarioMdFileUrl, sideEffectMdFileUrl)})`;
140
+ allTestMd += "\n";
141
+ if (hasOnly && !only) {
142
+ scenarioIgnoreActions[scenarioOutDirectoryUrl] = "ignore";
143
+ continue;
144
+ }
145
+ testRegistered.generateScenarioOutFileUrl = generateScenarioOutFileUrl;
146
+ testRegistered.scenarioMdFileUrl = scenarioMdFileUrl;
147
+ testToExecuteArray.push(testRegistered);
148
+ }
149
+ outDirectorySnapshot = takeDirectorySnapshot(outDirectoryUrl, {
150
+ ...filesystemActions,
151
+ ...scenarioIgnoreActions,
155
152
  });
156
- markdown += sideEffectsMarkdown;
153
+ for (const testToExecute of testToExecuteArray) {
154
+ const {
155
+ scenario,
156
+ fn,
157
+ callSite,
158
+ scenarioMdFileUrl,
159
+ generateScenarioOutFileUrl,
160
+ } = testToExecute;
161
+ const sideEffects = await captureSideEffects(fn, {
162
+ baseDirectory: String(new URL("./", callSite.url)),
163
+ });
164
+ sideEffectsMap.set(scenario, sideEffects);
165
+ const sideEffectsMarkdown = renderSideEffects(sideEffects, {
166
+ sourceFileUrl: `${callSite.url}#L${callSite.line}`,
167
+ sideEffectMdFileUrl: scenarioMdFileUrl,
168
+ generateOutFileUrl: generateScenarioOutFileUrl,
169
+ title: scenario,
170
+ });
171
+ writeFileSync(scenarioMdFileUrl, sideEffectsMarkdown);
172
+ }
173
+ testMd += allTestMd;
157
174
  }
158
175
  if (typeof fnReturnValue === "function") {
159
176
  await fnReturnValue();
@@ -174,21 +191,25 @@ export const snapshotTests = async (
174
191
  // });
175
192
  // }
176
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
+ }
177
207
  writeFileSync(sideEffectMdFileUrl, markdown);
178
208
  outDirectorySnapshot.compare(throwWhenDiff);
179
209
 
180
210
  return { dirUrlMap, sideEffectsMap };
181
211
  };
182
212
 
183
- const generateExecutingLink = (sourceFileUrl, sideEffectFileUrl) => {
184
- const relativeUrl = urlToRelativeUrl(sourceFileUrl, sideEffectFileUrl, {
185
- preferRelativeNotation: true,
186
- });
187
- const href = `${relativeUrl}`;
188
- const text = `${relativeUrl}`;
189
- return ` executing <a href="${href}">${text}</a>`;
190
- };
191
-
192
213
  // see https://github.com/parshap/node-sanitize-filename/blob/master/index.js
193
214
  const asValidFilename = (string) => {
194
215
  return string