@jsenv/snapshot 2.2.0 → 2.2.2

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.0",
3
+ "version": "2.2.2",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,111 +1,97 @@
1
- import {
2
- moveEntrySync,
3
- writeDirectorySync,
4
- writeFileSync,
5
- } from "@jsenv/filesystem";
1
+ import { readEntryStatSync, readFileSync } from "@jsenv/filesystem";
6
2
  import { urlToRelativeUrl } from "@jsenv/urls/src/url_to_relative_url.js";
7
- import { takeDirectorySnapshot } from "./filesystem_snapshot.js";
3
+ import { takeFileSnapshot } from "./filesystem_snapshot.js";
8
4
  import { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
9
5
 
10
6
  export const snapshotFunctionSideEffects = (
11
7
  fn,
12
8
  fnFileUrl,
9
+ sideEffectFileRelativeUrl,
13
10
  {
14
- sideEffectDirectoryName = "output",
15
11
  rootDirectoryUrl = new URL("./", fnFileUrl),
16
12
  captureConsole = true,
17
- filesystemRedirects,
13
+ filesystemEffects,
18
14
  } = {},
19
15
  ) => {
20
- const sideEffectDirectoryUrl = new URL(sideEffectDirectoryName, fnFileUrl);
21
- writeDirectorySync(sideEffectDirectoryUrl, { allowUseless: true });
22
- const sideEffectDirectorySnapshot = takeDirectorySnapshot(
23
- sideEffectDirectoryUrl,
24
- );
16
+ const sideEffectFileUrl = new URL(sideEffectFileRelativeUrl, fnFileUrl);
17
+ const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
18
+ const sideEffects = [];
25
19
  const finallyCallbackSet = new Set();
26
- const errorFileUrl = new URL("./error.txt", sideEffectDirectoryUrl);
27
- const resultFileUrl = new URL("./result.json", sideEffectDirectoryUrl);
28
20
  const onError = (e) => {
29
- writeFileSync(
30
- errorFileUrl,
31
- replaceFluctuatingValues(e.stack, {
32
- fileUrl: errorFileUrl,
33
- }),
34
- );
21
+ sideEffects.push({
22
+ type: "throw",
23
+ value: e,
24
+ });
35
25
  };
36
26
  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
- );
27
+ sideEffects.push({
28
+ type: "return",
29
+ value: result,
30
+ });
47
31
  };
48
32
  const onFinally = () => {
49
33
  for (const finallyCallback of finallyCallbackSet) {
50
34
  finallyCallback();
51
35
  }
52
- sideEffectDirectorySnapshot.compare();
36
+ sideEffectFileSnapshot.update(
37
+ stringifySideEffects(sideEffects, { rootDirectoryUrl }),
38
+ );
39
+ sideEffectFileSnapshot.compare();
53
40
  };
54
41
  if (captureConsole) {
55
- const installConsoleSpy = (methodName, consoleOutputFileUrl) => {
42
+ const installConsoleSpy = (methodName) => {
56
43
  const methodSpied = console[methodName];
57
- let output = "";
58
44
  console[methodName] = (message) => {
59
- if (output) {
60
- output += "\n";
61
- }
62
- output += message;
45
+ sideEffects.push({
46
+ type: `console.${methodName}`,
47
+ value: message,
48
+ });
63
49
  };
64
50
  finallyCallbackSet.add(() => {
65
51
  console[methodName] = methodSpied;
66
- if (output) {
67
- writeFileSync(
68
- consoleOutputFileUrl,
69
- replaceFluctuatingValues(output, {
70
- fileUrl: consoleOutputFileUrl,
71
- rootDirectoryUrl,
72
- }),
73
- );
74
- }
75
52
  });
76
53
  };
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
- );
54
+ installConsoleSpy("error");
55
+ installConsoleSpy("warn");
56
+ installConsoleSpy("info");
57
+ installConsoleSpy("log");
93
58
  }
94
- if (filesystemRedirects) {
95
- const filesystemEffectDirectoryUrl = new URL(
96
- "./fs/",
97
- sideEffectDirectoryUrl,
98
- );
99
- for (const filesystemRedirect of filesystemRedirects) {
59
+ if (filesystemEffects) {
60
+ for (const filesystemEffect of filesystemEffects) {
61
+ const from = new URL(filesystemEffect, fnFileUrl);
62
+ const relativeUrl = urlToRelativeUrl(from, fnFileUrl);
63
+ const atStartState = getFileState(from);
64
+ const onFileSystemSideEffect = (fsSideEffect) => {
65
+ const last = sideEffects.pop();
66
+ sideEffects.push(fsSideEffect);
67
+ sideEffects.push(last);
68
+ };
100
69
  finallyCallbackSet.add(() => {
101
- const from = new URL(filesystemRedirect, fnFileUrl);
102
- const relativeUrl = urlToRelativeUrl(from, fnFileUrl);
103
- moveEntrySync({
104
- from,
105
- to: new URL(relativeUrl, filesystemEffectDirectoryUrl),
106
- noEntryEffect: "none",
107
- overwrite: true,
108
- });
70
+ const nowState = getFileState(from);
71
+ if (atStartState.found && !nowState.found) {
72
+ onFileSystemSideEffect({
73
+ type: `remove file "${relativeUrl}"`,
74
+ value: atStartState.content,
75
+ });
76
+ return;
77
+ }
78
+ // we use same type because we don't want to differentiate between
79
+ // - writing file for the 1st time
80
+ // - updating file content
81
+ // the important part is the file content in the end of the function execution
82
+ if (
83
+ (!atStartState.found && nowState.found) ||
84
+ atStartState.content !== nowState.content ||
85
+ atStartState.mtimeMs !== nowState.mtimeMs
86
+ ) {
87
+ onFileSystemSideEffect({
88
+ type: `write file "${relativeUrl}"`,
89
+ value: nowState.content,
90
+ });
91
+ return;
92
+ }
93
+ // file is exactly the same
94
+ // function did not have any effect on the file
109
95
  });
110
96
  }
111
97
  }
@@ -136,3 +122,56 @@ export const snapshotFunctionSideEffects = (
136
122
  onFinally();
137
123
  }
138
124
  };
125
+
126
+ const stringifySideEffects = (sideEffects, { rootDirectoryUrl }) => {
127
+ let string = "";
128
+ let index = 0;
129
+ for (const sideEffect of sideEffects) {
130
+ if (string) {
131
+ string += "\n\n";
132
+ }
133
+ string += `${index + 1}. ${sideEffect.type}`;
134
+ string += "\n";
135
+ let value = sideEffect.value;
136
+ if (sideEffect.type === "throw") {
137
+ value = replaceFluctuatingValues(value.stack, {
138
+ stringType: "error",
139
+ });
140
+ } else if (sideEffect.type === "return") {
141
+ value =
142
+ value === undefined
143
+ ? undefined
144
+ : replaceFluctuatingValues(JSON.stringify(value, null, " "), {
145
+ stringType: "json",
146
+ rootDirectoryUrl,
147
+ });
148
+ } else if (sideEffect.type.startsWith("console.")) {
149
+ value = replaceFluctuatingValues(value, {
150
+ stringType: "console",
151
+ rootDirectoryUrl,
152
+ });
153
+ }
154
+ string += value;
155
+ index++;
156
+ }
157
+ return string;
158
+ };
159
+
160
+ const getFileState = (fileUrl) => {
161
+ try {
162
+ const fileContent = readFileSync(fileUrl);
163
+ const { mtimeMs } = readEntryStatSync(fileUrl);
164
+ return {
165
+ found: true,
166
+ mtimeMs,
167
+ content: String(fileContent),
168
+ };
169
+ } catch (e) {
170
+ if (e.code === "ENOENT") {
171
+ return {
172
+ found: false,
173
+ };
174
+ }
175
+ throw e;
176
+ }
177
+ };
@@ -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