@jsenv/snapshot 1.1.1 → 1.2.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 +2 -2
- package/src/file_snapshots.js +307 -0
- package/src/main.js +1 -327
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/snapshot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Snapshot testing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"@jsenv/filesystem": "4.6.0",
|
|
39
39
|
"@jsenv/urls": "2.2.1",
|
|
40
40
|
"@jsenv/utils": "2.1.0",
|
|
41
|
-
"@jsenv/assert": "2.13.
|
|
41
|
+
"@jsenv/assert": "2.13.1"
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
assertAndNormalizeDirectoryUrl,
|
|
4
|
+
assertAndNormalizeFileUrl,
|
|
5
|
+
comparePathnames,
|
|
6
|
+
ensureEmptyDirectorySync,
|
|
7
|
+
removeFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
removeDirectorySync,
|
|
10
|
+
writeFileStructureSync,
|
|
11
|
+
} from "@jsenv/filesystem";
|
|
12
|
+
import { urlToFilename, urlToRelativeUrl } from "@jsenv/urls";
|
|
13
|
+
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createAssertionError,
|
|
17
|
+
formatStringAssertionErrorMessage,
|
|
18
|
+
} from "@jsenv/assert";
|
|
19
|
+
|
|
20
|
+
export const takeFileSnapshot = (fileUrl) => {
|
|
21
|
+
fileUrl = assertAndNormalizeFileUrl(fileUrl);
|
|
22
|
+
const expectedFileSnapshot = createFileSnapshot(fileUrl);
|
|
23
|
+
removeFileSync(fileUrl, { allowUseless: true });
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
compare: () => {
|
|
27
|
+
compareFileSnapshots(createFileSnapshot(fileUrl), expectedFileSnapshot);
|
|
28
|
+
},
|
|
29
|
+
writeContent: (content) => {
|
|
30
|
+
writeFileSync(fileUrl, content);
|
|
31
|
+
},
|
|
32
|
+
restore: () => {
|
|
33
|
+
if (expectedFileSnapshot.empty) {
|
|
34
|
+
removeFileSync(fileUrl, { allowUseless: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(fileUrl, expectedFileSnapshot.content);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const createFileSnapshot = (fileUrl) => {
|
|
42
|
+
const fileSnapshot = {
|
|
43
|
+
type: "file",
|
|
44
|
+
url: fileUrl,
|
|
45
|
+
stat: null,
|
|
46
|
+
content: "",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
fileSnapshot.stat = statSync(new URL(fileUrl));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e.code === "ENOENT") {
|
|
53
|
+
return fileSnapshot;
|
|
54
|
+
}
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
if (!fileSnapshot.stat.isFile()) {
|
|
58
|
+
throw new Error(`file expected at ${fileUrl}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isTextual = CONTENT_TYPE.isTextual(
|
|
62
|
+
CONTENT_TYPE.fromUrlExtension(fileUrl),
|
|
63
|
+
);
|
|
64
|
+
if (isTextual) {
|
|
65
|
+
const contentAsString = readFileSync(new URL(fileUrl), "utf8");
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
// ensure unix line breaks
|
|
68
|
+
fileSnapshot.content = contentAsString.replace(/\r\n/g, "\n");
|
|
69
|
+
} else {
|
|
70
|
+
fileSnapshot.content = contentAsString;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
const contentAsBuffer = readFileSync(new URL(fileUrl));
|
|
74
|
+
if (contentAsBuffer.length === 0) {
|
|
75
|
+
fileSnapshot.content = "";
|
|
76
|
+
} else {
|
|
77
|
+
fileSnapshot.content = contentAsBuffer;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return fileSnapshot;
|
|
81
|
+
};
|
|
82
|
+
const compareFileSnapshots = (actualFileSnapshot, expectedFileSnapshot) => {
|
|
83
|
+
const fileUrl = actualFileSnapshot.url;
|
|
84
|
+
const filename = urlToFilename(fileUrl);
|
|
85
|
+
const failureMessage = `snapshot comparison failed for "${filename}"`;
|
|
86
|
+
|
|
87
|
+
if (!actualFileSnapshot.stat) {
|
|
88
|
+
throw createAssertionError(`${failureMessage}
|
|
89
|
+
--- reason ---
|
|
90
|
+
file not found
|
|
91
|
+
--- file ---
|
|
92
|
+
${fileUrl}`);
|
|
93
|
+
}
|
|
94
|
+
if (!expectedFileSnapshot.stat) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const actualFileContent = actualFileSnapshot.content;
|
|
98
|
+
const expectedFileContent = expectedFileSnapshot.content;
|
|
99
|
+
if (Buffer.isBuffer(actualFileContent)) {
|
|
100
|
+
if (actualFileContent.equals(expectedFileContent)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw createAssertionError(`${failureMessage}
|
|
104
|
+
--- reason ---
|
|
105
|
+
content has changed
|
|
106
|
+
--- file ---
|
|
107
|
+
${fileUrl}`);
|
|
108
|
+
}
|
|
109
|
+
if (actualFileContent === expectedFileContent) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const message = formatStringAssertionErrorMessage({
|
|
113
|
+
actual: actualFileContent,
|
|
114
|
+
expected: expectedFileContent,
|
|
115
|
+
name: `file content`,
|
|
116
|
+
});
|
|
117
|
+
throw createAssertionError(`${failureMessage}
|
|
118
|
+
--- reason ---
|
|
119
|
+
${message}
|
|
120
|
+
--- file ---
|
|
121
|
+
${fileUrl}`);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const takeDirectorySnapshot = (directoryUrl) => {
|
|
125
|
+
directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
|
|
126
|
+
directoryUrl = new URL(directoryUrl);
|
|
127
|
+
|
|
128
|
+
const expectedDirectorySnapshot = createDirectorySnapshot(directoryUrl);
|
|
129
|
+
ensureEmptyDirectorySync(directoryUrl);
|
|
130
|
+
return {
|
|
131
|
+
compare: () => {
|
|
132
|
+
const dirname = `${urlToFilename(directoryUrl)}/`;
|
|
133
|
+
const failureMessage = `snapshot comparison failed for "${dirname}"`;
|
|
134
|
+
const actualDirectorySnapshot = createDirectorySnapshot(directoryUrl);
|
|
135
|
+
if (!expectedDirectorySnapshot.stat || expectedDirectorySnapshot.empty) {
|
|
136
|
+
// the snapshot taken for directory/file/whatever is empty:
|
|
137
|
+
// - first time code executes:
|
|
138
|
+
// it defines snapshot that will be used for comparison by future runs
|
|
139
|
+
// - snapshot have been cleaned:
|
|
140
|
+
// we want to re-generated all snapshots without failing tests
|
|
141
|
+
// (happens when we know beforehand snapshot will change and we just want
|
|
142
|
+
// to review them using git diff)
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const actualFileSnapshots = actualDirectorySnapshot.fileSnapshots;
|
|
147
|
+
const expectedFileSnapshots = expectedDirectorySnapshot.fileSnapshots;
|
|
148
|
+
const actualRelativeUrls = Object.keys(actualFileSnapshots);
|
|
149
|
+
const expectedRelativeUrls = Object.keys(expectedFileSnapshots);
|
|
150
|
+
|
|
151
|
+
// missing_files
|
|
152
|
+
{
|
|
153
|
+
const missingRelativeUrls = expectedRelativeUrls.filter(
|
|
154
|
+
(expectedRelativeUrl) =>
|
|
155
|
+
!actualRelativeUrls.includes(expectedRelativeUrl),
|
|
156
|
+
);
|
|
157
|
+
const missingFileCount = missingRelativeUrls.length;
|
|
158
|
+
if (missingFileCount > 0) {
|
|
159
|
+
const missingUrls = missingRelativeUrls.map(
|
|
160
|
+
(relativeUrl) => new URL(relativeUrl, directoryUrl).href,
|
|
161
|
+
);
|
|
162
|
+
if (missingFileCount === 1) {
|
|
163
|
+
throw createAssertionError(`${failureMessage}
|
|
164
|
+
--- reason ---
|
|
165
|
+
"${missingRelativeUrls[0]}" is missing
|
|
166
|
+
--- file missing ---
|
|
167
|
+
${missingUrls[0]}`);
|
|
168
|
+
}
|
|
169
|
+
throw createAssertionError(`${failureMessage}
|
|
170
|
+
--- reason ---
|
|
171
|
+
${missingFileCount} files are missing
|
|
172
|
+
--- files missing ---
|
|
173
|
+
${missingUrls.join("\n")}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// unexpected files
|
|
178
|
+
{
|
|
179
|
+
const extraRelativeUrls = actualRelativeUrls.filter(
|
|
180
|
+
(actualRelativeUrl) =>
|
|
181
|
+
!expectedRelativeUrls.includes(actualRelativeUrl),
|
|
182
|
+
);
|
|
183
|
+
const extraFileCount = extraRelativeUrls.length;
|
|
184
|
+
if (extraFileCount > 0) {
|
|
185
|
+
const extraUrls = extraRelativeUrls.map(
|
|
186
|
+
(relativeUrl) => new URL(relativeUrl, directoryUrl).href,
|
|
187
|
+
);
|
|
188
|
+
if (extraFileCount === 1) {
|
|
189
|
+
throw createAssertionError(`${failureMessage}
|
|
190
|
+
--- reason ---
|
|
191
|
+
"${extraRelativeUrls[0]}" is unexpected
|
|
192
|
+
--- file unexpected ---
|
|
193
|
+
${extraUrls[0]}`);
|
|
194
|
+
}
|
|
195
|
+
throw createAssertionError(`${failureMessage}
|
|
196
|
+
--- reason ---
|
|
197
|
+
${extraFileCount} files are unexpected
|
|
198
|
+
--- files unexpected ---
|
|
199
|
+
${extraUrls.join("\n")}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// file contents
|
|
204
|
+
{
|
|
205
|
+
for (const relativeUrl of actualRelativeUrls) {
|
|
206
|
+
const actualFileSnapshot = actualFileSnapshots[relativeUrl];
|
|
207
|
+
const expectedFileSnapshot = expectedFileSnapshots[relativeUrl];
|
|
208
|
+
compareFileSnapshots(actualFileSnapshot, expectedFileSnapshot);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
addFile: (relativeUrl, content) => {
|
|
213
|
+
writeFileSync(new URL(relativeUrl, directoryUrl), content);
|
|
214
|
+
},
|
|
215
|
+
restore: () => {
|
|
216
|
+
if (expectedDirectorySnapshot.notFound) {
|
|
217
|
+
removeDirectorySync(directoryUrl, {
|
|
218
|
+
recursive: true,
|
|
219
|
+
allowUseless: true,
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (expectedDirectorySnapshot.empty) {
|
|
224
|
+
ensureEmptyDirectorySync(directoryUrl);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const fileStructure = {};
|
|
229
|
+
Object.keys(expectedDirectorySnapshot.fileSnapshots).forEach(
|
|
230
|
+
(relativeUrl) => {
|
|
231
|
+
const fileSnapshot =
|
|
232
|
+
expectedDirectorySnapshot.fileSnapshots[relativeUrl];
|
|
233
|
+
if (!fileSnapshot.empty) {
|
|
234
|
+
fileStructure[relativeUrl] = fileSnapshot.content;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
writeFileStructureSync(
|
|
239
|
+
directoryUrl,
|
|
240
|
+
expectedDirectorySnapshot.fileStructure,
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
const createDirectorySnapshot = (directoryUrl) => {
|
|
246
|
+
const directorySnapshot = {
|
|
247
|
+
type: "directory",
|
|
248
|
+
url: directoryUrl.href,
|
|
249
|
+
stat: null,
|
|
250
|
+
empty: false,
|
|
251
|
+
fileSnapshots: {},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
directorySnapshot.stat = statSync(directoryUrl);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
if (e.code === "ENOENT") {
|
|
258
|
+
return directorySnapshot;
|
|
259
|
+
}
|
|
260
|
+
if (e.code === "ENOTDIR") {
|
|
261
|
+
// trailing slash is forced on directoryUrl
|
|
262
|
+
// as a result Node.js throw ENOTDIR when doing "stat" operation
|
|
263
|
+
throw new Error(`directory expected at ${directoryUrl}`);
|
|
264
|
+
}
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
if (!directorySnapshot.stat.isDirectory()) {
|
|
268
|
+
throw new Error(`directory expected at ${directoryUrl}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const entryNames = readdirSync(directoryUrl);
|
|
272
|
+
if (entryNames.length === 0) {
|
|
273
|
+
directorySnapshot.empty = true;
|
|
274
|
+
return directorySnapshot;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const fileSnapshotsNaturalOrder = {};
|
|
278
|
+
const visitDirectory = (url) => {
|
|
279
|
+
try {
|
|
280
|
+
const directoryContent = readdirSync(url);
|
|
281
|
+
for (const filename of directoryContent) {
|
|
282
|
+
const contentUrl = new URL(filename, url);
|
|
283
|
+
const stat = statSync(contentUrl);
|
|
284
|
+
if (stat.isDirectory()) {
|
|
285
|
+
visitDirectory(new URL(`${contentUrl}/`));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const relativeUrl = urlToRelativeUrl(contentUrl, directoryUrl);
|
|
289
|
+
fileSnapshotsNaturalOrder[relativeUrl] = createFileSnapshot(contentUrl);
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
if (e && e.code === "ENOENT") {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
visitDirectory(directoryUrl);
|
|
299
|
+
|
|
300
|
+
const relativeUrls = Object.keys(fileSnapshotsNaturalOrder);
|
|
301
|
+
relativeUrls.sort(comparePathnames);
|
|
302
|
+
relativeUrls.forEach((relativeUrl) => {
|
|
303
|
+
directorySnapshot.fileSnapshots[relativeUrl] =
|
|
304
|
+
fileSnapshotsNaturalOrder[relativeUrl];
|
|
305
|
+
});
|
|
306
|
+
return directorySnapshot;
|
|
307
|
+
};
|
package/src/main.js
CHANGED
|
@@ -1,327 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
assertAndNormalizeDirectoryUrl,
|
|
4
|
-
assertAndNormalizeFileUrl,
|
|
5
|
-
comparePathnames,
|
|
6
|
-
ensureEmptyDirectorySync,
|
|
7
|
-
removeFileSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
removeDirectorySync,
|
|
10
|
-
writeFileStructureSync,
|
|
11
|
-
} from "@jsenv/filesystem";
|
|
12
|
-
import { urlToFilename, urlToRelativeUrl } from "@jsenv/urls";
|
|
13
|
-
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
createAssertionError,
|
|
17
|
-
formatStringAssertionErrorMessage,
|
|
18
|
-
} from "@jsenv/assert";
|
|
19
|
-
|
|
20
|
-
const snapshotSymbol = Symbol.for("snapshot");
|
|
21
|
-
|
|
22
|
-
export const takeDirectorySnapshot = (directoryUrl) => {
|
|
23
|
-
directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
|
|
24
|
-
directoryUrl = new URL(directoryUrl);
|
|
25
|
-
|
|
26
|
-
const expectedDirectorySnapshot = createDirectorySnapshot(directoryUrl);
|
|
27
|
-
ensureEmptyDirectorySync(directoryUrl);
|
|
28
|
-
return {
|
|
29
|
-
addFile: (relativeUrl, content) => {
|
|
30
|
-
writeFileSync(new URL(relativeUrl, directoryUrl), content);
|
|
31
|
-
},
|
|
32
|
-
compare: () => {
|
|
33
|
-
const actualDirectorySnapshot = createDirectorySnapshot(directoryUrl);
|
|
34
|
-
compareSnapshots(actualDirectorySnapshot, expectedDirectorySnapshot);
|
|
35
|
-
},
|
|
36
|
-
restore: () => {
|
|
37
|
-
if (expectedDirectorySnapshot.notFound) {
|
|
38
|
-
removeDirectorySync(directoryUrl, {
|
|
39
|
-
recursive: true,
|
|
40
|
-
allowUseless: true,
|
|
41
|
-
});
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (expectedDirectorySnapshot.empty) {
|
|
45
|
-
ensureEmptyDirectorySync(directoryUrl);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const fileStructure = {};
|
|
50
|
-
Object.keys(expectedDirectorySnapshot.fileSnapshots).forEach(
|
|
51
|
-
(relativeUrl) => {
|
|
52
|
-
const fileSnapshot =
|
|
53
|
-
expectedDirectorySnapshot.fileSnapshots[relativeUrl];
|
|
54
|
-
if (!fileSnapshot.empty) {
|
|
55
|
-
fileStructure[relativeUrl] = fileSnapshot.content;
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
writeFileStructureSync(
|
|
60
|
-
directoryUrl,
|
|
61
|
-
expectedDirectorySnapshot.fileStructure,
|
|
62
|
-
);
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
const createDirectorySnapshot = (directoryUrl) => {
|
|
67
|
-
const directorySnapshot = {
|
|
68
|
-
[snapshotSymbol]: true,
|
|
69
|
-
empty: false,
|
|
70
|
-
type: "directory",
|
|
71
|
-
url: directoryUrl.href,
|
|
72
|
-
notFound: false,
|
|
73
|
-
fileSnapshots: {},
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
let stat;
|
|
77
|
-
try {
|
|
78
|
-
stat = statSync(directoryUrl);
|
|
79
|
-
if (!stat.isDirectory()) {
|
|
80
|
-
throw new Error(`directory expected at ${directoryUrl}`);
|
|
81
|
-
}
|
|
82
|
-
const entryNames = readdirSync(directoryUrl);
|
|
83
|
-
if (entryNames.length === 0) {
|
|
84
|
-
directorySnapshot.empty = true;
|
|
85
|
-
return directorySnapshot;
|
|
86
|
-
}
|
|
87
|
-
} catch (e) {
|
|
88
|
-
if (e.code === "ENOENT") {
|
|
89
|
-
directorySnapshot.empty = true;
|
|
90
|
-
directorySnapshot.notFound = true;
|
|
91
|
-
return directorySnapshot;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const fileSnapshotsNaturalOrder = {};
|
|
96
|
-
const visitDirectory = (url) => {
|
|
97
|
-
try {
|
|
98
|
-
const directoryContent = readdirSync(url);
|
|
99
|
-
directoryContent.forEach((filename) => {
|
|
100
|
-
const contentUrl = new URL(filename, url);
|
|
101
|
-
const stat = statSync(contentUrl);
|
|
102
|
-
if (stat.isDirectory()) {
|
|
103
|
-
visitDirectory(new URL(`${contentUrl}/`));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const relativeUrl = urlToRelativeUrl(contentUrl, directoryUrl);
|
|
107
|
-
fileSnapshotsNaturalOrder[relativeUrl] = createFileSnapshot(contentUrl);
|
|
108
|
-
});
|
|
109
|
-
} catch (e) {
|
|
110
|
-
if (e && e.code === "ENOENT") {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
throw e;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
visitDirectory(directoryUrl);
|
|
117
|
-
|
|
118
|
-
const relativeUrls = Object.keys(fileSnapshotsNaturalOrder);
|
|
119
|
-
relativeUrls.sort(comparePathnames);
|
|
120
|
-
relativeUrls.forEach((relativeUrl) => {
|
|
121
|
-
directorySnapshot.fileSnapshots[relativeUrl] =
|
|
122
|
-
fileSnapshotsNaturalOrder[relativeUrl];
|
|
123
|
-
});
|
|
124
|
-
return directorySnapshot;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export const takeFileSnapshot = (fileUrl) => {
|
|
128
|
-
fileUrl = assertAndNormalizeFileUrl(fileUrl);
|
|
129
|
-
const expectedFileSnapshot = createFileSnapshot(fileUrl);
|
|
130
|
-
removeFileSync(fileUrl);
|
|
131
|
-
return {
|
|
132
|
-
writeContent: (content) => {
|
|
133
|
-
writeFileSync(fileUrl, content);
|
|
134
|
-
},
|
|
135
|
-
compare: () => {
|
|
136
|
-
const actualFileSnapshot = createFileSnapshot(fileUrl);
|
|
137
|
-
compareSnapshots(actualFileSnapshot, expectedFileSnapshot);
|
|
138
|
-
},
|
|
139
|
-
restore: () => {
|
|
140
|
-
if (expectedFileSnapshot.empty) {
|
|
141
|
-
removeFileSync(fileUrl, { allowUseless: true });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
writeFileSync(fileUrl, expectedFileSnapshot.content);
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
const createFileSnapshot = (fileUrl) => {
|
|
149
|
-
const fileSnapshot = {
|
|
150
|
-
[snapshotSymbol]: true,
|
|
151
|
-
empty: false,
|
|
152
|
-
type: "file",
|
|
153
|
-
url: fileUrl,
|
|
154
|
-
content: "",
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
let stat;
|
|
158
|
-
try {
|
|
159
|
-
stat = statSync(new URL(fileUrl));
|
|
160
|
-
} catch (e) {
|
|
161
|
-
if (e.code === "ENOENT") {
|
|
162
|
-
fileSnapshot.empty = true;
|
|
163
|
-
return fileSnapshot;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (!stat.isFile()) {
|
|
167
|
-
throw new Error(`file expected at ${fileUrl}`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const isTextual = CONTENT_TYPE.isTextual(
|
|
171
|
-
CONTENT_TYPE.fromUrlExtension(fileUrl),
|
|
172
|
-
);
|
|
173
|
-
if (isTextual) {
|
|
174
|
-
const contentAsString = readFileSync(new URL(fileUrl), "utf8");
|
|
175
|
-
if (process.platform === "win32") {
|
|
176
|
-
// ensure unix line breaks
|
|
177
|
-
fileSnapshot.content = contentAsString.replace(/\r\n/g, "\n");
|
|
178
|
-
} else {
|
|
179
|
-
fileSnapshot.content = contentAsString;
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
const contentAsBuffer = readFileSync(new URL(fileUrl));
|
|
183
|
-
if (contentAsBuffer.length === 0) {
|
|
184
|
-
fileSnapshot.content = "";
|
|
185
|
-
} else {
|
|
186
|
-
fileSnapshot.content = contentAsBuffer;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return fileSnapshot;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const compareSnapshots = (currentSnapshot, previousSnapshot) => {
|
|
193
|
-
if (!currentSnapshot || !currentSnapshot[snapshotSymbol]) {
|
|
194
|
-
throw new TypeError(
|
|
195
|
-
`1st argument must be a snapshot, received ${currentSnapshot}`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
if (!previousSnapshot || !previousSnapshot[snapshotSymbol]) {
|
|
199
|
-
throw new TypeError(
|
|
200
|
-
`2nd argument must be a snapshot, received ${previousSnapshot}`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const currentShapsnotType = currentSnapshot.type;
|
|
205
|
-
const previousSnapshotType = previousSnapshot.type;
|
|
206
|
-
if (currentShapsnotType !== previousSnapshotType) {
|
|
207
|
-
throw new TypeError(
|
|
208
|
-
`cannot compare snapshots of different types "${currentShapsnotType}" vs "${previousSnapshotType}"`,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
const comparer = snapshotComparers[currentShapsnotType];
|
|
212
|
-
if (!comparer) {
|
|
213
|
-
throw new TypeError(`Unknow snapshot type "${currentShapsnotType}"`);
|
|
214
|
-
}
|
|
215
|
-
if (previousSnapshot.empty) {
|
|
216
|
-
// the snapshot taken for directory/file/whatever is empty:
|
|
217
|
-
// - first time code executes:
|
|
218
|
-
// it defines snapshot that will be used for comparison by future runs
|
|
219
|
-
// - snapshot have been cleaned:
|
|
220
|
-
// we want to re-generated all snapshots without failing tests
|
|
221
|
-
// (happens when we know beforehand snapshot will change and we just want
|
|
222
|
-
// to review them using git diff)
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
comparer(currentSnapshot, previousSnapshot);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const snapshotComparers = {
|
|
229
|
-
directory: (currentDirectorySnapshot, previousDirectorySnapshot) => {
|
|
230
|
-
const failureMessage = `comparison with previous directory snapshot failed`;
|
|
231
|
-
const currentFileSnapshots = currentDirectorySnapshot.fileSnapshots;
|
|
232
|
-
const previousFileSnapshots = previousDirectorySnapshot.fileSnapshots;
|
|
233
|
-
const currentRelativeUrls = Object.keys(currentFileSnapshots);
|
|
234
|
-
const previousRelativeUrls = Object.keys(previousFileSnapshots);
|
|
235
|
-
|
|
236
|
-
// missing_files
|
|
237
|
-
{
|
|
238
|
-
const missingRelativeUrls = previousRelativeUrls.filter(
|
|
239
|
-
(previousRelativeUrl) =>
|
|
240
|
-
!currentRelativeUrls.includes(previousRelativeUrl),
|
|
241
|
-
);
|
|
242
|
-
const missingFileCount = missingRelativeUrls.length;
|
|
243
|
-
if (missingFileCount > 0) {
|
|
244
|
-
const missingUrls = missingRelativeUrls.map(
|
|
245
|
-
(relativeUrl) =>
|
|
246
|
-
new URL(relativeUrl, currentDirectorySnapshot.url).href,
|
|
247
|
-
);
|
|
248
|
-
if (missingFileCount === 1) {
|
|
249
|
-
throw createAssertionError(`${failureMessage}
|
|
250
|
-
--- reason ---
|
|
251
|
-
"${missingRelativeUrls[0]}" is missing
|
|
252
|
-
--- file missing ---
|
|
253
|
-
${missingUrls[0]}`);
|
|
254
|
-
}
|
|
255
|
-
throw createAssertionError(`${failureMessage}
|
|
256
|
-
--- reason ---
|
|
257
|
-
${missingFileCount} files are missing
|
|
258
|
-
--- files missing ---
|
|
259
|
-
${missingUrls.join("\n")}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// unexpected files
|
|
264
|
-
{
|
|
265
|
-
const extraRelativeUrls = currentRelativeUrls.filter(
|
|
266
|
-
(currentRelativeUrl) =>
|
|
267
|
-
!previousRelativeUrls.includes(currentRelativeUrl),
|
|
268
|
-
);
|
|
269
|
-
const extraFileCount = extraRelativeUrls.length;
|
|
270
|
-
if (extraFileCount > 0) {
|
|
271
|
-
const extraUrls = extraRelativeUrls.map(
|
|
272
|
-
(relativeUrl) =>
|
|
273
|
-
new URL(relativeUrl, currentDirectorySnapshot.url).href,
|
|
274
|
-
);
|
|
275
|
-
if (extraFileCount === 1) {
|
|
276
|
-
throw createAssertionError(`${failureMessage}
|
|
277
|
-
--- reason ---
|
|
278
|
-
"${extraRelativeUrls[0]}" is unexpected
|
|
279
|
-
--- file unexpected ---
|
|
280
|
-
${extraUrls[0]}`);
|
|
281
|
-
}
|
|
282
|
-
throw createAssertionError(`${failureMessage}
|
|
283
|
-
--- reason ---
|
|
284
|
-
${extraFileCount} files are unexpected
|
|
285
|
-
--- files unexpected ---
|
|
286
|
-
${extraUrls.join("\n")}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// file contents
|
|
291
|
-
{
|
|
292
|
-
for (const relativeUrl of currentRelativeUrls) {
|
|
293
|
-
const currentFileSnapshot = currentFileSnapshots[relativeUrl];
|
|
294
|
-
const previousFileSnapshot = previousFileSnapshots[relativeUrl];
|
|
295
|
-
compareSnapshots(currentFileSnapshot, previousFileSnapshot);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
file: (currentFileSnapshot, previousFileSnapshot) => {
|
|
300
|
-
const failureMessage = `comparison with previous file snapshot failed`;
|
|
301
|
-
const currentFileContent = currentFileSnapshot.content;
|
|
302
|
-
const previousFileContent = previousFileSnapshot.content;
|
|
303
|
-
if (Buffer.isBuffer(currentFileContent)) {
|
|
304
|
-
if (currentFileContent.equals(previousFileContent)) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
throw createAssertionError(`${failureMessage}
|
|
308
|
-
--- reason ---
|
|
309
|
-
"${urlToFilename(currentFileSnapshot.url)}" content has changed
|
|
310
|
-
--- file ---
|
|
311
|
-
${currentFileSnapshot.url}`);
|
|
312
|
-
}
|
|
313
|
-
if (currentFileContent === previousFileContent) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
const message = formatStringAssertionErrorMessage({
|
|
317
|
-
actual: currentFileContent,
|
|
318
|
-
expected: previousFileContent,
|
|
319
|
-
name: `"${urlToFilename(currentFileSnapshot.url)}" content`,
|
|
320
|
-
});
|
|
321
|
-
throw createAssertionError(`${failureMessage}
|
|
322
|
-
--- reason ---
|
|
323
|
-
${message}
|
|
324
|
-
--- file ---
|
|
325
|
-
${currentFileSnapshot.url}`);
|
|
326
|
-
},
|
|
327
|
-
};
|
|
1
|
+
export { takeDirectorySnapshot, takeFileSnapshot } from "./file_snapshots.js";
|