@jsenv/snapshot 2.2.1 → 2.2.3

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.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,6 +1,7 @@
1
1
  import {
2
- moveEntrySync,
3
- writeDirectorySync,
2
+ readEntryStatSync,
3
+ readFileSync,
4
+ removeFileSync,
4
5
  writeFileSync,
5
6
  } from "@jsenv/filesystem";
6
7
  import { urlToRelativeUrl } from "@jsenv/urls/src/url_to_relative_url.js";
@@ -10,102 +11,116 @@ import { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
10
11
  export const snapshotFunctionSideEffects = (
11
12
  fn,
12
13
  fnFileUrl,
14
+ sideEffectDirectoryRelativeUrl,
13
15
  {
14
- sideEffectDirectoryName = "output",
15
16
  rootDirectoryUrl = new URL("./", fnFileUrl),
16
17
  captureConsole = true,
17
18
  filesystemEffects,
19
+ restoreFilesystem,
18
20
  } = {},
19
21
  ) => {
20
- const sideEffectDirectoryUrl = new URL(sideEffectDirectoryName, fnFileUrl);
21
- writeDirectorySync(sideEffectDirectoryUrl, { allowUseless: true });
22
+ const sideEffectDirectoryUrl = new URL(
23
+ sideEffectDirectoryRelativeUrl,
24
+ fnFileUrl,
25
+ );
22
26
  const sideEffectDirectorySnapshot = takeDirectorySnapshot(
23
27
  sideEffectDirectoryUrl,
24
28
  );
29
+ const sideEffectFileUrl = new URL(
30
+ "./side_effect.txt",
31
+ sideEffectDirectoryUrl,
32
+ );
33
+ const sideEffects = [];
25
34
  const finallyCallbackSet = new Set();
26
- const errorFileUrl = new URL("./error.txt", sideEffectDirectoryUrl);
27
- const resultFileUrl = new URL("./result.json", sideEffectDirectoryUrl);
28
35
  const onError = (e) => {
29
- writeFileSync(
30
- errorFileUrl,
31
- replaceFluctuatingValues(e.stack, {
32
- fileUrl: errorFileUrl,
33
- }),
34
- );
36
+ sideEffects.push({
37
+ type: "throw",
38
+ value: e,
39
+ });
35
40
  };
36
41
  const onResult = (result) => {
37
- if (result === undefined) {
38
- return;
39
- }
40
- writeFileSync(
41
- resultFileUrl,
42
- replaceFluctuatingValues(JSON.stringify(result, null, " "), {
43
- fileUrl: resultFileUrl,
44
- rootDirectoryUrl,
45
- }),
46
- );
42
+ sideEffects.push({
43
+ type: "return",
44
+ value: result,
45
+ });
47
46
  };
48
47
  const onFinally = () => {
49
48
  for (const finallyCallback of finallyCallbackSet) {
50
49
  finallyCallback();
51
50
  }
51
+ writeFileSync(
52
+ sideEffectFileUrl,
53
+ stringifySideEffects(sideEffects, { rootDirectoryUrl }),
54
+ );
52
55
  sideEffectDirectorySnapshot.compare();
53
56
  };
54
57
  if (captureConsole) {
55
- const installConsoleSpy = (methodName, consoleOutputFileUrl) => {
58
+ const installConsoleSpy = (methodName) => {
56
59
  const methodSpied = console[methodName];
57
- let output = "";
58
60
  console[methodName] = (message) => {
59
- if (output) {
60
- output += "\n";
61
- }
62
- output += message;
61
+ sideEffects.push({
62
+ type: `console.${methodName}`,
63
+ value: message,
64
+ });
63
65
  };
64
66
  finallyCallbackSet.add(() => {
65
67
  console[methodName] = methodSpied;
66
- if (output) {
67
- writeFileSync(
68
- consoleOutputFileUrl,
69
- replaceFluctuatingValues(output, {
70
- fileUrl: consoleOutputFileUrl,
71
- rootDirectoryUrl,
72
- }),
73
- );
74
- }
75
68
  });
76
69
  };
77
- installConsoleSpy(
78
- "error",
79
- new URL("./console_errors.txt", sideEffectDirectoryUrl),
80
- );
81
- installConsoleSpy(
82
- "warn",
83
- new URL("./console_warnings.txt", sideEffectDirectoryUrl),
84
- );
85
- installConsoleSpy(
86
- "info",
87
- new URL("./console_infos.txt", sideEffectDirectoryUrl),
88
- );
89
- installConsoleSpy(
90
- "log",
91
- new URL("./console_logs.txt", sideEffectDirectoryUrl),
92
- );
70
+ installConsoleSpy("error");
71
+ installConsoleSpy("warn");
72
+ installConsoleSpy("info");
73
+ installConsoleSpy("log");
93
74
  }
94
75
  if (filesystemEffects) {
95
- const filesystemEffectDirectoryUrl = new URL(
96
- "./fs/",
97
- sideEffectDirectoryUrl,
98
- );
76
+ const fsSideEffectDirectoryUrl = new URL("./fs/", sideEffectDirectoryUrl);
99
77
  for (const filesystemEffect of filesystemEffects) {
78
+ const from = new URL(filesystemEffect, fnFileUrl);
79
+ const relativeUrl = urlToRelativeUrl(from, fnFileUrl);
80
+ const toUrl = new URL(relativeUrl, fsSideEffectDirectoryUrl);
81
+ const atStartState = getFileState(from);
82
+ const onFileSystemSideEffect = (fsSideEffect) => {
83
+ const last = sideEffects.pop();
84
+ sideEffects.push(fsSideEffect);
85
+ sideEffects.push(last);
86
+ };
100
87
  finallyCallbackSet.add(() => {
101
- const from = new URL(filesystemEffect, fnFileUrl);
102
- const relativeUrl = urlToRelativeUrl(from, fnFileUrl);
103
- moveEntrySync({
104
- from,
105
- to: new URL(relativeUrl, filesystemEffectDirectoryUrl),
106
- noEntryEffect: "none",
107
- overwrite: true,
108
- });
88
+ const nowState = getFileState(from);
89
+ if (atStartState.found && !nowState.found) {
90
+ onFileSystemSideEffect({
91
+ type: `remove file "${relativeUrl}"`,
92
+ });
93
+ if (restoreFilesystem) {
94
+ writeFileSync(from, atStartState.content);
95
+ }
96
+ return;
97
+ }
98
+ // we use same type because we don't want to differentiate between
99
+ // - writing file for the 1st time
100
+ // - updating file content
101
+ // the important part is the file content in the end of the function execution
102
+ if (
103
+ (!atStartState.found && nowState.found) ||
104
+ atStartState.content !== nowState.content ||
105
+ atStartState.mtimeMs !== nowState.mtimeMs
106
+ ) {
107
+ writeFileSync(toUrl, nowState.content);
108
+ onFileSystemSideEffect({
109
+ type: `write file "./fs/${relativeUrl}"`,
110
+ });
111
+ if (restoreFilesystem) {
112
+ if (atStartState.found) {
113
+ if (atStartState.content !== nowState.content) {
114
+ writeFileSync(from, atStartState.content);
115
+ }
116
+ } else {
117
+ removeFileSync(from);
118
+ }
119
+ }
120
+ return;
121
+ }
122
+ // file is exactly the same
123
+ // function did not have any effect on the file
109
124
  });
110
125
  }
111
126
  }
@@ -136,3 +151,67 @@ export const snapshotFunctionSideEffects = (
136
151
  onFinally();
137
152
  }
138
153
  };
154
+
155
+ const stringifySideEffects = (sideEffects, { rootDirectoryUrl }) => {
156
+ let string = "";
157
+ let index = 0;
158
+ for (const sideEffect of sideEffects) {
159
+ if (string) {
160
+ string += "\n\n";
161
+ }
162
+ string += `${index + 1}. ${sideEffect.type}`;
163
+ let value = sideEffect.value;
164
+ if (sideEffect.type.startsWith("console.")) {
165
+ value = replaceFluctuatingValues(value, {
166
+ stringType: "console",
167
+ rootDirectoryUrl,
168
+ });
169
+ string += "\n";
170
+ string += value;
171
+ } else if (
172
+ sideEffect.type.startsWith("remove file") ||
173
+ sideEffect.type.startsWith("write file")
174
+ ) {
175
+ } else if (sideEffect.type === "throw") {
176
+ value = replaceFluctuatingValues(value.stack, {
177
+ stringType: "error",
178
+ });
179
+ string += "\n";
180
+ string += value;
181
+ } else if (sideEffect.type === "return") {
182
+ value =
183
+ value === undefined
184
+ ? undefined
185
+ : replaceFluctuatingValues(JSON.stringify(value, null, " "), {
186
+ stringType: "json",
187
+ rootDirectoryUrl,
188
+ });
189
+ string += "\n";
190
+ string += value;
191
+ } else {
192
+ string += "\n";
193
+ string += value;
194
+ }
195
+ index++;
196
+ }
197
+ return string;
198
+ };
199
+
200
+ const getFileState = (fileUrl) => {
201
+ try {
202
+ const fileContent = readFileSync(fileUrl);
203
+ const { mtimeMs } = readEntryStatSync(fileUrl);
204
+ return {
205
+ found: true,
206
+ mtimeMs,
207
+ content: String(fileContent),
208
+ };
209
+ } catch (e) {
210
+ if (e.code === "ENOENT") {
211
+ return {
212
+ found: false,
213
+ };
214
+ }
215
+ throw e;
216
+ }
217
+ };
@@ -19,6 +19,7 @@ import stripAnsi from "strip-ansi";
19
19
  export const replaceFluctuatingValues = (
20
20
  string,
21
21
  {
22
+ stringType,
22
23
  fileUrl,
23
24
  removeAnsi = true,
24
25
  rootDirectoryUrl = pathToFileURL(process.cwd()),
@@ -60,37 +61,42 @@ export const replaceFluctuatingValues = (
60
61
  return value;
61
62
  };
62
63
 
63
- if (fileUrl) {
64
+ if (fileUrl && stringType === undefined) {
64
65
  const extension = urlToExtension(fileUrl);
65
- if (extension === ".svg" || extension === ".html") {
66
- // do parse html
67
- const htmlAst =
68
- extension === ".svg"
69
- ? parseSvgString(string)
70
- : parseHtml({
71
- html: string,
72
- storeOriginalPositions: false,
73
- });
74
- // for each attribute value
75
- // and each text node content
76
- visitHtmlNodes(htmlAst, {
77
- "*": (node) => {
78
- const htmlNodeText = getHtmlNodeText(node);
79
- if (htmlNodeText) {
80
- setHtmlNodeText(node, replaceThings(htmlNodeText));
81
- }
82
- const attributes = getHtmlNodeAttributes(node);
83
- if (attributes) {
84
- for (const name of Object.keys(attributes)) {
85
- attributes[name] = replaceThings(attributes[name]);
86
- }
87
- setHtmlNodeAttributes(node, attributes);
88
- }
89
- },
90
- });
91
- return stringifyHtmlAst(htmlAst);
66
+ if (extension === ".html") {
67
+ stringType = "html";
68
+ } else if (extension === ".svg") {
69
+ stringType = "svg";
92
70
  }
93
71
  }
72
+ if (stringType === "html") {
73
+ // do parse html
74
+ const htmlAst =
75
+ stringType === "svg"
76
+ ? parseSvgString(string)
77
+ : parseHtml({
78
+ html: string,
79
+ storeOriginalPositions: false,
80
+ });
81
+ // for each attribute value
82
+ // and each text node content
83
+ visitHtmlNodes(htmlAst, {
84
+ "*": (node) => {
85
+ const htmlNodeText = getHtmlNodeText(node);
86
+ if (htmlNodeText) {
87
+ setHtmlNodeText(node, replaceThings(htmlNodeText));
88
+ }
89
+ const attributes = getHtmlNodeAttributes(node);
90
+ if (attributes) {
91
+ for (const name of Object.keys(attributes)) {
92
+ attributes[name] = replaceThings(attributes[name]);
93
+ }
94
+ setHtmlNodeAttributes(node, attributes);
95
+ }
96
+ },
97
+ });
98
+ return stringifyHtmlAst(htmlAst);
99
+ }
94
100
  return replaceThings(string);
95
101
  };
96
102