@cronn/lib-file-snapshots 0.19.0 → 0.19.1
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/dist/index.d.mts +163 -0
- package/dist/index.mjs +266 -0
- package/package.json +10 -10
- package/dist/index.d.ts +0 -155
- package/dist/index.js +0 -365
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
//#region src/types/serializer.d.ts
|
|
2
|
+
interface SnapshotSerializer {
|
|
3
|
+
/**
|
|
4
|
+
* The file extension associated with the serialized value
|
|
5
|
+
*/
|
|
6
|
+
readonly fileExtension: string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when value can be serialized
|
|
9
|
+
*
|
|
10
|
+
* @param value The value to be serialized
|
|
11
|
+
*/
|
|
12
|
+
canSerialize(value: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Serializes value
|
|
15
|
+
*
|
|
16
|
+
* @param value The value to be serialized
|
|
17
|
+
* @return {string} The serialized value
|
|
18
|
+
* @throws {Error} Will throw an error if value cannot be serialized.
|
|
19
|
+
*/
|
|
20
|
+
serialize(value: unknown): string;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/serializers/json-serializer.d.ts
|
|
24
|
+
interface JsonSerializerOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Serializes `undefined` properties in objects. By default, they are omitted.
|
|
27
|
+
*
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
includeUndefinedObjectProperties?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Custom normalizers to apply before serialization
|
|
33
|
+
*/
|
|
34
|
+
normalizers?: Array<JsonNormalizer>;
|
|
35
|
+
/**
|
|
36
|
+
* Indentation size in spaces
|
|
37
|
+
*
|
|
38
|
+
* @default 2
|
|
39
|
+
*/
|
|
40
|
+
indentSize?: number;
|
|
41
|
+
}
|
|
42
|
+
type JsonNormalizer = (value: unknown, context: JsonNormalizerContext) => unknown;
|
|
43
|
+
interface JsonNormalizerContext {
|
|
44
|
+
key?: string;
|
|
45
|
+
index?: number;
|
|
46
|
+
}
|
|
47
|
+
declare class JsonSerializer implements SnapshotSerializer {
|
|
48
|
+
readonly fileExtension = "json";
|
|
49
|
+
private readonly includeUndefinedObjectProperties;
|
|
50
|
+
private readonly normalizers;
|
|
51
|
+
private readonly indentSize;
|
|
52
|
+
constructor(options?: JsonSerializerOptions);
|
|
53
|
+
canSerialize(_value: unknown): boolean;
|
|
54
|
+
serialize(value: unknown): string;
|
|
55
|
+
private normalizeValueRecursive;
|
|
56
|
+
private normalizeNumber;
|
|
57
|
+
private normalizeArray;
|
|
58
|
+
private normalizeObject;
|
|
59
|
+
private normalizeDate;
|
|
60
|
+
private normalizePlainObject;
|
|
61
|
+
private normalizeMap;
|
|
62
|
+
private serializedValue;
|
|
63
|
+
private assertKeyType;
|
|
64
|
+
private applyCustomNormalizers;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/serializers/text-serializer.d.ts
|
|
68
|
+
interface TextSerializerOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Custom normalizers to apply before serialization
|
|
71
|
+
*/
|
|
72
|
+
normalizers?: Array<TextNormalizer>;
|
|
73
|
+
/**
|
|
74
|
+
* File extension used for storing the text file
|
|
75
|
+
*
|
|
76
|
+
* @default "txt"
|
|
77
|
+
*/
|
|
78
|
+
fileExtension?: string;
|
|
79
|
+
}
|
|
80
|
+
type TextNormalizer = (value: string) => string;
|
|
81
|
+
declare class TextSerializer implements SnapshotSerializer {
|
|
82
|
+
readonly fileExtension: string;
|
|
83
|
+
private readonly normalizers;
|
|
84
|
+
constructor(options?: TextSerializerOptions);
|
|
85
|
+
canSerialize(value: unknown): value is string;
|
|
86
|
+
serialize(value: unknown): string;
|
|
87
|
+
private normalizeValue;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/types/matcher.d.ts
|
|
91
|
+
interface ValidationFileMatcherConfig {
|
|
92
|
+
/**
|
|
93
|
+
* Directory in which golden masters are stored
|
|
94
|
+
*/
|
|
95
|
+
validationDir: string;
|
|
96
|
+
/**
|
|
97
|
+
* Directory in which file snapshots from test runs are stored
|
|
98
|
+
*/
|
|
99
|
+
outputDir: string;
|
|
100
|
+
/**
|
|
101
|
+
* The full path to the snapshot file
|
|
102
|
+
*
|
|
103
|
+
* @example "src/tests/feature/test/when_x_then_y"
|
|
104
|
+
*/
|
|
105
|
+
filePath: string;
|
|
106
|
+
/**
|
|
107
|
+
* The serializer to use for the snapshot
|
|
108
|
+
*/
|
|
109
|
+
serializer: SnapshotSerializer;
|
|
110
|
+
/**
|
|
111
|
+
* Whether to update golden masters with the actual result.
|
|
112
|
+
*
|
|
113
|
+
* @default "missing"
|
|
114
|
+
*/
|
|
115
|
+
updateSnapshots?: UpdateSnapshotsType;
|
|
116
|
+
}
|
|
117
|
+
type UpdateSnapshotsType = "all" | "missing" | "none";
|
|
118
|
+
interface ValidationFileMatcherResult {
|
|
119
|
+
actual: string;
|
|
120
|
+
expected: string;
|
|
121
|
+
outputFilePath: string;
|
|
122
|
+
validationFilePath: string;
|
|
123
|
+
message: () => string;
|
|
124
|
+
writeFileSnapshots: () => void;
|
|
125
|
+
}
|
|
126
|
+
type FilePathResolver = (params: FilePathResolverParams) => string;
|
|
127
|
+
interface FilePathResolverParams {
|
|
128
|
+
testPath: string;
|
|
129
|
+
titlePath: Array<string>;
|
|
130
|
+
name?: string;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/matcher/validation-file-matcher.d.ts
|
|
134
|
+
declare class ValidationFileMatcher {
|
|
135
|
+
private readonly updateSnapshots;
|
|
136
|
+
private readonly serializer;
|
|
137
|
+
private readonly filePaths;
|
|
138
|
+
private validationFile;
|
|
139
|
+
constructor(config: ValidationFileMatcherConfig);
|
|
140
|
+
get isValidationFileMissing(): boolean;
|
|
141
|
+
get isUpdate(): boolean;
|
|
142
|
+
matchFileSnapshot(actual: unknown): ValidationFileMatcherResult;
|
|
143
|
+
private buildFilePaths;
|
|
144
|
+
private readValidationFile;
|
|
145
|
+
private createMatcherResult;
|
|
146
|
+
private writeFileSnapshots;
|
|
147
|
+
private resolveExpected;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/matcher/file-path-resolver.d.ts
|
|
151
|
+
declare function resolveNameAsFile(params: FilePathResolverParams): string;
|
|
152
|
+
declare function resolveNameAsFileSuffix(params: FilePathResolverParams): string;
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/utils/file.d.ts
|
|
155
|
+
declare function normalizeFileName(testName: string): string;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/utils/guards.d.ts
|
|
158
|
+
declare function isString(value: unknown): value is string;
|
|
159
|
+
declare function isArray(value: unknown): value is Array<unknown>;
|
|
160
|
+
type PlainObject = Record<PropertyKey, unknown>;
|
|
161
|
+
declare function isPlainObject(value: unknown): value is PlainObject;
|
|
162
|
+
//#endregion
|
|
163
|
+
export { type FilePathResolver, type FilePathResolverParams, type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotSerializer, type TextNormalizer, TextSerializer, type UpdateSnapshotsType, ValidationFileMatcher, type ValidationFileMatcherResult, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import * as fs$1 from "node:fs";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import * as path$1 from "node:path";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/utils/file.ts
|
|
8
|
+
const NEW_LINE_SEPARATOR = "\n";
|
|
9
|
+
const EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
|
|
10
|
+
function normalizeFileName(testName) {
|
|
11
|
+
return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
|
|
12
|
+
}
|
|
13
|
+
function readSnapshotFile(path) {
|
|
14
|
+
const contents = fs.readFileSync(path, { encoding: "utf8" });
|
|
15
|
+
if (os.EOL === NEW_LINE_SEPARATOR) return contents;
|
|
16
|
+
return contents.replace(new RegExp(os.EOL, "g"), NEW_LINE_SEPARATOR);
|
|
17
|
+
}
|
|
18
|
+
function writeSnapshotFile(file, data) {
|
|
19
|
+
mkdirRecursive(path.dirname(file));
|
|
20
|
+
fs.writeFileSync(file, data, { encoding: "utf8" });
|
|
21
|
+
}
|
|
22
|
+
function mkdirRecursive(path) {
|
|
23
|
+
fs.mkdirSync(path, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
function addTrailingNewLine(data) {
|
|
26
|
+
return `${data}${NEW_LINE_SEPARATOR}`;
|
|
27
|
+
}
|
|
28
|
+
function addMissingFileMarker(data) {
|
|
29
|
+
return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils/guards.ts
|
|
34
|
+
function isString(value) {
|
|
35
|
+
return typeof value === "string";
|
|
36
|
+
}
|
|
37
|
+
function isArray(value) {
|
|
38
|
+
return Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function isObject(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function isPlainObject(value) {
|
|
44
|
+
if (!isObject(value)) return false;
|
|
45
|
+
const proto = Object.getPrototypeOf(value);
|
|
46
|
+
return proto === null || proto === Object.prototype;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/utils/validators.ts
|
|
51
|
+
function isPositiveInteger(value) {
|
|
52
|
+
return Number.isInteger(value) && value > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/serializers/json-serializer.ts
|
|
57
|
+
var JsonSerializer = class {
|
|
58
|
+
fileExtension = "json";
|
|
59
|
+
includeUndefinedObjectProperties;
|
|
60
|
+
normalizers;
|
|
61
|
+
indentSize;
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
if (options.indentSize !== void 0 && !isPositiveInteger(options.indentSize)) throw new Error("Invalid option indentSize: value must be a positive integer.");
|
|
64
|
+
this.includeUndefinedObjectProperties = options.includeUndefinedObjectProperties ?? false;
|
|
65
|
+
this.normalizers = options.normalizers ?? [];
|
|
66
|
+
this.indentSize = options.indentSize ?? 2;
|
|
67
|
+
}
|
|
68
|
+
canSerialize(_value) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
serialize(value) {
|
|
72
|
+
const jsonValue = this.normalizeValueRecursive(value);
|
|
73
|
+
return addTrailingNewLine(JSON.stringify(jsonValue, void 0, this.indentSize));
|
|
74
|
+
}
|
|
75
|
+
normalizeValueRecursive(value, context) {
|
|
76
|
+
const customValue = this.applyCustomNormalizers(value, context);
|
|
77
|
+
if (customValue === void 0) return this.serializedValue("undefined");
|
|
78
|
+
if (customValue === null || isString(customValue) || typeof customValue === "boolean") return context === void 0 ? this.serializedValue(typeof customValue, { value: customValue }) : customValue;
|
|
79
|
+
if (typeof customValue === "number") return this.normalizeNumber(customValue);
|
|
80
|
+
if (typeof customValue === "bigint") return this.serializedValue("bigint", { value: customValue.toString() });
|
|
81
|
+
if (typeof customValue === "symbol") return this.serializedValue("symbol", { description: customValue.description });
|
|
82
|
+
if (typeof customValue === "function") return this.serializedValue("function", { name: customValue.name });
|
|
83
|
+
if (typeof customValue === "object") return this.normalizeObject(customValue);
|
|
84
|
+
throw new Error(`Missing JSON normalization for value of type ${typeof customValue}.`);
|
|
85
|
+
}
|
|
86
|
+
normalizeNumber(value) {
|
|
87
|
+
if (Number.isNaN(value)) return this.serializedValue("number", { value: "Number.NaN" });
|
|
88
|
+
switch (value) {
|
|
89
|
+
case Number.MIN_VALUE: return this.serializedValue("number", { value: "Number.MIN_VALUE" });
|
|
90
|
+
case Number.MAX_VALUE: return this.serializedValue("number", { value: "Number.MAX_VALUE" });
|
|
91
|
+
case Number.MIN_SAFE_INTEGER: return this.serializedValue("number", { value: "Number.MIN_SAFE_INTEGER" });
|
|
92
|
+
case Number.MAX_SAFE_INTEGER: return this.serializedValue("number", { value: "Number.MAX_SAFE_INTEGER" });
|
|
93
|
+
case Number.NEGATIVE_INFINITY: return this.serializedValue("number", { value: "Number.NEGATIVE_INFINITY" });
|
|
94
|
+
case Number.POSITIVE_INFINITY: return this.serializedValue("number", { value: "Number.POSITIVE_INFINITY" });
|
|
95
|
+
default: return value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
normalizeArray(value) {
|
|
99
|
+
return value.map((item, index) => this.normalizeValueRecursive(item, { index }));
|
|
100
|
+
}
|
|
101
|
+
normalizeObject(value) {
|
|
102
|
+
if (isArray(value)) return this.normalizeArray(value);
|
|
103
|
+
if (isPlainObject(value)) return this.normalizePlainObject(value);
|
|
104
|
+
if (value instanceof Date) return this.normalizeDate(value);
|
|
105
|
+
if (value instanceof Promise) return this.serializedValue("Promise");
|
|
106
|
+
if (value instanceof Set) return this.serializedValue("Set", { values: this.normalizeArray(Array.from(value.values())) });
|
|
107
|
+
if (value instanceof Map) {
|
|
108
|
+
const mapAsObject = this.normalizeMap(value);
|
|
109
|
+
return this.serializedValue("Map", { values: this.normalizePlainObject(mapAsObject) });
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Missing JSON normalization for object of type ${Object.getPrototypeOf(value)}`);
|
|
112
|
+
}
|
|
113
|
+
normalizeDate(value) {
|
|
114
|
+
if (Number.isNaN(value.getTime())) return this.serializedValue("Date", { value: "Invalid date" });
|
|
115
|
+
return this.serializedValue("Date", { value: value.toISOString() });
|
|
116
|
+
}
|
|
117
|
+
normalizePlainObject(value) {
|
|
118
|
+
const normalizedObject = {};
|
|
119
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
120
|
+
if (propertyValue === void 0 && !this.includeUndefinedObjectProperties) continue;
|
|
121
|
+
this.assertKeyType(key);
|
|
122
|
+
normalizedObject[key] = this.normalizeValueRecursive(propertyValue, { key });
|
|
123
|
+
}
|
|
124
|
+
return normalizedObject;
|
|
125
|
+
}
|
|
126
|
+
normalizeMap(value) {
|
|
127
|
+
return Array.from(value.entries()).reduce((object, [key, value]) => {
|
|
128
|
+
this.assertKeyType(key);
|
|
129
|
+
object[key] = value;
|
|
130
|
+
return object;
|
|
131
|
+
}, {});
|
|
132
|
+
}
|
|
133
|
+
serializedValue(type, additionalProps = {}) {
|
|
134
|
+
return {
|
|
135
|
+
$type: type,
|
|
136
|
+
...additionalProps
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
assertKeyType(key) {
|
|
140
|
+
if (!isString(key)) throw new Error(`Key of type ${typeof key} cannot be normalized.`);
|
|
141
|
+
}
|
|
142
|
+
applyCustomNormalizers(value, context = {}) {
|
|
143
|
+
let normalizedValue = value;
|
|
144
|
+
for (const normalizer of this.normalizers) normalizedValue = normalizer(normalizedValue, context);
|
|
145
|
+
return normalizedValue;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/serializers/text-serializer.ts
|
|
151
|
+
var TextSerializer = class {
|
|
152
|
+
fileExtension;
|
|
153
|
+
normalizers;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
this.normalizers = options.normalizers ?? [];
|
|
156
|
+
this.fileExtension = options.fileExtension ?? "txt";
|
|
157
|
+
}
|
|
158
|
+
canSerialize(value) {
|
|
159
|
+
return isString(value);
|
|
160
|
+
}
|
|
161
|
+
serialize(value) {
|
|
162
|
+
if (!this.canSerialize(value)) throw new Error(`Missing text serialization for value of type ${typeof value}.`);
|
|
163
|
+
return addTrailingNewLine(this.normalizeValue(value));
|
|
164
|
+
}
|
|
165
|
+
normalizeValue(value) {
|
|
166
|
+
let normalizedValue = value;
|
|
167
|
+
for (const normalizer of this.normalizers) normalizedValue = normalizer(normalizedValue);
|
|
168
|
+
return normalizedValue.trim();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/matcher/validation-file-matcher.ts
|
|
174
|
+
var ValidationFileMatcher = class {
|
|
175
|
+
updateSnapshots;
|
|
176
|
+
serializer;
|
|
177
|
+
filePaths;
|
|
178
|
+
validationFile;
|
|
179
|
+
constructor(config) {
|
|
180
|
+
this.updateSnapshots = config.updateSnapshots ?? "missing";
|
|
181
|
+
this.serializer = config.serializer;
|
|
182
|
+
this.filePaths = this.buildFilePaths(config);
|
|
183
|
+
this.validationFile = this.readValidationFile();
|
|
184
|
+
}
|
|
185
|
+
get isValidationFileMissing() {
|
|
186
|
+
return this.validationFile === void 0;
|
|
187
|
+
}
|
|
188
|
+
get isUpdate() {
|
|
189
|
+
return this.updateSnapshots === "all" || this.isValidationFileMissing && this.updateSnapshots === "missing";
|
|
190
|
+
}
|
|
191
|
+
matchFileSnapshot(actual) {
|
|
192
|
+
if (!this.serializer.canSerialize(actual)) throw new Error(`Cannot serialize value of type ${typeof actual}`);
|
|
193
|
+
const serializedActual = this.serializer.serialize(actual);
|
|
194
|
+
const expected = this.resolveExpected(serializedActual);
|
|
195
|
+
return this.createMatcherResult({
|
|
196
|
+
actual: serializedActual,
|
|
197
|
+
expected
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
buildFilePaths(config) {
|
|
201
|
+
const { validationDir, outputDir, filePath, serializer } = config;
|
|
202
|
+
const filePathWithExtension = `${filePath}.${serializer.fileExtension}`;
|
|
203
|
+
return {
|
|
204
|
+
outputFilePath: path$1.join(outputDir, filePathWithExtension),
|
|
205
|
+
validationFilePath: path$1.join(validationDir, filePathWithExtension)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
readValidationFile() {
|
|
209
|
+
const { validationFilePath } = this.filePaths;
|
|
210
|
+
if (!fs$1.existsSync(validationFilePath)) return;
|
|
211
|
+
return readSnapshotFile(validationFilePath);
|
|
212
|
+
}
|
|
213
|
+
createMatcherResult(params) {
|
|
214
|
+
const { actual, expected } = params;
|
|
215
|
+
const { outputFilePath, validationFilePath } = this.filePaths;
|
|
216
|
+
const isValidationFileMissing = this.isValidationFileMissing;
|
|
217
|
+
return {
|
|
218
|
+
actual,
|
|
219
|
+
expected,
|
|
220
|
+
outputFilePath,
|
|
221
|
+
validationFilePath,
|
|
222
|
+
message: () => isValidationFileMissing ? `Missing validation file '${validationFilePath}'` : `Output file '${outputFilePath}'\ndoes not match validation file '${validationFilePath}'`,
|
|
223
|
+
writeFileSnapshots: () => this.writeFileSnapshots(params)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
writeFileSnapshots(matcherResult) {
|
|
227
|
+
const { actual, expected } = matcherResult;
|
|
228
|
+
const { outputFilePath, validationFilePath } = this.filePaths;
|
|
229
|
+
writeSnapshotFile(outputFilePath, actual);
|
|
230
|
+
if (this.isUpdate) {
|
|
231
|
+
const validationFileData = this.isValidationFileMissing ? expected : actual;
|
|
232
|
+
writeSnapshotFile(validationFilePath, validationFileData);
|
|
233
|
+
this.validationFile = validationFileData;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
resolveExpected(actual) {
|
|
237
|
+
if (this.validationFile === void 0) return addMissingFileMarker(actual);
|
|
238
|
+
return this.validationFile;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/matcher/file-path-resolver.ts
|
|
244
|
+
function resolveNameAsFile(params) {
|
|
245
|
+
return resolveFilePath(params, ({ basePath, normalizedName }) => path.join(basePath, normalizedName));
|
|
246
|
+
}
|
|
247
|
+
function resolveNameAsFileSuffix(params) {
|
|
248
|
+
return resolveFilePath(params, ({ basePath, normalizedName }) => `${basePath}_${normalizedName}`);
|
|
249
|
+
}
|
|
250
|
+
function resolveBasePath(params) {
|
|
251
|
+
const { testPath, titlePath } = params;
|
|
252
|
+
const normalizedTitlePath = titlePath.map(normalizeFileName);
|
|
253
|
+
return path.join(testPath, ...normalizedTitlePath);
|
|
254
|
+
}
|
|
255
|
+
function resolveFilePath(params, resolveNamedFilePath) {
|
|
256
|
+
const { name, ...baseParams } = params;
|
|
257
|
+
const basePath = resolveBasePath(baseParams);
|
|
258
|
+
if (name === void 0) return basePath;
|
|
259
|
+
return resolveNamedFilePath({
|
|
260
|
+
basePath,
|
|
261
|
+
normalizedName: normalizeFileName(name)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
export { JsonSerializer, TextSerializer, ValidationFileMatcher, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cronn/lib-file-snapshots",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "The library agnostic core for testing file snapshots",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"file snapshots"
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"exports": {
|
|
36
36
|
"./package.json": "./package.json",
|
|
37
37
|
".": {
|
|
38
|
-
"types": "./dist/index.d.
|
|
39
|
-
"default": "./dist/index.
|
|
38
|
+
"types": "./dist/index.d.mts",
|
|
39
|
+
"default": "./dist/index.mjs"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@trivago/prettier-plugin-sort-imports": "6.0.2",
|
|
47
|
-
"@types/node": "24.10.
|
|
48
|
-
"@vitest/coverage-v8": "4.0.
|
|
47
|
+
"@types/node": "24.10.12",
|
|
48
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
49
49
|
"eslint": "9.39.2",
|
|
50
50
|
"eslint-config-prettier": "10.1.8",
|
|
51
51
|
"eslint-plugin-unused-imports": "4.3.0",
|
|
52
|
-
"prettier": "3.8.
|
|
53
|
-
"
|
|
52
|
+
"prettier": "3.8.1",
|
|
53
|
+
"tsdown": "0.20.3",
|
|
54
54
|
"typescript": "5.9.3",
|
|
55
|
-
"typescript-eslint": "8.
|
|
56
|
-
"vitest": "4.0.
|
|
55
|
+
"typescript-eslint": "8.54.0",
|
|
56
|
+
"vitest": "4.0.18",
|
|
57
57
|
"@cronn/shared-configs": "0.0.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
@@ -62,6 +62,6 @@
|
|
|
62
62
|
"test": "vitest run",
|
|
63
63
|
"test:coverage": "vitest run --coverage",
|
|
64
64
|
"compile": "tsc",
|
|
65
|
-
"build": "
|
|
65
|
+
"build": "tsdown"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
interface SnapshotSerializer {
|
|
2
|
-
/**
|
|
3
|
-
* The file extension associated with the serialized value
|
|
4
|
-
*/
|
|
5
|
-
readonly fileExtension: string;
|
|
6
|
-
/**
|
|
7
|
-
* Returns true when value can be serialized
|
|
8
|
-
*
|
|
9
|
-
* @param value The value to be serialized
|
|
10
|
-
*/
|
|
11
|
-
canSerialize(value: unknown): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Serializes value
|
|
14
|
-
*
|
|
15
|
-
* @param value The value to be serialized
|
|
16
|
-
* @return {string} The serialized value
|
|
17
|
-
* @throws {Error} Will throw an error if value cannot be serialized.
|
|
18
|
-
*/
|
|
19
|
-
serialize(value: unknown): string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface JsonSerializerOptions {
|
|
23
|
-
/**
|
|
24
|
-
* Serializes `undefined` properties in objects. By default, they are omitted.
|
|
25
|
-
*
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
includeUndefinedObjectProperties?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Custom normalizers to apply before serialization
|
|
31
|
-
*/
|
|
32
|
-
normalizers?: Array<JsonNormalizer>;
|
|
33
|
-
/**
|
|
34
|
-
* Indentation size in spaces
|
|
35
|
-
*
|
|
36
|
-
* @default 2
|
|
37
|
-
*/
|
|
38
|
-
indentSize?: number;
|
|
39
|
-
}
|
|
40
|
-
type JsonNormalizer = (value: unknown, context: JsonNormalizerContext) => unknown;
|
|
41
|
-
interface JsonNormalizerContext {
|
|
42
|
-
key?: string;
|
|
43
|
-
index?: number;
|
|
44
|
-
}
|
|
45
|
-
declare class JsonSerializer implements SnapshotSerializer {
|
|
46
|
-
readonly fileExtension = "json";
|
|
47
|
-
private readonly includeUndefinedObjectProperties;
|
|
48
|
-
private readonly normalizers;
|
|
49
|
-
private readonly indentSize;
|
|
50
|
-
constructor(options?: JsonSerializerOptions);
|
|
51
|
-
canSerialize(_value: unknown): boolean;
|
|
52
|
-
serialize(value: unknown): string;
|
|
53
|
-
private normalizeValueRecursive;
|
|
54
|
-
private normalizeNumber;
|
|
55
|
-
private normalizeArray;
|
|
56
|
-
private normalizeObject;
|
|
57
|
-
private normalizeDate;
|
|
58
|
-
private normalizePlainObject;
|
|
59
|
-
private normalizeMap;
|
|
60
|
-
private serializedValue;
|
|
61
|
-
private assertKeyType;
|
|
62
|
-
private applyCustomNormalizers;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface TextSerializerOptions {
|
|
66
|
-
/**
|
|
67
|
-
* Custom normalizers to apply before serialization
|
|
68
|
-
*/
|
|
69
|
-
normalizers?: Array<TextNormalizer>;
|
|
70
|
-
/**
|
|
71
|
-
* File extension used for storing the text file
|
|
72
|
-
*
|
|
73
|
-
* @default "txt"
|
|
74
|
-
*/
|
|
75
|
-
fileExtension?: string;
|
|
76
|
-
}
|
|
77
|
-
type TextNormalizer = (value: string) => string;
|
|
78
|
-
declare class TextSerializer implements SnapshotSerializer {
|
|
79
|
-
readonly fileExtension: string;
|
|
80
|
-
private readonly normalizers;
|
|
81
|
-
constructor(options?: TextSerializerOptions);
|
|
82
|
-
canSerialize(value: unknown): value is string;
|
|
83
|
-
serialize(value: unknown): string;
|
|
84
|
-
private normalizeValue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface ValidationFileMatcherConfig {
|
|
88
|
-
/**
|
|
89
|
-
* Directory in which golden masters are stored
|
|
90
|
-
*/
|
|
91
|
-
validationDir: string;
|
|
92
|
-
/**
|
|
93
|
-
* Directory in which file snapshots from test runs are stored
|
|
94
|
-
*/
|
|
95
|
-
outputDir: string;
|
|
96
|
-
/**
|
|
97
|
-
* The full path to the snapshot file
|
|
98
|
-
*
|
|
99
|
-
* @example "src/tests/feature/test/when_x_then_y"
|
|
100
|
-
*/
|
|
101
|
-
filePath: string;
|
|
102
|
-
/**
|
|
103
|
-
* The serializer to use for the snapshot
|
|
104
|
-
*/
|
|
105
|
-
serializer: SnapshotSerializer;
|
|
106
|
-
/**
|
|
107
|
-
* Whether to update golden masters with the actual result.
|
|
108
|
-
*
|
|
109
|
-
* @default "missing"
|
|
110
|
-
*/
|
|
111
|
-
updateSnapshots?: UpdateSnapshotsType;
|
|
112
|
-
}
|
|
113
|
-
type UpdateSnapshotsType = "all" | "missing" | "none";
|
|
114
|
-
interface ValidationFileMatcherResult {
|
|
115
|
-
actual: string;
|
|
116
|
-
expected: string;
|
|
117
|
-
outputFilePath: string;
|
|
118
|
-
validationFilePath: string;
|
|
119
|
-
message: () => string;
|
|
120
|
-
writeFileSnapshots: () => void;
|
|
121
|
-
}
|
|
122
|
-
type FilePathResolver = (params: FilePathResolverParams) => string;
|
|
123
|
-
interface FilePathResolverParams {
|
|
124
|
-
testPath: string;
|
|
125
|
-
titlePath: Array<string>;
|
|
126
|
-
name?: string;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
declare class ValidationFileMatcher {
|
|
130
|
-
private readonly updateSnapshots;
|
|
131
|
-
private readonly serializer;
|
|
132
|
-
private readonly filePaths;
|
|
133
|
-
private validationFile;
|
|
134
|
-
constructor(config: ValidationFileMatcherConfig);
|
|
135
|
-
get isValidationFileMissing(): boolean;
|
|
136
|
-
get isUpdate(): boolean;
|
|
137
|
-
matchFileSnapshot(actual: unknown): ValidationFileMatcherResult;
|
|
138
|
-
private buildFilePaths;
|
|
139
|
-
private readValidationFile;
|
|
140
|
-
private createMatcherResult;
|
|
141
|
-
private writeFileSnapshots;
|
|
142
|
-
private resolveExpected;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
declare function resolveNameAsFile(params: FilePathResolverParams): string;
|
|
146
|
-
declare function resolveNameAsFileSuffix(params: FilePathResolverParams): string;
|
|
147
|
-
|
|
148
|
-
declare function normalizeFileName(testName: string): string;
|
|
149
|
-
|
|
150
|
-
declare function isString(value: unknown): value is string;
|
|
151
|
-
declare function isArray(value: unknown): value is Array<unknown>;
|
|
152
|
-
type PlainObject = Record<PropertyKey, unknown>;
|
|
153
|
-
declare function isPlainObject(value: unknown): value is PlainObject;
|
|
154
|
-
|
|
155
|
-
export { type FilePathResolver, type FilePathResolverParams, type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotSerializer, type TextNormalizer, TextSerializer, type UpdateSnapshotsType, ValidationFileMatcher, type ValidationFileMatcherResult, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
|
package/dist/index.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
// src/utils/file.ts
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import path from "path";
|
|
5
|
-
var NEW_LINE_SEPARATOR = "\n";
|
|
6
|
-
var EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
|
|
7
|
-
function normalizeFileName(testName) {
|
|
8
|
-
return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
|
|
9
|
-
}
|
|
10
|
-
function readSnapshotFile(path4) {
|
|
11
|
-
const contents = fs.readFileSync(path4, { encoding: "utf8" });
|
|
12
|
-
if (os.EOL === NEW_LINE_SEPARATOR) {
|
|
13
|
-
return contents;
|
|
14
|
-
}
|
|
15
|
-
return contents.replace(new RegExp(os.EOL, "g"), NEW_LINE_SEPARATOR);
|
|
16
|
-
}
|
|
17
|
-
function writeSnapshotFile(file, data) {
|
|
18
|
-
mkdirRecursive(path.dirname(file));
|
|
19
|
-
fs.writeFileSync(file, data, { encoding: "utf8" });
|
|
20
|
-
}
|
|
21
|
-
function mkdirRecursive(path4) {
|
|
22
|
-
fs.mkdirSync(path4, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
function addTrailingNewLine(data) {
|
|
25
|
-
return `${data}${NEW_LINE_SEPARATOR}`;
|
|
26
|
-
}
|
|
27
|
-
function addMissingFileMarker(data) {
|
|
28
|
-
return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// src/utils/guards.ts
|
|
32
|
-
function isString(value) {
|
|
33
|
-
return typeof value === "string";
|
|
34
|
-
}
|
|
35
|
-
function isArray(value) {
|
|
36
|
-
return Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
function isObject(value) {
|
|
39
|
-
return typeof value === "object" && value !== null && !isArray(value);
|
|
40
|
-
}
|
|
41
|
-
function isPlainObject(value) {
|
|
42
|
-
if (!isObject(value)) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
const proto = Object.getPrototypeOf(value);
|
|
46
|
-
return proto === null || proto === Object.prototype;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// src/utils/validators.ts
|
|
50
|
-
function isPositiveInteger(value) {
|
|
51
|
-
return Number.isInteger(value) && value > 0;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/serializers/json-serializer.ts
|
|
55
|
-
var JsonSerializer = class {
|
|
56
|
-
fileExtension = "json";
|
|
57
|
-
includeUndefinedObjectProperties;
|
|
58
|
-
normalizers;
|
|
59
|
-
indentSize;
|
|
60
|
-
constructor(options = {}) {
|
|
61
|
-
if (options.indentSize !== void 0 && !isPositiveInteger(options.indentSize)) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
"Invalid option indentSize: value must be a positive integer."
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
this.includeUndefinedObjectProperties = options.includeUndefinedObjectProperties ?? false;
|
|
67
|
-
this.normalizers = options.normalizers ?? [];
|
|
68
|
-
this.indentSize = options.indentSize ?? 2;
|
|
69
|
-
}
|
|
70
|
-
canSerialize(_value) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
serialize(value) {
|
|
74
|
-
const jsonValue = this.normalizeValueRecursive(value);
|
|
75
|
-
const jsonString = JSON.stringify(jsonValue, void 0, this.indentSize);
|
|
76
|
-
return addTrailingNewLine(jsonString);
|
|
77
|
-
}
|
|
78
|
-
normalizeValueRecursive(value, context) {
|
|
79
|
-
const customValue = this.applyCustomNormalizers(value, context);
|
|
80
|
-
if (customValue === void 0) {
|
|
81
|
-
return this.serializedValue("undefined");
|
|
82
|
-
}
|
|
83
|
-
if (customValue === null || isString(customValue) || typeof customValue === "boolean") {
|
|
84
|
-
const isRoot = context === void 0;
|
|
85
|
-
return isRoot ? this.serializedValue(typeof customValue, {
|
|
86
|
-
value: customValue
|
|
87
|
-
}) : customValue;
|
|
88
|
-
}
|
|
89
|
-
if (typeof customValue === "number") {
|
|
90
|
-
return this.normalizeNumber(customValue);
|
|
91
|
-
}
|
|
92
|
-
if (typeof customValue === "bigint") {
|
|
93
|
-
return this.serializedValue("bigint", {
|
|
94
|
-
value: customValue.toString()
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (typeof customValue === "symbol") {
|
|
98
|
-
return this.serializedValue("symbol", {
|
|
99
|
-
description: customValue.description
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (typeof customValue === "function") {
|
|
103
|
-
return this.serializedValue("function", { name: customValue.name });
|
|
104
|
-
}
|
|
105
|
-
if (typeof customValue === "object") {
|
|
106
|
-
return this.normalizeObject(customValue);
|
|
107
|
-
}
|
|
108
|
-
throw new Error(
|
|
109
|
-
`Missing JSON normalization for value of type ${typeof customValue}.`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
normalizeNumber(value) {
|
|
113
|
-
if (Number.isNaN(value)) {
|
|
114
|
-
return this.serializedValue("number", { value: "Number.NaN" });
|
|
115
|
-
}
|
|
116
|
-
switch (value) {
|
|
117
|
-
case Number.MIN_VALUE:
|
|
118
|
-
return this.serializedValue("number", { value: "Number.MIN_VALUE" });
|
|
119
|
-
case Number.MAX_VALUE:
|
|
120
|
-
return this.serializedValue("number", { value: "Number.MAX_VALUE" });
|
|
121
|
-
case Number.MIN_SAFE_INTEGER:
|
|
122
|
-
return this.serializedValue("number", {
|
|
123
|
-
value: "Number.MIN_SAFE_INTEGER"
|
|
124
|
-
});
|
|
125
|
-
case Number.MAX_SAFE_INTEGER:
|
|
126
|
-
return this.serializedValue("number", {
|
|
127
|
-
value: "Number.MAX_SAFE_INTEGER"
|
|
128
|
-
});
|
|
129
|
-
case Number.NEGATIVE_INFINITY:
|
|
130
|
-
return this.serializedValue("number", {
|
|
131
|
-
value: "Number.NEGATIVE_INFINITY"
|
|
132
|
-
});
|
|
133
|
-
case Number.POSITIVE_INFINITY:
|
|
134
|
-
return this.serializedValue("number", {
|
|
135
|
-
value: "Number.POSITIVE_INFINITY"
|
|
136
|
-
});
|
|
137
|
-
default:
|
|
138
|
-
return value;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
normalizeArray(value) {
|
|
142
|
-
return value.map(
|
|
143
|
-
(item, index) => this.normalizeValueRecursive(item, { index })
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
normalizeObject(value) {
|
|
147
|
-
if (isArray(value)) {
|
|
148
|
-
return this.normalizeArray(value);
|
|
149
|
-
}
|
|
150
|
-
if (isPlainObject(value)) {
|
|
151
|
-
return this.normalizePlainObject(value);
|
|
152
|
-
}
|
|
153
|
-
if (value instanceof Date) {
|
|
154
|
-
return this.normalizeDate(value);
|
|
155
|
-
}
|
|
156
|
-
if (value instanceof Promise) {
|
|
157
|
-
return this.serializedValue("Promise");
|
|
158
|
-
}
|
|
159
|
-
if (value instanceof Set) {
|
|
160
|
-
return this.serializedValue("Set", {
|
|
161
|
-
values: this.normalizeArray(Array.from(value.values()))
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
if (value instanceof Map) {
|
|
165
|
-
const mapAsObject = this.normalizeMap(value);
|
|
166
|
-
return this.serializedValue("Map", {
|
|
167
|
-
values: this.normalizePlainObject(mapAsObject)
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
throw new Error(
|
|
171
|
-
`Missing JSON normalization for object of type ${Object.getPrototypeOf(value)}`
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
normalizeDate(value) {
|
|
175
|
-
if (Number.isNaN(value.getTime())) {
|
|
176
|
-
return this.serializedValue("Date", { value: "Invalid date" });
|
|
177
|
-
}
|
|
178
|
-
return this.serializedValue("Date", { value: value.toISOString() });
|
|
179
|
-
}
|
|
180
|
-
normalizePlainObject(value) {
|
|
181
|
-
const normalizedObject = {};
|
|
182
|
-
for (const [key, propertyValue] of Object.entries(value)) {
|
|
183
|
-
if (propertyValue === void 0 && !this.includeUndefinedObjectProperties) {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
this.assertKeyType(key);
|
|
187
|
-
normalizedObject[key] = this.normalizeValueRecursive(propertyValue, {
|
|
188
|
-
key
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return normalizedObject;
|
|
192
|
-
}
|
|
193
|
-
normalizeMap(value) {
|
|
194
|
-
return Array.from(value.entries()).reduce(
|
|
195
|
-
(object, [key, value2]) => {
|
|
196
|
-
this.assertKeyType(key);
|
|
197
|
-
object[key] = value2;
|
|
198
|
-
return object;
|
|
199
|
-
},
|
|
200
|
-
{}
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
serializedValue(type, additionalProps = {}) {
|
|
204
|
-
return { $type: type, ...additionalProps };
|
|
205
|
-
}
|
|
206
|
-
assertKeyType(key) {
|
|
207
|
-
if (!isString(key)) {
|
|
208
|
-
throw new Error(`Key of type ${typeof key} cannot be normalized.`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
applyCustomNormalizers(value, context = {}) {
|
|
212
|
-
let normalizedValue = value;
|
|
213
|
-
for (const normalizer of this.normalizers) {
|
|
214
|
-
normalizedValue = normalizer(normalizedValue, context);
|
|
215
|
-
}
|
|
216
|
-
return normalizedValue;
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// src/serializers/text-serializer.ts
|
|
221
|
-
var TextSerializer = class {
|
|
222
|
-
fileExtension;
|
|
223
|
-
normalizers;
|
|
224
|
-
constructor(options = {}) {
|
|
225
|
-
this.normalizers = options.normalizers ?? [];
|
|
226
|
-
this.fileExtension = options.fileExtension ?? "txt";
|
|
227
|
-
}
|
|
228
|
-
canSerialize(value) {
|
|
229
|
-
return isString(value);
|
|
230
|
-
}
|
|
231
|
-
serialize(value) {
|
|
232
|
-
if (!this.canSerialize(value)) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`Missing text serialization for value of type ${typeof value}.`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
const normalizedValue = this.normalizeValue(value);
|
|
238
|
-
return addTrailingNewLine(normalizedValue);
|
|
239
|
-
}
|
|
240
|
-
normalizeValue(value) {
|
|
241
|
-
let normalizedValue = value;
|
|
242
|
-
for (const normalizer of this.normalizers) {
|
|
243
|
-
normalizedValue = normalizer(normalizedValue);
|
|
244
|
-
}
|
|
245
|
-
return normalizedValue.trim();
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// src/matcher/validation-file-matcher.ts
|
|
250
|
-
import * as fs2 from "fs";
|
|
251
|
-
import * as path2 from "path";
|
|
252
|
-
var ValidationFileMatcher = class {
|
|
253
|
-
updateSnapshots;
|
|
254
|
-
serializer;
|
|
255
|
-
filePaths;
|
|
256
|
-
validationFile;
|
|
257
|
-
constructor(config) {
|
|
258
|
-
this.updateSnapshots = config.updateSnapshots ?? "missing";
|
|
259
|
-
this.serializer = config.serializer;
|
|
260
|
-
this.filePaths = this.buildFilePaths(config);
|
|
261
|
-
this.validationFile = this.readValidationFile();
|
|
262
|
-
}
|
|
263
|
-
get isValidationFileMissing() {
|
|
264
|
-
return this.validationFile === void 0;
|
|
265
|
-
}
|
|
266
|
-
get isUpdate() {
|
|
267
|
-
return this.updateSnapshots === "all" || this.isValidationFileMissing && this.updateSnapshots === "missing";
|
|
268
|
-
}
|
|
269
|
-
matchFileSnapshot(actual) {
|
|
270
|
-
if (!this.serializer.canSerialize(actual)) {
|
|
271
|
-
throw new Error(`Cannot serialize value of type ${typeof actual}`);
|
|
272
|
-
}
|
|
273
|
-
const serializedActual = this.serializer.serialize(actual);
|
|
274
|
-
const expected = this.resolveExpected(serializedActual);
|
|
275
|
-
return this.createMatcherResult({
|
|
276
|
-
actual: serializedActual,
|
|
277
|
-
expected
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
buildFilePaths(config) {
|
|
281
|
-
const { validationDir, outputDir, filePath, serializer } = config;
|
|
282
|
-
const filePathWithExtension = `${filePath}.${serializer.fileExtension}`;
|
|
283
|
-
return {
|
|
284
|
-
outputFilePath: path2.join(outputDir, filePathWithExtension),
|
|
285
|
-
validationFilePath: path2.join(validationDir, filePathWithExtension)
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
readValidationFile() {
|
|
289
|
-
const { validationFilePath } = this.filePaths;
|
|
290
|
-
if (!fs2.existsSync(validationFilePath)) {
|
|
291
|
-
return void 0;
|
|
292
|
-
}
|
|
293
|
-
return readSnapshotFile(validationFilePath);
|
|
294
|
-
}
|
|
295
|
-
createMatcherResult(params) {
|
|
296
|
-
const { actual, expected } = params;
|
|
297
|
-
const { outputFilePath, validationFilePath } = this.filePaths;
|
|
298
|
-
const isValidationFileMissing = this.isValidationFileMissing;
|
|
299
|
-
return {
|
|
300
|
-
actual,
|
|
301
|
-
expected,
|
|
302
|
-
outputFilePath,
|
|
303
|
-
validationFilePath,
|
|
304
|
-
message: () => isValidationFileMissing ? `Missing validation file '${validationFilePath}'` : `Output file '${outputFilePath}'
|
|
305
|
-
does not match validation file '${validationFilePath}'`,
|
|
306
|
-
writeFileSnapshots: () => this.writeFileSnapshots(params)
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
writeFileSnapshots(matcherResult) {
|
|
310
|
-
const { actual, expected } = matcherResult;
|
|
311
|
-
const { outputFilePath, validationFilePath } = this.filePaths;
|
|
312
|
-
writeSnapshotFile(outputFilePath, actual);
|
|
313
|
-
if (this.isUpdate) {
|
|
314
|
-
const validationFileData = this.isValidationFileMissing ? expected : actual;
|
|
315
|
-
writeSnapshotFile(validationFilePath, validationFileData);
|
|
316
|
-
this.validationFile = validationFileData;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
resolveExpected(actual) {
|
|
320
|
-
if (this.validationFile === void 0) {
|
|
321
|
-
return addMissingFileMarker(actual);
|
|
322
|
-
}
|
|
323
|
-
return this.validationFile;
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
// src/matcher/file-path-resolver.ts
|
|
328
|
-
import path3 from "path";
|
|
329
|
-
function resolveNameAsFile(params) {
|
|
330
|
-
return resolveFilePath(
|
|
331
|
-
params,
|
|
332
|
-
({ basePath, normalizedName }) => path3.join(basePath, normalizedName)
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
function resolveNameAsFileSuffix(params) {
|
|
336
|
-
return resolveFilePath(
|
|
337
|
-
params,
|
|
338
|
-
({ basePath, normalizedName }) => `${basePath}_${normalizedName}`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
function resolveBasePath(params) {
|
|
342
|
-
const { testPath, titlePath } = params;
|
|
343
|
-
const normalizedTitlePath = titlePath.map(normalizeFileName);
|
|
344
|
-
return path3.join(testPath, ...normalizedTitlePath);
|
|
345
|
-
}
|
|
346
|
-
function resolveFilePath(params, resolveNamedFilePath) {
|
|
347
|
-
const { name, ...baseParams } = params;
|
|
348
|
-
const basePath = resolveBasePath(baseParams);
|
|
349
|
-
if (name === void 0) {
|
|
350
|
-
return basePath;
|
|
351
|
-
}
|
|
352
|
-
const normalizedName = normalizeFileName(name);
|
|
353
|
-
return resolveNamedFilePath({ basePath, normalizedName });
|
|
354
|
-
}
|
|
355
|
-
export {
|
|
356
|
-
JsonSerializer,
|
|
357
|
-
TextSerializer,
|
|
358
|
-
ValidationFileMatcher,
|
|
359
|
-
isArray,
|
|
360
|
-
isPlainObject,
|
|
361
|
-
isString,
|
|
362
|
-
normalizeFileName,
|
|
363
|
-
resolveNameAsFile,
|
|
364
|
-
resolveNameAsFileSuffix
|
|
365
|
-
};
|