@aljazmc/testero 0.4.3 → 0.5.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/README.md +109 -91
- package/bin/main.js +1447 -474
- package/bin/types/arguments.d.ts +5 -1
- package/bin/types/coverage.d.ts +63 -0
- package/bin/types/expect/assertion.d.ts +5 -2
- package/bin/types/expect/equality.d.ts +2 -0
- package/bin/types/expect/matchers.d.ts +1 -1
- package/bin/types/reporter.d.ts +3 -0
- package/bin/types/rules.d.ts +9 -0
- package/bin/types/runner.d.ts +5 -0
- package/bin/types/transformer.d.ts +9 -1
- package/bin/types/watcher.d.ts +52 -0
- package/package.json +2 -3
package/bin/main.js
CHANGED
|
@@ -21,22 +21,33 @@ var init_pattern = __esm({
|
|
|
21
21
|
__name(this, "Pattern");
|
|
22
22
|
}
|
|
23
23
|
#anchored;
|
|
24
|
+
#directories;
|
|
24
25
|
#regex;
|
|
25
|
-
|
|
26
|
+
/** With the name of the setting the glob comes from, for its errors. */
|
|
27
|
+
constructor(source, setting = "glob") {
|
|
28
|
+
this.#directories = /[^/]\/+$/.test(source);
|
|
26
29
|
const glob = source.replace(/^\.?\/+|\/+$/g, "");
|
|
27
|
-
this.#anchored = glob.includes("/");
|
|
28
|
-
this.#regex = new RegExp(
|
|
30
|
+
this.#anchored = glob.includes("/") || /^\.?\//.test(source);
|
|
31
|
+
this.#regex = new RegExp(
|
|
32
|
+
`^${_Pattern.#toRegex(glob, source, setting)}$`
|
|
33
|
+
);
|
|
29
34
|
}
|
|
30
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Whether a path, or a folder it is in, matches. Whether the path itself
|
|
37
|
+
* is a folder matters to globs ending with `/`.
|
|
38
|
+
*/
|
|
39
|
+
matches(path, directory = false) {
|
|
31
40
|
const segments = path.split("/");
|
|
32
|
-
return segments.some(
|
|
33
|
-
(
|
|
41
|
+
return segments.some((segment, index) => {
|
|
42
|
+
if (this.#directories && index === segments.length - 1 && !directory)
|
|
43
|
+
return false;
|
|
44
|
+
return this.#regex.test(
|
|
34
45
|
this.#anchored ? segments.slice(0, index + 1).join("/") : segment
|
|
35
|
-
)
|
|
36
|
-
);
|
|
46
|
+
);
|
|
47
|
+
});
|
|
37
48
|
}
|
|
38
49
|
/** Translates `**`, `*`, `?` and `{a,b}` alternatives, other characters match themselves. */
|
|
39
|
-
static #toRegex(glob) {
|
|
50
|
+
static #toRegex(glob, source, setting) {
|
|
40
51
|
const tokens = {
|
|
41
52
|
"**/": "(?:.*/)?",
|
|
42
53
|
"**": ".*",
|
|
@@ -46,7 +57,7 @@ var init_pattern = __esm({
|
|
|
46
57
|
let regex = "";
|
|
47
58
|
let braces = 0;
|
|
48
59
|
for (const [token] of glob.matchAll(/\*\*\/|\*\*|[\s\S]/g)) {
|
|
49
|
-
if (token
|
|
60
|
+
if (Object.hasOwn(tokens, token)) regex += tokens[token];
|
|
50
61
|
else if (token === "{") {
|
|
51
62
|
braces++;
|
|
52
63
|
regex += "(?:";
|
|
@@ -56,6 +67,10 @@ var init_pattern = __esm({
|
|
|
56
67
|
} else if (token === "," && braces > 0) regex += "|";
|
|
57
68
|
else regex += token.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
58
69
|
}
|
|
70
|
+
if (braces > 0)
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Invalid ${setting} ${JSON.stringify(source)}: a { is not closed by a }.`
|
|
73
|
+
);
|
|
59
74
|
return regex;
|
|
60
75
|
}
|
|
61
76
|
};
|
|
@@ -74,19 +89,29 @@ var init_ignore = __esm({
|
|
|
74
89
|
}
|
|
75
90
|
#patterns;
|
|
76
91
|
constructor(sources) {
|
|
77
|
-
this.#patterns = sources.map((source) => new Pattern(source));
|
|
92
|
+
this.#patterns = sources.map((source) => new Pattern(source, "ignore"));
|
|
78
93
|
}
|
|
79
|
-
|
|
94
|
+
/** Whether a path is ignored, globs ending with `/` only ignore folders. */
|
|
95
|
+
matches(path, directory = false) {
|
|
80
96
|
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
81
|
-
return this.#patterns.some(
|
|
97
|
+
return this.#patterns.some(
|
|
98
|
+
(pattern) => pattern.matches(normalized, directory)
|
|
99
|
+
);
|
|
82
100
|
}
|
|
83
101
|
};
|
|
84
102
|
}
|
|
85
103
|
});
|
|
86
104
|
|
|
87
105
|
// shared/config/src/config.ts
|
|
88
|
-
import {
|
|
89
|
-
|
|
106
|
+
import {
|
|
107
|
+
existsSync,
|
|
108
|
+
readdirSync,
|
|
109
|
+
readFileSync,
|
|
110
|
+
realpathSync,
|
|
111
|
+
statSync,
|
|
112
|
+
writeFileSync
|
|
113
|
+
} from "node:fs";
|
|
114
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
90
115
|
import { pathToFileURL } from "node:url";
|
|
91
116
|
var Config;
|
|
92
117
|
var init_config = __esm({
|
|
@@ -103,6 +128,11 @@ var init_config = __esm({
|
|
|
103
128
|
rules;
|
|
104
129
|
/** What the config file set, before the defaults were merged in. */
|
|
105
130
|
custom;
|
|
131
|
+
/**
|
|
132
|
+
* Reports files and folders that are left out, like a broken symlink, an
|
|
133
|
+
* unreadable folder or a file that is not UTF-8, as a warning by default.
|
|
134
|
+
*/
|
|
135
|
+
warn = /* @__PURE__ */ __name((message) => console.log(`warn: ${message}`), "warn");
|
|
106
136
|
constructor(options, root = process.cwd(), custom = {}) {
|
|
107
137
|
this.root = root;
|
|
108
138
|
this.ignore = new Ignore(options.ignore);
|
|
@@ -114,6 +144,7 @@ var init_config = __esm({
|
|
|
114
144
|
const file = join(root, `${name}.config.${extension}`);
|
|
115
145
|
if (existsSync(file)) {
|
|
116
146
|
const { default: options } = await import(pathToFileURL(file).href);
|
|
147
|
+
_Config.#check(`${name}.config.${extension}`, options);
|
|
117
148
|
return new _Config(
|
|
118
149
|
_Config.merge(defaults, options),
|
|
119
150
|
root,
|
|
@@ -123,12 +154,26 @@ var init_config = __esm({
|
|
|
123
154
|
}
|
|
124
155
|
return new _Config(defaults, root);
|
|
125
156
|
}
|
|
157
|
+
/** Fails clearly on a config file that does not export the shape of the defaults. */
|
|
158
|
+
static #check(file, options) {
|
|
159
|
+
if (options === void 0) return;
|
|
160
|
+
if (options?.constructor !== Object)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`${file} must export default an object, like { ignore: [], rules: {} }, not ${Array.isArray(options) ? "an array" : typeof options}.`
|
|
163
|
+
);
|
|
164
|
+
const { ignore } = options;
|
|
165
|
+
if (ignore !== void 0 && !(Array.isArray(ignore) && ignore.every((item) => typeof item === "string")))
|
|
166
|
+
throw new Error(`${file}: ignore must be a list of strings.`);
|
|
167
|
+
}
|
|
126
168
|
/** Handles `--config`, writing the defaults to a TypeScript config file, or with `--config js` a JavaScript one. Returns whether it was given. */
|
|
127
169
|
static init(name, defaults, argv = process.argv.slice(2), root = process.cwd()) {
|
|
128
|
-
const index = argv.
|
|
170
|
+
const index = argv.findIndex(
|
|
171
|
+
(arg) => arg === "--config" || arg.startsWith("--config=")
|
|
172
|
+
);
|
|
129
173
|
if (index === -1) return false;
|
|
130
|
-
const
|
|
131
|
-
const
|
|
174
|
+
const [, inline] = argv[index].split(/=(.*)/s);
|
|
175
|
+
const next = inline ?? argv[index + 1];
|
|
176
|
+
const extension = next === void 0 || next === "" || next.startsWith("-") ? "ts" : next;
|
|
132
177
|
try {
|
|
133
178
|
console.log(
|
|
134
179
|
`created: ${_Config.export(name, defaults, extension, root)}`
|
|
@@ -188,24 +233,111 @@ ${items.map((item) => `${inner}${item},
|
|
|
188
233
|
merged[key] = _Config.merge(base[key], value);
|
|
189
234
|
return merged;
|
|
190
235
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Whether a file, relative to the root, is one of some paths or in one
|
|
238
|
+
* of them, like the paths a tool is given on its command line, relative
|
|
239
|
+
* to `base`. Without paths, every file is. A path that does not exist is
|
|
240
|
+
* an error.
|
|
241
|
+
*/
|
|
242
|
+
within(paths, base = process.cwd()) {
|
|
243
|
+
if (paths.length === 0) return () => true;
|
|
244
|
+
const relatives = paths.map((path) => {
|
|
245
|
+
const absolute = resolve(base, path);
|
|
246
|
+
if (!existsSync(absolute))
|
|
247
|
+
throw new Error(`${path}: no such file or folder.`);
|
|
248
|
+
return relative(this.root, absolute).split(sep).join("/");
|
|
194
249
|
});
|
|
250
|
+
return (file) => relatives.some(
|
|
251
|
+
(path) => path === "" || file === path || file.startsWith(`${path}/`)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The files that are not ignored, relative to the root. Symlinks are
|
|
256
|
+
* followed, a folder by every path to it, but not into a folder it is in,
|
|
257
|
+
* which would never end; broken symlinks and unreadable folders are
|
|
258
|
+
* reported and left out, like pipes and other special files.
|
|
259
|
+
*/
|
|
260
|
+
*files(directory = "") {
|
|
261
|
+
yield* this.#walk(directory, /* @__PURE__ */ new Set());
|
|
262
|
+
}
|
|
263
|
+
*#walk(directory, ancestors) {
|
|
264
|
+
const absolute = join(this.root, directory);
|
|
265
|
+
let entries;
|
|
266
|
+
let real;
|
|
267
|
+
try {
|
|
268
|
+
real = realpathSync(absolute);
|
|
269
|
+
if (ancestors.has(real)) return;
|
|
270
|
+
entries = readdirSync(absolute, { withFileTypes: true });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.warn(
|
|
273
|
+
`${directory || "."}: the folder cannot be read (${_Config.#reason(error)}), left out.`
|
|
274
|
+
);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
ancestors.add(real);
|
|
195
278
|
for (const entry of entries) {
|
|
196
279
|
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
197
|
-
|
|
198
|
-
if (entry.
|
|
199
|
-
|
|
280
|
+
let kind = entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other";
|
|
281
|
+
if (entry.isSymbolicLink())
|
|
282
|
+
try {
|
|
283
|
+
const target = statSync(join(this.root, path));
|
|
284
|
+
kind = target.isDirectory() ? "directory" : target.isFile() ? "file" : "other";
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (!this.ignore.matches(path))
|
|
287
|
+
this.warn(
|
|
288
|
+
`${path}: a broken symlink (${_Config.#reason(error)}), left out.`
|
|
289
|
+
);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (this.ignore.matches(path, kind === "directory")) continue;
|
|
293
|
+
if (kind === "directory") yield* this.#walk(path, ancestors);
|
|
294
|
+
else if (kind === "file") yield path;
|
|
200
295
|
}
|
|
296
|
+
ancestors.delete(real);
|
|
201
297
|
}
|
|
202
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Text files that are not ignored and that accepts takes. Binaries, with
|
|
300
|
+
* a NUL byte, are skipped; files that are not UTF-8 are reported and
|
|
301
|
+
* skipped, as writing them back as UTF-8 would change their text.
|
|
302
|
+
*/
|
|
203
303
|
*sources(accepts = () => true) {
|
|
304
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
204
305
|
for (const file of this.files()) {
|
|
205
306
|
if (!accepts(file)) continue;
|
|
206
307
|
const path = join(this.root, file);
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
let bytes;
|
|
309
|
+
try {
|
|
310
|
+
bytes = readFileSync(path);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.warn(
|
|
313
|
+
`${file}: the file cannot be read (${_Config.#reason(error)}), left out.`
|
|
314
|
+
);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (bytes.includes(0)) continue;
|
|
318
|
+
let text;
|
|
319
|
+
try {
|
|
320
|
+
text = utf8.decode(bytes);
|
|
321
|
+
} catch {
|
|
322
|
+
this.warn(`${file}: not UTF-8, left out.`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
yield { file, path, text };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Why a file system call failed, in words. */
|
|
329
|
+
static #reason(error) {
|
|
330
|
+
const code = error?.code;
|
|
331
|
+
switch (code) {
|
|
332
|
+
case "ENOENT":
|
|
333
|
+
return "its target does not exist";
|
|
334
|
+
case "EACCES":
|
|
335
|
+
case "EPERM":
|
|
336
|
+
return "no permission";
|
|
337
|
+
case "ELOOP":
|
|
338
|
+
return "a loop of symlinks";
|
|
339
|
+
default:
|
|
340
|
+
return error instanceof Error ? error.message : String(error);
|
|
209
341
|
}
|
|
210
342
|
}
|
|
211
343
|
};
|
|
@@ -223,9 +355,10 @@ var init_includes = __esm({
|
|
|
223
355
|
__name(this, "Includes");
|
|
224
356
|
}
|
|
225
357
|
#globs;
|
|
226
|
-
|
|
358
|
+
/** With the name of the setting the globs come from, for their errors. */
|
|
359
|
+
constructor(globs, setting = "includes") {
|
|
227
360
|
this.#globs = globs.map((glob) => ({
|
|
228
|
-
pattern: new Pattern(glob.replace(/^!/, "")),
|
|
361
|
+
pattern: new Pattern(glob.replace(/^!/, ""), setting),
|
|
229
362
|
included: !glob.startsWith("!")
|
|
230
363
|
}));
|
|
231
364
|
}
|
|
@@ -249,11 +382,152 @@ var init_src = __esm({
|
|
|
249
382
|
}
|
|
250
383
|
});
|
|
251
384
|
|
|
385
|
+
// packages/testero/src/expect/equality.ts
|
|
386
|
+
var Equality;
|
|
387
|
+
var init_equality = __esm({
|
|
388
|
+
"packages/testero/src/expect/equality.ts"() {
|
|
389
|
+
"use strict";
|
|
390
|
+
Equality = class _Equality {
|
|
391
|
+
static {
|
|
392
|
+
__name(this, "Equality");
|
|
393
|
+
}
|
|
394
|
+
/** Whether a value matches by its own rule, like `expect.any(Number)`. */
|
|
395
|
+
static asymmetric(value) {
|
|
396
|
+
return typeof value === "object" && value !== null && typeof value.asymmetricMatch === "function";
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Whether two values are deeply equal. Loosely, properties set to
|
|
400
|
+
* undefined count as missing; strictly, types and holes in arrays count.
|
|
401
|
+
*/
|
|
402
|
+
static equals(a, b, strict = false) {
|
|
403
|
+
return _Equality.#equals(a, b, strict, [], []);
|
|
404
|
+
}
|
|
405
|
+
/** Whether received has every property expected has, recursively, like `toMatchObject`. */
|
|
406
|
+
static matches(received, expected) {
|
|
407
|
+
if (_Equality.asymmetric(expected))
|
|
408
|
+
return expected.asymmetricMatch(received);
|
|
409
|
+
if (!_Equality.#plainish(expected) || typeof received !== "object" || received === null)
|
|
410
|
+
return _Equality.equals(received, expected);
|
|
411
|
+
if (Array.isArray(expected))
|
|
412
|
+
return Array.isArray(received) && received.length === expected.length && expected.every(
|
|
413
|
+
(item, index) => _Equality.matches(received[index], item)
|
|
414
|
+
);
|
|
415
|
+
return _Equality.#keys(expected, true).every(
|
|
416
|
+
(key) => key in received && _Equality.matches(
|
|
417
|
+
received[key],
|
|
418
|
+
expected[key]
|
|
419
|
+
)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
/** Objects and arrays that `toMatchObject` compares by their properties. */
|
|
423
|
+
static #plainish(value) {
|
|
424
|
+
return typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Error);
|
|
425
|
+
}
|
|
426
|
+
static #equals(a, b, strict, aSeen, bSeen) {
|
|
427
|
+
if (_Equality.asymmetric(a)) return a.asymmetricMatch(b);
|
|
428
|
+
if (_Equality.asymmetric(b)) return b.asymmetricMatch(a);
|
|
429
|
+
if (Object.is(a, b)) return true;
|
|
430
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
431
|
+
return false;
|
|
432
|
+
const tag = Object.prototype.toString.call(a);
|
|
433
|
+
if (tag !== Object.prototype.toString.call(b)) return false;
|
|
434
|
+
if (strict && Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
435
|
+
return false;
|
|
436
|
+
const bytes = _Equality.bytes(a);
|
|
437
|
+
if (bytes) return _Equality.#sameBytes(bytes, _Equality.bytes(b));
|
|
438
|
+
if (a instanceof URL) return a.href === b.href;
|
|
439
|
+
if (a instanceof Date)
|
|
440
|
+
return Object.is(a.getTime(), b.getTime());
|
|
441
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
442
|
+
if (a instanceof Error && b instanceof Error)
|
|
443
|
+
return a.name === b.name && a.message === b.message;
|
|
444
|
+
if (a instanceof Number || a instanceof String || a instanceof Boolean)
|
|
445
|
+
return Object.is(
|
|
446
|
+
a.valueOf(),
|
|
447
|
+
b.valueOf()
|
|
448
|
+
);
|
|
449
|
+
const index = aSeen.indexOf(a);
|
|
450
|
+
if (index >= 0) return bSeen[index] === b;
|
|
451
|
+
aSeen.push(a);
|
|
452
|
+
bSeen.push(b);
|
|
453
|
+
try {
|
|
454
|
+
return _Equality.#structures(a, b, strict, aSeen, bSeen);
|
|
455
|
+
} finally {
|
|
456
|
+
aSeen.pop();
|
|
457
|
+
bSeen.pop();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** The bytes of an ArrayBuffer, SharedArrayBuffer or DataView, undefined for other values. */
|
|
461
|
+
static bytes(value) {
|
|
462
|
+
if (value instanceof DataView)
|
|
463
|
+
return new Uint8Array(
|
|
464
|
+
value.buffer,
|
|
465
|
+
value.byteOffset,
|
|
466
|
+
value.byteLength
|
|
467
|
+
);
|
|
468
|
+
if (value instanceof ArrayBuffer || typeof SharedArrayBuffer === "function" && value instanceof SharedArrayBuffer)
|
|
469
|
+
return new Uint8Array(value);
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
static #sameBytes(a, b) {
|
|
473
|
+
return b !== void 0 && a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
474
|
+
}
|
|
475
|
+
static #structures(a, b, strict, aSeen, bSeen) {
|
|
476
|
+
const equal = /* @__PURE__ */ __name((x, y) => _Equality.#equals(x, y, strict, aSeen, bSeen), "equal");
|
|
477
|
+
if (a instanceof Map && b instanceof Map)
|
|
478
|
+
return a.size === b.size && [...a].every(
|
|
479
|
+
([key, value]) => b.has(key) ? equal(value, b.get(key)) : [...b].some(
|
|
480
|
+
([other, item]) => equal(key, other) && equal(value, item)
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
if (a instanceof Set && b instanceof Set)
|
|
484
|
+
return a.size === b.size && [...a].every(
|
|
485
|
+
(value) => b.has(value) || [...b].some((other) => equal(value, other))
|
|
486
|
+
);
|
|
487
|
+
if (Array.isArray(a) || ArrayBuffer.isView(a)) {
|
|
488
|
+
const left = a;
|
|
489
|
+
const right = b;
|
|
490
|
+
if (left.length !== right.length) return false;
|
|
491
|
+
for (let index = 0; index < left.length; index++) {
|
|
492
|
+
if (strict && index in left !== index in right) return false;
|
|
493
|
+
if (!equal(left[index], right[index])) return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const indexed = Array.isArray(a) || ArrayBuffer.isView(a);
|
|
497
|
+
const keysOf = /* @__PURE__ */ __name((object) => _Equality.#keys(object, strict).filter(
|
|
498
|
+
(key) => !(indexed && typeof key === "string" && /^\d+$/.test(key))
|
|
499
|
+
), "keysOf");
|
|
500
|
+
const aKeys = keysOf(a);
|
|
501
|
+
const bKeys = keysOf(b);
|
|
502
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
503
|
+
return aKeys.every(
|
|
504
|
+
(key) => Object.hasOwn(b, key) && equal(
|
|
505
|
+
a[key],
|
|
506
|
+
b[key]
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
/** Own enumerable keys, without the ones set to undefined unless strict. */
|
|
511
|
+
static #keys(object, strict) {
|
|
512
|
+
const record = object;
|
|
513
|
+
const keys = [
|
|
514
|
+
...Object.keys(object),
|
|
515
|
+
...Object.getOwnPropertySymbols(object).filter(
|
|
516
|
+
(symbol) => Object.getOwnPropertyDescriptor(object, symbol)?.enumerable
|
|
517
|
+
)
|
|
518
|
+
];
|
|
519
|
+
return strict ? keys : keys.filter((key) => record[key] !== void 0);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
252
525
|
// packages/testero/src/expect/format.ts
|
|
253
526
|
var Format;
|
|
254
527
|
var init_format = __esm({
|
|
255
528
|
"packages/testero/src/expect/format.ts"() {
|
|
256
529
|
"use strict";
|
|
530
|
+
init_equality();
|
|
257
531
|
Format = class _Format {
|
|
258
532
|
static {
|
|
259
533
|
__name(this, "Format");
|
|
@@ -325,6 +599,7 @@ Received: ${_Format.inline(received)}`;
|
|
|
325
599
|
if (value instanceof Date)
|
|
326
600
|
return Number.isNaN(value.getTime()) ? "Date { NaN }" : value.toISOString();
|
|
327
601
|
if (value instanceof RegExp) return String(value);
|
|
602
|
+
if (value instanceof URL) return `URL ${JSON.stringify(value.href)}`;
|
|
328
603
|
if (value instanceof Error) return `[${value.name}: ${value.message}]`;
|
|
329
604
|
if (seen.includes(value)) return "[Circular]";
|
|
330
605
|
const inner = [...seen, value];
|
|
@@ -348,6 +623,15 @@ Received: ${_Format.inline(received)}`;
|
|
|
348
623
|
indent,
|
|
349
624
|
multiline
|
|
350
625
|
);
|
|
626
|
+
const bytes = Equality.bytes(value);
|
|
627
|
+
if (bytes)
|
|
628
|
+
return _Format.#block(
|
|
629
|
+
`${name ?? "ArrayBuffer"} [`,
|
|
630
|
+
"]",
|
|
631
|
+
Array.from(bytes, String),
|
|
632
|
+
indent,
|
|
633
|
+
multiline
|
|
634
|
+
);
|
|
351
635
|
if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
352
636
|
const items = Array.from(value, print);
|
|
353
637
|
const prefix2 = name && name !== "Array" ? `${name} [` : "[";
|
|
@@ -392,7 +676,11 @@ var init_each = __esm({
|
|
|
392
676
|
*/
|
|
393
677
|
static rows(table, values) {
|
|
394
678
|
if (_Each.#template(table)) {
|
|
395
|
-
const names = table
|
|
679
|
+
const names = (table[0] ?? "").trim().split(/\s*\|\s*/);
|
|
680
|
+
if (values.length % names.length !== 0)
|
|
681
|
+
throw new TypeError(
|
|
682
|
+
`each table has ${names.length} columns (${names.join(", ")}), but ${values.length} values do not fill whole rows`
|
|
683
|
+
);
|
|
396
684
|
const rows = [];
|
|
397
685
|
for (let index = 0; index < values.length; index += names.length)
|
|
398
686
|
rows.push([
|
|
@@ -588,7 +876,8 @@ var init_collector = __esm({
|
|
|
588
876
|
for (const [index, row] of Each.rows(table, values).entries())
|
|
589
877
|
this.#test(
|
|
590
878
|
Each.title(String(name), row, index),
|
|
591
|
-
|
|
879
|
+
// a parameter after the row's values is the done callback
|
|
880
|
+
body.length > row.length ? (done) => body(...row, done) : () => body(...row),
|
|
592
881
|
mode,
|
|
593
882
|
timeout
|
|
594
883
|
);
|
|
@@ -714,9 +1003,14 @@ var init_mock = __esm({
|
|
|
714
1003
|
this.state.instances.push(context);
|
|
715
1004
|
this.state.results.push(result);
|
|
716
1005
|
this.state.lastCall = args;
|
|
1006
|
+
const index = this.state.instances.length - 1;
|
|
717
1007
|
const implementation = this.#once.shift() ?? this.#implementation;
|
|
718
1008
|
try {
|
|
719
1009
|
result.value = !implementation ? void 0 : target ? Reflect.construct(implementation, args, target) : implementation.apply(context, args);
|
|
1010
|
+
if (target && typeof result.value === "object" && result.value) {
|
|
1011
|
+
this.state.instances[index] = result.value;
|
|
1012
|
+
this.state.contexts[index] = result.value;
|
|
1013
|
+
}
|
|
720
1014
|
result.type = "return";
|
|
721
1015
|
return result.value;
|
|
722
1016
|
} catch (error) {
|
|
@@ -788,123 +1082,617 @@ var init_mock = __esm({
|
|
|
788
1082
|
}
|
|
789
1083
|
});
|
|
790
1084
|
|
|
791
|
-
// packages/testero/src/
|
|
792
|
-
var
|
|
793
|
-
var
|
|
794
|
-
"packages/testero/src/
|
|
1085
|
+
// packages/testero/src/globals.ts
|
|
1086
|
+
var Globals;
|
|
1087
|
+
var init_globals = __esm({
|
|
1088
|
+
"packages/testero/src/globals.ts"() {
|
|
795
1089
|
"use strict";
|
|
796
|
-
|
|
1090
|
+
init_mock();
|
|
1091
|
+
Globals = class _Globals {
|
|
797
1092
|
static {
|
|
798
|
-
__name(this, "
|
|
799
|
-
}
|
|
800
|
-
/** Whether a value matches by its own rule, like `expect.any(Number)`. */
|
|
801
|
-
static asymmetric(value) {
|
|
802
|
-
return typeof value === "object" && value !== null && typeof value.asymmetricMatch === "function";
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* Whether two values are deeply equal. Loosely, properties set to
|
|
806
|
-
* undefined count as missing; strictly, types and holes in arrays count.
|
|
807
|
-
*/
|
|
808
|
-
static equals(a, b, strict = false) {
|
|
809
|
-
return _Equality.#equals(a, b, strict, [], []);
|
|
1093
|
+
__name(this, "Globals");
|
|
810
1094
|
}
|
|
811
|
-
|
|
812
|
-
static
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1095
|
+
static key = /* @__PURE__ */ Symbol.for("testero");
|
|
1096
|
+
static names = [
|
|
1097
|
+
"describe",
|
|
1098
|
+
"fdescribe",
|
|
1099
|
+
"xdescribe",
|
|
1100
|
+
"test",
|
|
1101
|
+
"it",
|
|
1102
|
+
"fit",
|
|
1103
|
+
"xit",
|
|
1104
|
+
"xtest",
|
|
1105
|
+
"beforeAll",
|
|
1106
|
+
"afterAll",
|
|
1107
|
+
"beforeEach",
|
|
1108
|
+
"afterEach",
|
|
1109
|
+
"expect",
|
|
1110
|
+
"testero",
|
|
1111
|
+
"jest"
|
|
1112
|
+
];
|
|
1113
|
+
/** The module the transformer injects, it reads the globals of the file being run. */
|
|
1114
|
+
static source() {
|
|
1115
|
+
return [
|
|
1116
|
+
'const globals = globalThis[Symbol.for("testero")];',
|
|
1117
|
+
..._Globals.names.map(
|
|
1118
|
+
(name) => `export const ${name} = globals.${name};`
|
|
825
1119
|
)
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
/** Objects and arrays that `toMatchObject` compares by their properties. */
|
|
829
|
-
static #plainish(value) {
|
|
830
|
-
return typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp) && !(value instanceof Map) && !(value instanceof Set) && !(value instanceof Error);
|
|
1120
|
+
].join("\n");
|
|
831
1121
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1122
|
+
/** Sets the globals for the next test file to import. */
|
|
1123
|
+
static install(collector, expect, settings) {
|
|
1124
|
+
const { describe, test } = collector;
|
|
1125
|
+
const testero = {
|
|
1126
|
+
fn: Mocks.fn,
|
|
1127
|
+
spyOn: Mocks.spyOn,
|
|
1128
|
+
isMockFunction: Mocks.isMockFunction,
|
|
1129
|
+
clearAllMocks: /* @__PURE__ */ __name(() => {
|
|
1130
|
+
Mocks.clearAll();
|
|
1131
|
+
return testero;
|
|
1132
|
+
}, "clearAllMocks"),
|
|
1133
|
+
resetAllMocks: /* @__PURE__ */ __name(() => {
|
|
1134
|
+
Mocks.resetAll();
|
|
1135
|
+
return testero;
|
|
1136
|
+
}, "resetAllMocks"),
|
|
1137
|
+
restoreAllMocks: /* @__PURE__ */ __name(() => {
|
|
1138
|
+
Mocks.restoreAll();
|
|
1139
|
+
return testero;
|
|
1140
|
+
}, "restoreAllMocks"),
|
|
1141
|
+
/** Sets the timeout of the tests and hooks of this file. */
|
|
1142
|
+
setTimeout: /* @__PURE__ */ __name((timeout) => {
|
|
1143
|
+
settings.timeout = timeout;
|
|
1144
|
+
return testero;
|
|
1145
|
+
}, "setTimeout")
|
|
1146
|
+
};
|
|
1147
|
+
globalThis[_Globals.key] = {
|
|
1148
|
+
describe,
|
|
1149
|
+
fdescribe: describe.only,
|
|
1150
|
+
xdescribe: describe.skip,
|
|
1151
|
+
test,
|
|
1152
|
+
it: test,
|
|
1153
|
+
fit: test.only,
|
|
1154
|
+
xit: test.skip,
|
|
1155
|
+
xtest: test.skip,
|
|
1156
|
+
beforeAll: collector.hook("beforeAll"),
|
|
1157
|
+
afterAll: collector.hook("afterAll"),
|
|
1158
|
+
beforeEach: collector.hook("beforeEach"),
|
|
1159
|
+
afterEach: collector.hook("afterEach"),
|
|
1160
|
+
expect: expect.create(),
|
|
1161
|
+
testero,
|
|
1162
|
+
jest: testero
|
|
1163
|
+
};
|
|
862
1164
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// packages/testero/src/transformer.ts
|
|
1170
|
+
import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1171
|
+
import { tmpdir } from "node:os";
|
|
1172
|
+
import { dirname, extname, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1173
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1174
|
+
import { build } from "esbuild";
|
|
1175
|
+
var Transformer;
|
|
1176
|
+
var init_transformer = __esm({
|
|
1177
|
+
"packages/testero/src/transformer.ts"() {
|
|
1178
|
+
"use strict";
|
|
1179
|
+
init_globals();
|
|
1180
|
+
Transformer = class _Transformer {
|
|
1181
|
+
static {
|
|
1182
|
+
__name(this, "Transformer");
|
|
1183
|
+
}
|
|
1184
|
+
/** Packages test files import the globals from, like with Jest. */
|
|
1185
|
+
static modules = ["@jest/globals", "@aljazmc/testero"];
|
|
1186
|
+
static #loaders = {
|
|
1187
|
+
".js": "js",
|
|
1188
|
+
".cjs": "js",
|
|
1189
|
+
".mjs": "js",
|
|
1190
|
+
".jsx": "jsx",
|
|
1191
|
+
".ts": "ts",
|
|
1192
|
+
".cts": "ts",
|
|
1193
|
+
".mts": "ts",
|
|
1194
|
+
".tsx": "tsx"
|
|
1195
|
+
};
|
|
1196
|
+
/** The import.meta properties that describe the source file. */
|
|
1197
|
+
static #meta = ["url", "filename", "dirname"];
|
|
1198
|
+
/** CommonJS globals the ES module bundle lacks, mapped to the same descriptions. */
|
|
1199
|
+
static #commonjs = {
|
|
1200
|
+
__dirname: "dirname",
|
|
1201
|
+
__filename: "filename"
|
|
1202
|
+
};
|
|
1203
|
+
/** Sources that refer to the location of their own file. */
|
|
1204
|
+
static #located = /\bimport\.meta\b|\b__dirname\b|\b__filename\b/;
|
|
1205
|
+
/** Marks the resolutions the plugin asks esbuild for itself. */
|
|
1206
|
+
static #own = /* @__PURE__ */ Symbol("testero");
|
|
1207
|
+
/** Where compiled test files go, stack frames there are testero's own. */
|
|
1208
|
+
static prefix = join2(tmpdir(), "testero-build-");
|
|
1209
|
+
#directory = mkdtempSync(_Transformer.prefix);
|
|
1210
|
+
#globals = join2(this.#directory, "globals.mjs");
|
|
1211
|
+
#count = 0;
|
|
1212
|
+
/** The code of each compiled module by its path, with an inline source map, for coverage. */
|
|
1213
|
+
bundles = /* @__PURE__ */ new Map();
|
|
1214
|
+
#keep;
|
|
1215
|
+
/** Keeps the code of the compiled modules in `bundles` when told. */
|
|
1216
|
+
constructor(keep = false) {
|
|
1217
|
+
this.#keep = keep;
|
|
1218
|
+
writeFileSync2(this.#globals, Globals.source());
|
|
1219
|
+
}
|
|
1220
|
+
/** Bundles a test file, returns the URL of the module to import. */
|
|
1221
|
+
async compile(file) {
|
|
1222
|
+
const outfile = join2(this.#directory, `${++this.#count}.mjs`);
|
|
1223
|
+
const { outputFiles } = await build({
|
|
1224
|
+
...this.#options(file),
|
|
1225
|
+
outfile,
|
|
1226
|
+
sourcemap: "inline",
|
|
1227
|
+
sourcesContent: false,
|
|
1228
|
+
// for coverage, code no test imports counts too
|
|
1229
|
+
treeShaking: !this.#keep
|
|
1230
|
+
});
|
|
1231
|
+
const code = outputFiles[0]?.text ?? "";
|
|
1232
|
+
writeFileSync2(outfile, code);
|
|
1233
|
+
if (this.#keep) this.bundles.set(outfile, code);
|
|
1234
|
+
return pathToFileURL2(outfile).href;
|
|
1235
|
+
}
|
|
1236
|
+
/** The absolute paths of the local files a test file is bundled from, itself included. */
|
|
1237
|
+
async inputs(file) {
|
|
1238
|
+
const { metafile } = await build({
|
|
1239
|
+
...this.#options(file),
|
|
1240
|
+
metafile: true
|
|
1241
|
+
});
|
|
1242
|
+
return Object.keys(metafile.inputs).filter((input) => !/^[\w-]+:/.test(input)).map((input) => resolve2(input)).filter((input) => input !== this.#globals);
|
|
1243
|
+
}
|
|
1244
|
+
/** What compiling and listing the inputs share. */
|
|
1245
|
+
#options(file) {
|
|
1246
|
+
return {
|
|
1247
|
+
entryPoints: [file],
|
|
1248
|
+
write: false,
|
|
1249
|
+
bundle: true,
|
|
1250
|
+
platform: "node",
|
|
1251
|
+
format: "esm",
|
|
1252
|
+
target: `node${process.versions.node.split(".")[0]}`,
|
|
1253
|
+
inject: [this.#globals],
|
|
1254
|
+
// defines replace only unbound names, so a file's own `__dirname` stays
|
|
1255
|
+
define: Object.fromEntries([
|
|
1256
|
+
..._Transformer.#meta.map((name) => [
|
|
1257
|
+
`import.meta.${name}`,
|
|
1258
|
+
`__testero.${name}`
|
|
1259
|
+
]),
|
|
1260
|
+
...Object.entries(_Transformer.#commonjs).map(
|
|
1261
|
+
([name, property2]) => [name, `__testero.${property2}`]
|
|
869
1262
|
)
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1263
|
+
]),
|
|
1264
|
+
// CommonJS code in the bundle may require installed packages
|
|
1265
|
+
banner: {
|
|
1266
|
+
js: 'import { createRequire as __testeroRequire } from "node:module";\nconst require = __testeroRequire(import.meta.url);'
|
|
1267
|
+
},
|
|
1268
|
+
logLevel: "silent",
|
|
1269
|
+
plugins: [this.#plugin()]
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
/** The esbuild loader of a source file, undefined for files it does not compile. */
|
|
1273
|
+
static loader(path) {
|
|
1274
|
+
return _Transformer.#loaders[extname(path)];
|
|
1275
|
+
}
|
|
1276
|
+
/** Removes the compiled files. */
|
|
1277
|
+
dispose() {
|
|
1278
|
+
rmSync(this.#directory, { recursive: true, force: true });
|
|
1279
|
+
}
|
|
1280
|
+
#plugin() {
|
|
1281
|
+
return {
|
|
1282
|
+
name: "testero",
|
|
1283
|
+
setup: /* @__PURE__ */ __name((build2) => {
|
|
1284
|
+
build2.onResolve({ filter: /^testero-meta:/ }, (args) => ({
|
|
1285
|
+
path: args.path.slice("testero-meta:".length),
|
|
1286
|
+
namespace: "testero-meta"
|
|
1287
|
+
}));
|
|
1288
|
+
build2.onLoad(
|
|
1289
|
+
{ filter: /.*/, namespace: "testero-meta" },
|
|
1290
|
+
(args) => ({
|
|
1291
|
+
contents: `export default ${JSON.stringify({
|
|
1292
|
+
url: pathToFileURL2(args.path).href,
|
|
1293
|
+
filename: args.path,
|
|
1294
|
+
dirname: dirname(args.path)
|
|
1295
|
+
})};`,
|
|
1296
|
+
loader: "js"
|
|
1297
|
+
})
|
|
1298
|
+
);
|
|
1299
|
+
build2.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
1300
|
+
if (args.pluginData === _Transformer.#own) return void 0;
|
|
1301
|
+
if (_Transformer.modules.includes(args.path))
|
|
1302
|
+
return { path: this.#globals };
|
|
1303
|
+
const resolved = await build2.resolve(args.path, {
|
|
1304
|
+
kind: args.kind,
|
|
1305
|
+
resolveDir: args.resolveDir,
|
|
1306
|
+
pluginData: _Transformer.#own
|
|
1307
|
+
});
|
|
1308
|
+
if (resolved.errors.length > 0)
|
|
1309
|
+
return { errors: resolved.errors };
|
|
1310
|
+
if (resolved.external)
|
|
1311
|
+
return { path: args.path, external: true };
|
|
1312
|
+
if (!_Transformer.#installed(resolved.path))
|
|
1313
|
+
return { path: resolved.path };
|
|
1314
|
+
return {
|
|
1315
|
+
path: args.kind === "require-call" ? resolved.path : pathToFileURL2(resolved.path).href,
|
|
1316
|
+
external: true
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
build2.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
1320
|
+
if (_Transformer.#installed(args.path)) return void 0;
|
|
1321
|
+
const source = readFileSync2(args.path, "utf8");
|
|
1322
|
+
return {
|
|
1323
|
+
contents: _Transformer.#locate(source, args.path),
|
|
1324
|
+
loader: _Transformer.#loaders[extname(args.path)] ?? "js",
|
|
1325
|
+
resolveDir: dirname(args.path)
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
}, "setup")
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* A source with `__testero` describing its file, when it refers to it.
|
|
1333
|
+
* Modules import it after their last line, as imports are hoisted.
|
|
1334
|
+
* CommonJS files, without import or export statements, require it at the
|
|
1335
|
+
* start of their first line instead, as an import would make them modules
|
|
1336
|
+
* and their `module.exports` would be lost.
|
|
1337
|
+
*/
|
|
1338
|
+
static #locate(source, path) {
|
|
1339
|
+
if (!_Transformer.#located.test(source)) return source;
|
|
1340
|
+
const meta = JSON.stringify(`testero-meta:${path}`);
|
|
1341
|
+
if (/\.m[jt]s$/.test(path) || /^\s*(?:import\s*[\w{*"']|export\b)/m.test(source))
|
|
1342
|
+
return `${source}
|
|
1343
|
+
import __testero from ${meta};
|
|
1344
|
+
`;
|
|
1345
|
+
const start = source.startsWith("#!") ? source.indexOf("\n") + 1 : 0;
|
|
1346
|
+
if (source.startsWith("#!") && start === 0) return source;
|
|
1347
|
+
return `${source.slice(0, start)}var __testero = require(${meta}).default;${source.slice(start)}`;
|
|
1348
|
+
}
|
|
1349
|
+
static #installed(path) {
|
|
1350
|
+
return path.split(sep2).includes("node_modules");
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// packages/testero/src/coverage.ts
|
|
1357
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1358
|
+
import { Session } from "node:inspector/promises";
|
|
1359
|
+
import { dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
1360
|
+
import { fileURLToPath } from "node:url";
|
|
1361
|
+
import { transform } from "esbuild";
|
|
1362
|
+
var Coverage;
|
|
1363
|
+
var init_coverage = __esm({
|
|
1364
|
+
"packages/testero/src/coverage.ts"() {
|
|
1365
|
+
"use strict";
|
|
1366
|
+
init_transformer();
|
|
1367
|
+
Coverage = class _Coverage {
|
|
1368
|
+
static {
|
|
1369
|
+
__name(this, "Coverage");
|
|
1370
|
+
}
|
|
1371
|
+
static #digits = (() => {
|
|
1372
|
+
const digits = new Int8Array(128).fill(-1);
|
|
1373
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1374
|
+
for (let index = 0; index < alphabet.length; index++)
|
|
1375
|
+
digits[alphabet.charCodeAt(index)] = index;
|
|
1376
|
+
return digits;
|
|
1377
|
+
})();
|
|
1378
|
+
static #inline = /\/\/# sourceMappingURL=data:application\/json;base64,([\w+/=]+)\s*$/;
|
|
1379
|
+
#root;
|
|
1380
|
+
#tracks;
|
|
1381
|
+
/** What runs within this one took, which reset the counts of this one. */
|
|
1382
|
+
#taken = [];
|
|
1383
|
+
#receive = /* @__PURE__ */ __name((result) => this.#taken.push(result), "#receive");
|
|
1384
|
+
/** Reports the files, relative to the root, that `tracks` takes. */
|
|
1385
|
+
constructor(root, tracks) {
|
|
1386
|
+
this.#root = root;
|
|
1387
|
+
this.#tracks = tracks;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* The runs measuring coverage, a run within a run, like testero testing
|
|
1391
|
+
* itself, among them, and their session. Starting coverage clears the
|
|
1392
|
+
* counts of calls and taking it resets all counts, of the whole process,
|
|
1393
|
+
* so the runs share one session, started once, and each gets what any
|
|
1394
|
+
* of the others takes. Kept on globalThis, as nested runs may come from
|
|
1395
|
+
* different copies of this module.
|
|
1396
|
+
*/
|
|
1397
|
+
static #shared() {
|
|
1398
|
+
const key = /* @__PURE__ */ Symbol.for("@aljazmc/testero.coverage");
|
|
1399
|
+
const global = globalThis;
|
|
1400
|
+
global[key] ??= { session: void 0, runs: /* @__PURE__ */ new Set() };
|
|
1401
|
+
return global[key];
|
|
1402
|
+
}
|
|
1403
|
+
/** Starts counting, before the tests load. */
|
|
1404
|
+
async start() {
|
|
1405
|
+
const shared = _Coverage.#shared();
|
|
1406
|
+
if (!shared.session) {
|
|
1407
|
+
const session = new Session();
|
|
1408
|
+
session.connect();
|
|
1409
|
+
await session.post("Profiler.enable");
|
|
1410
|
+
await session.post("Profiler.startPreciseCoverage", {
|
|
1411
|
+
callCount: true,
|
|
1412
|
+
detailed: true
|
|
1413
|
+
});
|
|
1414
|
+
shared.session = session;
|
|
1415
|
+
}
|
|
1416
|
+
shared.runs.add(this.#receive);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* The coverage of the tracked sources of the bundles, by their paths,
|
|
1420
|
+
* and of the files no test loaded, sorted by path.
|
|
1421
|
+
*/
|
|
1422
|
+
async collect(bundles, unloaded = []) {
|
|
1423
|
+
const shared = _Coverage.#shared();
|
|
1424
|
+
const session = shared.session;
|
|
1425
|
+
if (!session || !shared.runs.delete(this.#receive))
|
|
1426
|
+
throw new Error("Coverage was not started.");
|
|
1427
|
+
const { result } = await session.post(
|
|
1428
|
+
"Profiler.takePreciseCoverage"
|
|
1429
|
+
);
|
|
1430
|
+
for (const receive of shared.runs) receive(result);
|
|
1431
|
+
if (shared.runs.size === 0) {
|
|
1432
|
+
session.disconnect();
|
|
1433
|
+
shared.session = void 0;
|
|
1434
|
+
}
|
|
1435
|
+
const files = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const taken of [...this.#taken, result])
|
|
1437
|
+
for (const script of taken) {
|
|
1438
|
+
const path = script.url.startsWith("file:") ? fileURLToPath(script.url) : script.url;
|
|
1439
|
+
const code = bundles.get(path);
|
|
1440
|
+
if (code !== void 0)
|
|
1441
|
+
this.#bundle(code, dirname2(path), script.functions, files);
|
|
1442
|
+
}
|
|
1443
|
+
for (const file of unloaded)
|
|
1444
|
+
if (!files.has(file)) files.set(file, await this.#unloaded(file));
|
|
1445
|
+
return [...files.values()].sort(
|
|
1446
|
+
(a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
/** Adds the counts of the sources of one bundle. */
|
|
1450
|
+
#bundle(code, directory, functions, files) {
|
|
1451
|
+
const inline = _Coverage.#inline.exec(code)?.[1];
|
|
1452
|
+
if (inline === void 0) return;
|
|
1453
|
+
const map = JSON.parse(Buffer.from(inline, "base64").toString());
|
|
1454
|
+
const sources = map.sources.map((source) => {
|
|
1455
|
+
const file = relative2(this.#root, resolve3(directory, source)).split("\\").join("/");
|
|
1456
|
+
if (!this.#tracks(file)) return void 0;
|
|
1457
|
+
let coverage = files.get(file);
|
|
1458
|
+
if (!coverage) {
|
|
1459
|
+
coverage = {
|
|
1460
|
+
file,
|
|
1461
|
+
lines: /* @__PURE__ */ new Map(),
|
|
1462
|
+
functions: /* @__PURE__ */ new Map(),
|
|
1463
|
+
loaded: true
|
|
1464
|
+
};
|
|
1465
|
+
files.set(file, coverage);
|
|
882
1466
|
}
|
|
1467
|
+
return coverage;
|
|
1468
|
+
});
|
|
1469
|
+
if (sources.every((source) => source === void 0)) return;
|
|
1470
|
+
const counts = new Float64Array(code.length);
|
|
1471
|
+
const ranges = functions.flatMap((fn) => fn.ranges).sort(
|
|
1472
|
+
(a, b) => b.endOffset - b.startOffset - (a.endOffset - a.startOffset)
|
|
1473
|
+
);
|
|
1474
|
+
for (const range of ranges)
|
|
1475
|
+
counts.fill(range.count, range.startOffset, range.endOffset);
|
|
1476
|
+
const starts = [0];
|
|
1477
|
+
for (let index = 0; index < code.length; index++)
|
|
1478
|
+
if (code.charCodeAt(index) === 10) starts.push(index + 1);
|
|
1479
|
+
const segments = _Coverage.decode(map.mappings);
|
|
1480
|
+
const offsets = segments.map(
|
|
1481
|
+
(segment) => (starts[segment.line] ?? code.length) + segment.column
|
|
1482
|
+
);
|
|
1483
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1484
|
+
segments.forEach((segment, index) => {
|
|
1485
|
+
const coverage = sources[segment.source];
|
|
1486
|
+
if (!coverage) return;
|
|
1487
|
+
const next = segments[index + 1];
|
|
1488
|
+
const end = next?.line === segment.line ? offsets[index + 1] : starts[segment.line + 1] ?? code.length;
|
|
1489
|
+
let offset = offsets[index];
|
|
1490
|
+
while (offset < end && /\s/.test(code[offset])) offset++;
|
|
1491
|
+
if (offset >= end) return;
|
|
1492
|
+
const counted = lines.get(coverage) ?? /* @__PURE__ */ new Map();
|
|
1493
|
+
lines.set(coverage, counted);
|
|
1494
|
+
const line = segment.sourceLine + 1;
|
|
1495
|
+
const count = counts[offset] ?? 0;
|
|
1496
|
+
counted.set(line, Math.max(counted.get(line) ?? 0, count));
|
|
1497
|
+
});
|
|
1498
|
+
for (const [coverage, counted] of lines)
|
|
1499
|
+
for (const [line, count] of counted)
|
|
1500
|
+
coverage.lines.set(
|
|
1501
|
+
line,
|
|
1502
|
+
(coverage.lines.get(line) ?? 0) + count
|
|
1503
|
+
);
|
|
1504
|
+
for (const { functionName, ranges: ranges2 } of functions) {
|
|
1505
|
+
const range = ranges2[0];
|
|
1506
|
+
if (!range || range.startOffset === 0 || functionName.startsWith("<"))
|
|
1507
|
+
continue;
|
|
1508
|
+
const index = _Coverage.#before(offsets, range.startOffset) ?? -1;
|
|
1509
|
+
const segment = segments[index];
|
|
1510
|
+
const line = starts[_Coverage.#before(starts, range.startOffset) ?? 0];
|
|
1511
|
+
if (!segment || offsets[index] < (line ?? 0)) continue;
|
|
1512
|
+
const coverage = sources[segment.source];
|
|
1513
|
+
if (!coverage) continue;
|
|
1514
|
+
const key = `${segment.sourceLine + 1}:${segment.sourceColumn + 1}`;
|
|
1515
|
+
const known = coverage.functions.get(key);
|
|
1516
|
+
if (known) known.count += range.count;
|
|
1517
|
+
else
|
|
1518
|
+
coverage.functions.set(key, {
|
|
1519
|
+
name: functionName,
|
|
1520
|
+
line: segment.sourceLine + 1,
|
|
1521
|
+
count: range.count
|
|
1522
|
+
});
|
|
883
1523
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1524
|
+
}
|
|
1525
|
+
/** A file no test loaded: its lines with code, none of them run. */
|
|
1526
|
+
async #unloaded(file) {
|
|
1527
|
+
const coverage = {
|
|
1528
|
+
file,
|
|
1529
|
+
lines: /* @__PURE__ */ new Map(),
|
|
1530
|
+
functions: /* @__PURE__ */ new Map(),
|
|
1531
|
+
loaded: false
|
|
1532
|
+
};
|
|
1533
|
+
const path = join3(this.#root, file);
|
|
1534
|
+
const loader = Transformer.loader(path);
|
|
1535
|
+
if (!loader) return coverage;
|
|
1536
|
+
try {
|
|
1537
|
+
const { map } = await transform(readFileSync3(path, "utf8"), {
|
|
1538
|
+
loader,
|
|
1539
|
+
sourcemap: "external",
|
|
1540
|
+
sourcefile: file
|
|
1541
|
+
});
|
|
1542
|
+
const { mappings } = JSON.parse(map);
|
|
1543
|
+
for (const segment of _Coverage.decode(mappings))
|
|
1544
|
+
coverage.lines.set(segment.sourceLine + 1, 0);
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
return coverage;
|
|
1548
|
+
}
|
|
1549
|
+
/** The index of the last of the sorted numbers at or before a number. */
|
|
1550
|
+
static #before(sorted, value) {
|
|
1551
|
+
let low = 0;
|
|
1552
|
+
let high = sorted.length - 1;
|
|
1553
|
+
let found;
|
|
1554
|
+
while (low <= high) {
|
|
1555
|
+
const middle = low + high >> 1;
|
|
1556
|
+
if (sorted[middle] <= value) {
|
|
1557
|
+
found = middle;
|
|
1558
|
+
low = middle + 1;
|
|
1559
|
+
} else high = middle - 1;
|
|
1560
|
+
}
|
|
1561
|
+
return found;
|
|
1562
|
+
}
|
|
1563
|
+
/** The mappings of a source map with a source, in order, its base64 VLQs decoded. */
|
|
1564
|
+
static decode(mappings) {
|
|
1565
|
+
const segments = [];
|
|
1566
|
+
let line = 0;
|
|
1567
|
+
let column = 0;
|
|
1568
|
+
let source = 0;
|
|
1569
|
+
let sourceLine = 0;
|
|
1570
|
+
let sourceColumn = 0;
|
|
1571
|
+
let fields = [];
|
|
1572
|
+
let value = 0;
|
|
1573
|
+
let shift = 0;
|
|
1574
|
+
for (let index = 0; index <= mappings.length; index++) {
|
|
1575
|
+
const code = mappings.charCodeAt(index);
|
|
1576
|
+
if (Number.isNaN(code) || code === 44 || code === 59) {
|
|
1577
|
+
if (fields.length > 0) {
|
|
1578
|
+
column += fields[0];
|
|
1579
|
+
if (fields.length >= 4) {
|
|
1580
|
+
source += fields[1];
|
|
1581
|
+
sourceLine += fields[2];
|
|
1582
|
+
sourceColumn += fields[3];
|
|
1583
|
+
segments.push({
|
|
1584
|
+
line,
|
|
1585
|
+
column,
|
|
1586
|
+
source,
|
|
1587
|
+
sourceLine,
|
|
1588
|
+
sourceColumn
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
fields = [];
|
|
1592
|
+
}
|
|
1593
|
+
if (code === 59) {
|
|
1594
|
+
line++;
|
|
1595
|
+
column = 0;
|
|
1596
|
+
}
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
const digit = _Coverage.#digits[code] ?? -1;
|
|
1600
|
+
if (digit < 0)
|
|
1601
|
+
throw new Error(
|
|
1602
|
+
`The source map has "${mappings[index]}" in its mappings.`
|
|
1603
|
+
);
|
|
1604
|
+
value += (digit & 31) * 2 ** shift;
|
|
1605
|
+
if (digit & 32) shift += 5;
|
|
1606
|
+
else {
|
|
1607
|
+
const magnitude = Math.floor(value / 2);
|
|
1608
|
+
fields.push(value % 2 === 1 ? -magnitude : magnitude);
|
|
1609
|
+
value = 0;
|
|
1610
|
+
shift = 0;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return segments;
|
|
1614
|
+
}
|
|
1615
|
+
/** How many of the lines with code ran. */
|
|
1616
|
+
static lines(coverage) {
|
|
1617
|
+
const counts = [...coverage.lines.values()];
|
|
1618
|
+
return {
|
|
1619
|
+
covered: counts.filter((count) => count > 0).length,
|
|
1620
|
+
total: counts.length
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/** How many of the functions ran. */
|
|
1624
|
+
static functions(coverage) {
|
|
1625
|
+
const counts = [...coverage.functions.values()];
|
|
1626
|
+
return {
|
|
1627
|
+
covered: counts.filter(({ count }) => count > 0).length,
|
|
1628
|
+
total: counts.length
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/** The sums of ratios. */
|
|
1632
|
+
static sum(ratios) {
|
|
1633
|
+
return ratios.reduce(
|
|
1634
|
+
(sum, ratio) => ({
|
|
1635
|
+
covered: sum.covered + ratio.covered,
|
|
1636
|
+
total: sum.total + ratio.total
|
|
1637
|
+
}),
|
|
1638
|
+
{ covered: 0, total: 0 }
|
|
896
1639
|
);
|
|
897
1640
|
}
|
|
898
|
-
/**
|
|
899
|
-
static
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
];
|
|
907
|
-
|
|
1641
|
+
/** A percentage like Jest's, 100 when there is nothing to run, unless nothing loaded it. */
|
|
1642
|
+
static percent(ratio, loaded = true) {
|
|
1643
|
+
if (ratio.total === 0) return loaded ? 100 : 0;
|
|
1644
|
+
return Math.floor(ratio.covered / ratio.total * 1e4) / 100;
|
|
1645
|
+
}
|
|
1646
|
+
/** The lines that did not run, in ranges like `3-5,9`. */
|
|
1647
|
+
static uncovered(coverage) {
|
|
1648
|
+
const lines = [...coverage.lines].filter(([, count]) => count === 0).map(([line]) => line).sort((a, b) => a - b);
|
|
1649
|
+
const ranges = [];
|
|
1650
|
+
for (let index = 0; index < lines.length; ) {
|
|
1651
|
+
const first = lines[index];
|
|
1652
|
+
let last = first;
|
|
1653
|
+
while (lines[index + 1] === last + 1)
|
|
1654
|
+
last = lines[++index];
|
|
1655
|
+
ranges.push(first === last ? `${first}` : `${first}-${last}`);
|
|
1656
|
+
index++;
|
|
1657
|
+
}
|
|
1658
|
+
return ranges.join(",");
|
|
1659
|
+
}
|
|
1660
|
+
/** The coverage in the lcov format editors and services read. */
|
|
1661
|
+
static lcov(root, files) {
|
|
1662
|
+
return files.map((coverage) => {
|
|
1663
|
+
const records = ["TN:", `SF:${join3(root, coverage.file)}`];
|
|
1664
|
+
const functions = [...coverage.functions.values()].sort(
|
|
1665
|
+
(a, b) => a.line - b.line
|
|
1666
|
+
);
|
|
1667
|
+
const names = /* @__PURE__ */ new Map();
|
|
1668
|
+
const named = functions.map((fn) => {
|
|
1669
|
+
const base = fn.name || "(anonymous)";
|
|
1670
|
+
const seen = names.get(base) ?? 0;
|
|
1671
|
+
names.set(base, seen + 1);
|
|
1672
|
+
return {
|
|
1673
|
+
...fn,
|
|
1674
|
+
name: seen === 0 ? base : `${base}_${seen}`
|
|
1675
|
+
};
|
|
1676
|
+
});
|
|
1677
|
+
for (const fn of named)
|
|
1678
|
+
records.push(`FN:${fn.line},${fn.name}`);
|
|
1679
|
+
for (const fn of named)
|
|
1680
|
+
records.push(`FNDA:${fn.count},${fn.name}`);
|
|
1681
|
+
const ratio = _Coverage.functions(coverage);
|
|
1682
|
+
records.push(`FNF:${ratio.total}`, `FNH:${ratio.covered}`);
|
|
1683
|
+
for (const [line, count] of [...coverage.lines].sort(
|
|
1684
|
+
([a], [b]) => a - b
|
|
1685
|
+
))
|
|
1686
|
+
records.push(`DA:${line},${count}`);
|
|
1687
|
+
const lines = _Coverage.lines(coverage);
|
|
1688
|
+
records.push(
|
|
1689
|
+
`LF:${lines.total}`,
|
|
1690
|
+
`LH:${lines.covered}`,
|
|
1691
|
+
"end_of_record"
|
|
1692
|
+
);
|
|
1693
|
+
return `${records.join("\n")}
|
|
1694
|
+
`;
|
|
1695
|
+
}).join("");
|
|
908
1696
|
}
|
|
909
1697
|
};
|
|
910
1698
|
}
|
|
@@ -1375,46 +2163,47 @@ var init_assertion = __esm({
|
|
|
1375
2163
|
}
|
|
1376
2164
|
/** The same assertion, expecting the opposite. */
|
|
1377
2165
|
get not() {
|
|
1378
|
-
return
|
|
1379
|
-
...this.#context,
|
|
1380
|
-
isNot: !this.#context.isNot
|
|
1381
|
-
});
|
|
2166
|
+
return this.#with({ isNot: !this.#context.isNot });
|
|
1382
2167
|
}
|
|
1383
2168
|
/** The assertion on what a promise resolves to, its matchers return promises. */
|
|
1384
2169
|
get resolves() {
|
|
1385
|
-
return
|
|
1386
|
-
...this.#context,
|
|
1387
|
-
promise: "resolves"
|
|
1388
|
-
});
|
|
2170
|
+
return this.#with({ promise: "resolves" });
|
|
1389
2171
|
}
|
|
1390
2172
|
/** The assertion on why a promise rejects, its matchers return promises. */
|
|
1391
2173
|
get rejects() {
|
|
1392
|
-
return
|
|
2174
|
+
return this.#with({ promise: "rejects" });
|
|
2175
|
+
}
|
|
2176
|
+
/** The same received value with another context, of the same class to keep its matchers. */
|
|
2177
|
+
#with(change) {
|
|
2178
|
+
const Same = this.constructor;
|
|
2179
|
+
return new Same(this.#received, {
|
|
1393
2180
|
...this.#context,
|
|
1394
|
-
|
|
2181
|
+
...change
|
|
1395
2182
|
});
|
|
1396
2183
|
}
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
2184
|
+
/**
|
|
2185
|
+
* Adds a matcher to the assertions of a class: Assertion itself for the
|
|
2186
|
+
* built in ones, the class of one test file for `expect.extend`.
|
|
2187
|
+
*/
|
|
2188
|
+
static define(target, name, matcher) {
|
|
2189
|
+
Object.defineProperty(target.prototype, name, {
|
|
1401
2190
|
configurable: true,
|
|
1402
2191
|
writable: true,
|
|
1403
2192
|
value: /* @__PURE__ */ __name(function check(...args) {
|
|
1404
|
-
return this.#run(name, args, check);
|
|
2193
|
+
return this.#run(name, matcher, args, check);
|
|
1405
2194
|
}, "check")
|
|
1406
2195
|
});
|
|
1407
2196
|
}
|
|
1408
2197
|
/** Runs a matcher now, or once the promise settles for `.resolves` and `.rejects`. */
|
|
1409
|
-
#run(name, args, caller) {
|
|
2198
|
+
#run(name, matcher, args, caller) {
|
|
1410
2199
|
_Assertion.count++;
|
|
1411
2200
|
const origin = {};
|
|
1412
2201
|
Error.captureStackTrace(origin, caller);
|
|
1413
2202
|
const { promise } = this.#context;
|
|
1414
|
-
if (!promise) return this.#check(
|
|
2203
|
+
if (!promise) return this.#check(matcher, this.#received, args, origin);
|
|
1415
2204
|
const settled = /* @__PURE__ */ __name((value, resolved) => {
|
|
1416
2205
|
if (resolved === (promise === "resolves"))
|
|
1417
|
-
return this.#check(
|
|
2206
|
+
return this.#check(matcher, value, args, origin);
|
|
1418
2207
|
throw _Assertion.#error(
|
|
1419
2208
|
`expect(received).${promise}.${name}()
|
|
1420
2209
|
|
|
@@ -1435,19 +2224,34 @@ Matcher error: received value must be a promise`,
|
|
|
1435
2224
|
(reason) => settled(reason, false)
|
|
1436
2225
|
);
|
|
1437
2226
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
2227
|
+
/** Calls a matcher, and fails when it does not pass, after awaiting an async one. */
|
|
2228
|
+
#check(matcher, received, args, origin) {
|
|
2229
|
+
const failed = /* @__PURE__ */ __name((error) => _Assertion.#error(
|
|
2230
|
+
error instanceof Error ? error.message : String(error),
|
|
2231
|
+
origin
|
|
2232
|
+
), "failed");
|
|
1440
2233
|
let result;
|
|
1441
2234
|
try {
|
|
1442
2235
|
result = matcher.call(this.#context, received, ...args);
|
|
1443
2236
|
} catch (error) {
|
|
2237
|
+
throw failed(error);
|
|
2238
|
+
}
|
|
2239
|
+
if (typeof result?.then === "function")
|
|
2240
|
+
return Promise.resolve(result).then(
|
|
2241
|
+
(settled) => this.#verdict(settled, origin),
|
|
2242
|
+
(error) => {
|
|
2243
|
+
throw failed(error);
|
|
2244
|
+
}
|
|
2245
|
+
);
|
|
2246
|
+
return this.#verdict(result, origin);
|
|
2247
|
+
}
|
|
2248
|
+
/** Throws when a matcher result is not what the assertion expects. */
|
|
2249
|
+
#verdict(result, origin) {
|
|
2250
|
+
if (Boolean(result?.pass) === this.#context.isNot)
|
|
1444
2251
|
throw _Assertion.#error(
|
|
1445
|
-
|
|
2252
|
+
typeof result?.message === "function" ? String(result.message()) : "The matcher did not return a result with pass and message.",
|
|
1446
2253
|
origin
|
|
1447
2254
|
);
|
|
1448
|
-
}
|
|
1449
|
-
if (result.pass === this.#context.isNot)
|
|
1450
|
-
throw _Assertion.#error(result.message(), origin);
|
|
1451
2255
|
}
|
|
1452
2256
|
static #error(message, origin) {
|
|
1453
2257
|
const error = new AssertionError(message);
|
|
@@ -1458,7 +2262,7 @@ ${frames}` : ""}`;
|
|
|
1458
2262
|
}
|
|
1459
2263
|
};
|
|
1460
2264
|
for (const [name, matcher] of Object.entries(matchers))
|
|
1461
|
-
Assertion.define(name, matcher);
|
|
2265
|
+
Assertion.define(Assertion, name, matcher);
|
|
1462
2266
|
}
|
|
1463
2267
|
});
|
|
1464
2268
|
|
|
@@ -1587,308 +2391,97 @@ var init_asymmetric = __esm({
|
|
|
1587
2391
|
if (other === this.sample) return true;
|
|
1588
2392
|
return Math.abs(this.sample - other) < 10 ** -this.#digits / 2;
|
|
1589
2393
|
}
|
|
1590
|
-
toString() {
|
|
1591
|
-
return `Number${this.prefix}CloseTo ${this.sample} (${this.#digits} digits)`;
|
|
1592
|
-
}
|
|
1593
|
-
};
|
|
1594
|
-
}
|
|
1595
|
-
});
|
|
1596
|
-
|
|
1597
|
-
// packages/testero/src/expect/expect.ts
|
|
1598
|
-
var negatable, Expect;
|
|
1599
|
-
var init_expect = __esm({
|
|
1600
|
-
"packages/testero/src/expect/expect.ts"() {
|
|
1601
|
-
"use strict";
|
|
1602
|
-
init_assertion();
|
|
1603
|
-
init_asymmetric();
|
|
1604
|
-
negatable = /* @__PURE__ */ __name((inverse) => ({
|
|
1605
|
-
objectContaining: /* @__PURE__ */ __name((object) => new ObjectContaining(object, inverse), "objectContaining"),
|
|
1606
|
-
arrayContaining: /* @__PURE__ */ __name((array) => new ArrayContaining(array, inverse), "arrayContaining"),
|
|
1607
|
-
stringContaining: /* @__PURE__ */ __name((text) => new StringContaining(text, inverse), "stringContaining"),
|
|
1608
|
-
stringMatching: /* @__PURE__ */ __name((pattern) => new StringMatching(new RegExp(pattern), inverse), "stringMatching"),
|
|
1609
|
-
closeTo: /* @__PURE__ */ __name((number, digits) => new CloseTo(number, digits, inverse), "closeTo")
|
|
1610
|
-
}), "negatable");
|
|
1611
|
-
Expect = class {
|
|
1612
|
-
static {
|
|
1613
|
-
__name(this, "Expect");
|
|
1614
|
-
}
|
|
1615
|
-
#expected;
|
|
1616
|
-
#needed = false;
|
|
1617
|
-
/** Starts the counts of a new test. */
|
|
1618
|
-
reset() {
|
|
1619
|
-
Assertion.count = 0;
|
|
1620
|
-
this.#expected = void 0;
|
|
1621
|
-
this.#needed = false;
|
|
1622
|
-
}
|
|
1623
|
-
/** Failures of `expect.assertions(n)` and `expect.hasAssertions()` after a test. */
|
|
1624
|
-
verify() {
|
|
1625
|
-
const count = Assertion.count;
|
|
1626
|
-
if (this.#expected !== void 0 && count !== this.#expected)
|
|
1627
|
-
return [
|
|
1628
|
-
new AssertionError(
|
|
1629
|
-
`expect.assertions(${this.#expected})
|
|
1630
|
-
|
|
1631
|
-
Expected ${this.#expected} assertion${this.#expected === 1 ? "" : "s"} to be called but received ${count} assertion call${count === 1 ? "" : "s"}.`
|
|
1632
|
-
)
|
|
1633
|
-
];
|
|
1634
|
-
if (this.#needed && count === 0)
|
|
1635
|
-
return [
|
|
1636
|
-
new AssertionError(
|
|
1637
|
-
"expect.hasAssertions()\n\nExpected at least one assertion to be called but received none."
|
|
1638
|
-
)
|
|
1639
|
-
];
|
|
1640
|
-
return [];
|
|
1641
|
-
}
|
|
1642
|
-
/** The function test files call as `expect`. */
|
|
1643
|
-
create() {
|
|
1644
|
-
const expect = /* @__PURE__ */ __name((received) => new Assertion(received), "expect");
|
|
1645
|
-
return Object.assign(expect, {
|
|
1646
|
-
...negatable(false),
|
|
1647
|
-
not: negatable(true),
|
|
1648
|
-
any: /* @__PURE__ */ __name((type) => new Any(type), "any"),
|
|
1649
|
-
anything: /* @__PURE__ */ __name(() => new Anything(void 0), "anything"),
|
|
1650
|
-
assertions: /* @__PURE__ */ __name((count) => {
|
|
1651
|
-
this.#expected = count;
|
|
1652
|
-
}, "assertions"),
|
|
1653
|
-
hasAssertions: /* @__PURE__ */ __name(() => {
|
|
1654
|
-
this.#needed = true;
|
|
1655
|
-
}, "hasAssertions"),
|
|
1656
|
-
extend: /* @__PURE__ */ __name((added) => {
|
|
1657
|
-
for (const [name, matcher] of Object.entries(added))
|
|
1658
|
-
Assertion.define(name, matcher);
|
|
1659
|
-
}, "extend")
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
});
|
|
1665
|
-
|
|
1666
|
-
// packages/testero/src/globals.ts
|
|
1667
|
-
var Globals;
|
|
1668
|
-
var init_globals = __esm({
|
|
1669
|
-
"packages/testero/src/globals.ts"() {
|
|
1670
|
-
"use strict";
|
|
1671
|
-
init_mock();
|
|
1672
|
-
Globals = class _Globals {
|
|
1673
|
-
static {
|
|
1674
|
-
__name(this, "Globals");
|
|
1675
|
-
}
|
|
1676
|
-
static key = /* @__PURE__ */ Symbol.for("testero");
|
|
1677
|
-
static names = [
|
|
1678
|
-
"describe",
|
|
1679
|
-
"fdescribe",
|
|
1680
|
-
"xdescribe",
|
|
1681
|
-
"test",
|
|
1682
|
-
"it",
|
|
1683
|
-
"fit",
|
|
1684
|
-
"xit",
|
|
1685
|
-
"xtest",
|
|
1686
|
-
"beforeAll",
|
|
1687
|
-
"afterAll",
|
|
1688
|
-
"beforeEach",
|
|
1689
|
-
"afterEach",
|
|
1690
|
-
"expect",
|
|
1691
|
-
"testero",
|
|
1692
|
-
"jest"
|
|
1693
|
-
];
|
|
1694
|
-
/** The module the transformer injects, it reads the globals of the file being run. */
|
|
1695
|
-
static source() {
|
|
1696
|
-
return [
|
|
1697
|
-
'const globals = globalThis[Symbol.for("testero")];',
|
|
1698
|
-
..._Globals.names.map(
|
|
1699
|
-
(name) => `export const ${name} = globals.${name};`
|
|
1700
|
-
)
|
|
1701
|
-
].join("\n");
|
|
1702
|
-
}
|
|
1703
|
-
/** Sets the globals for the next test file to import. */
|
|
1704
|
-
static install(collector, expect, settings) {
|
|
1705
|
-
const { describe, test } = collector;
|
|
1706
|
-
const testero = {
|
|
1707
|
-
fn: Mocks.fn,
|
|
1708
|
-
spyOn: Mocks.spyOn,
|
|
1709
|
-
isMockFunction: Mocks.isMockFunction,
|
|
1710
|
-
clearAllMocks: /* @__PURE__ */ __name(() => {
|
|
1711
|
-
Mocks.clearAll();
|
|
1712
|
-
return testero;
|
|
1713
|
-
}, "clearAllMocks"),
|
|
1714
|
-
resetAllMocks: /* @__PURE__ */ __name(() => {
|
|
1715
|
-
Mocks.resetAll();
|
|
1716
|
-
return testero;
|
|
1717
|
-
}, "resetAllMocks"),
|
|
1718
|
-
restoreAllMocks: /* @__PURE__ */ __name(() => {
|
|
1719
|
-
Mocks.restoreAll();
|
|
1720
|
-
return testero;
|
|
1721
|
-
}, "restoreAllMocks"),
|
|
1722
|
-
/** Sets the timeout of the tests and hooks of this file. */
|
|
1723
|
-
setTimeout: /* @__PURE__ */ __name((timeout) => {
|
|
1724
|
-
settings.timeout = timeout;
|
|
1725
|
-
return testero;
|
|
1726
|
-
}, "setTimeout")
|
|
1727
|
-
};
|
|
1728
|
-
globalThis[_Globals.key] = {
|
|
1729
|
-
describe,
|
|
1730
|
-
fdescribe: describe.only,
|
|
1731
|
-
xdescribe: describe.skip,
|
|
1732
|
-
test,
|
|
1733
|
-
it: test,
|
|
1734
|
-
fit: test.only,
|
|
1735
|
-
xit: test.skip,
|
|
1736
|
-
xtest: test.skip,
|
|
1737
|
-
beforeAll: collector.hook("beforeAll"),
|
|
1738
|
-
afterAll: collector.hook("afterAll"),
|
|
1739
|
-
beforeEach: collector.hook("beforeEach"),
|
|
1740
|
-
afterEach: collector.hook("afterEach"),
|
|
1741
|
-
expect: expect.create(),
|
|
1742
|
-
testero,
|
|
1743
|
-
jest: testero
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
});
|
|
1749
|
-
|
|
1750
|
-
// packages/testero/src/transformer.ts
|
|
1751
|
-
import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1752
|
-
import { tmpdir } from "node:os";
|
|
1753
|
-
import { dirname, extname, join as join2, sep } from "node:path";
|
|
1754
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1755
|
-
import { build } from "esbuild";
|
|
1756
|
-
var Transformer;
|
|
1757
|
-
var init_transformer = __esm({
|
|
1758
|
-
"packages/testero/src/transformer.ts"() {
|
|
2394
|
+
toString() {
|
|
2395
|
+
return `Number${this.prefix}CloseTo ${this.sample} (${this.#digits} digits)`;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
|
|
2401
|
+
// packages/testero/src/expect/expect.ts
|
|
2402
|
+
var negatable, Expect;
|
|
2403
|
+
var init_expect = __esm({
|
|
2404
|
+
"packages/testero/src/expect/expect.ts"() {
|
|
1759
2405
|
"use strict";
|
|
1760
|
-
|
|
1761
|
-
|
|
2406
|
+
init_assertion();
|
|
2407
|
+
init_asymmetric();
|
|
2408
|
+
negatable = /* @__PURE__ */ __name((inverse) => ({
|
|
2409
|
+
objectContaining: /* @__PURE__ */ __name((object) => new ObjectContaining(object, inverse), "objectContaining"),
|
|
2410
|
+
arrayContaining: /* @__PURE__ */ __name((array) => new ArrayContaining(array, inverse), "arrayContaining"),
|
|
2411
|
+
stringContaining: /* @__PURE__ */ __name((text) => new StringContaining(text, inverse), "stringContaining"),
|
|
2412
|
+
stringMatching: /* @__PURE__ */ __name((pattern) => new StringMatching(new RegExp(pattern), inverse), "stringMatching"),
|
|
2413
|
+
closeTo: /* @__PURE__ */ __name((number, digits) => new CloseTo(number, digits, inverse), "closeTo")
|
|
2414
|
+
}), "negatable");
|
|
2415
|
+
Expect = class {
|
|
1762
2416
|
static {
|
|
1763
|
-
__name(this, "
|
|
2417
|
+
__name(this, "Expect");
|
|
1764
2418
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
".ts": "ts",
|
|
1773
|
-
".cts": "ts",
|
|
1774
|
-
".mts": "ts",
|
|
1775
|
-
".tsx": "tsx"
|
|
2419
|
+
#expected;
|
|
2420
|
+
#needed = false;
|
|
2421
|
+
/** The assertions of this test file, `expect.extend` adds matchers to them only. */
|
|
2422
|
+
#Assertion = class extends Assertion {
|
|
2423
|
+
static {
|
|
2424
|
+
__name(this, "#Assertion");
|
|
2425
|
+
}
|
|
1776
2426
|
};
|
|
1777
|
-
/**
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
static prefix = join2(tmpdir(), "testero-build-");
|
|
1783
|
-
#directory = mkdtempSync(_Transformer.prefix);
|
|
1784
|
-
#globals = join2(this.#directory, "globals.mjs");
|
|
1785
|
-
#count = 0;
|
|
1786
|
-
constructor() {
|
|
1787
|
-
writeFileSync2(this.#globals, Globals.source());
|
|
1788
|
-
}
|
|
1789
|
-
/** Bundles a test file, returns the URL of the module to import. */
|
|
1790
|
-
async compile(file) {
|
|
1791
|
-
const outfile = join2(this.#directory, `${++this.#count}.mjs`);
|
|
1792
|
-
const { outputFiles } = await build({
|
|
1793
|
-
entryPoints: [file],
|
|
1794
|
-
outfile,
|
|
1795
|
-
write: false,
|
|
1796
|
-
bundle: true,
|
|
1797
|
-
platform: "node",
|
|
1798
|
-
format: "esm",
|
|
1799
|
-
target: `node${process.versions.node.split(".")[0]}`,
|
|
1800
|
-
sourcemap: "inline",
|
|
1801
|
-
sourcesContent: false,
|
|
1802
|
-
inject: [this.#globals],
|
|
1803
|
-
define: Object.fromEntries(
|
|
1804
|
-
_Transformer.#meta.map((name) => [
|
|
1805
|
-
`import.meta.${name}`,
|
|
1806
|
-
`__testero.${name}`
|
|
1807
|
-
])
|
|
1808
|
-
),
|
|
1809
|
-
// CommonJS code in the bundle may require installed packages
|
|
1810
|
-
banner: {
|
|
1811
|
-
js: 'import { createRequire as __testeroRequire } from "node:module";\nconst require = __testeroRequire(import.meta.url);'
|
|
1812
|
-
},
|
|
1813
|
-
logLevel: "silent",
|
|
1814
|
-
plugins: [this.#plugin()]
|
|
1815
|
-
});
|
|
1816
|
-
writeFileSync2(outfile, outputFiles[0]?.text ?? "");
|
|
1817
|
-
return pathToFileURL2(outfile).href;
|
|
1818
|
-
}
|
|
1819
|
-
/** Removes the compiled files. */
|
|
1820
|
-
dispose() {
|
|
1821
|
-
rmSync(this.#directory, { recursive: true, force: true });
|
|
2427
|
+
/** Starts the counts of a new test. */
|
|
2428
|
+
reset() {
|
|
2429
|
+
Assertion.count = 0;
|
|
2430
|
+
this.#expected = void 0;
|
|
2431
|
+
this.#needed = false;
|
|
1822
2432
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
);
|
|
1842
|
-
build2.onResolve({ filter: /^[^./]/ }, async (args) => {
|
|
1843
|
-
if (args.pluginData === _Transformer.#own) return void 0;
|
|
1844
|
-
if (_Transformer.modules.includes(args.path))
|
|
1845
|
-
return { path: this.#globals };
|
|
1846
|
-
const resolved = await build2.resolve(args.path, {
|
|
1847
|
-
kind: args.kind,
|
|
1848
|
-
resolveDir: args.resolveDir,
|
|
1849
|
-
pluginData: _Transformer.#own
|
|
1850
|
-
});
|
|
1851
|
-
if (resolved.errors.length > 0)
|
|
1852
|
-
return { errors: resolved.errors };
|
|
1853
|
-
if (resolved.external)
|
|
1854
|
-
return { path: args.path, external: true };
|
|
1855
|
-
if (!_Transformer.#installed(resolved.path))
|
|
1856
|
-
return { path: resolved.path };
|
|
1857
|
-
return {
|
|
1858
|
-
path: args.kind === "require-call" ? resolved.path : pathToFileURL2(resolved.path).href,
|
|
1859
|
-
external: true
|
|
1860
|
-
};
|
|
1861
|
-
});
|
|
1862
|
-
build2.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
|
|
1863
|
-
if (_Transformer.#installed(args.path)) return void 0;
|
|
1864
|
-
const source = readFileSync2(args.path, "utf8");
|
|
1865
|
-
const contents = /\bimport\.meta\b/.test(source) ? `${source}
|
|
1866
|
-
import __testero from ${JSON.stringify(`testero-meta:${args.path}`)};
|
|
1867
|
-
` : source;
|
|
1868
|
-
return {
|
|
1869
|
-
contents,
|
|
1870
|
-
loader: _Transformer.#loaders[extname(args.path)] ?? "js",
|
|
1871
|
-
resolveDir: dirname(args.path)
|
|
1872
|
-
};
|
|
1873
|
-
});
|
|
1874
|
-
}, "setup")
|
|
1875
|
-
};
|
|
2433
|
+
/** Failures of `expect.assertions(n)` and `expect.hasAssertions()` after a test. */
|
|
2434
|
+
verify() {
|
|
2435
|
+
const count = Assertion.count;
|
|
2436
|
+
if (this.#expected !== void 0 && count !== this.#expected)
|
|
2437
|
+
return [
|
|
2438
|
+
new AssertionError(
|
|
2439
|
+
`expect.assertions(${this.#expected})
|
|
2440
|
+
|
|
2441
|
+
Expected ${this.#expected} assertion${this.#expected === 1 ? "" : "s"} to be called but received ${count} assertion call${count === 1 ? "" : "s"}.`
|
|
2442
|
+
)
|
|
2443
|
+
];
|
|
2444
|
+
if (this.#needed && count === 0)
|
|
2445
|
+
return [
|
|
2446
|
+
new AssertionError(
|
|
2447
|
+
"expect.hasAssertions()\n\nExpected at least one assertion to be called but received none."
|
|
2448
|
+
)
|
|
2449
|
+
];
|
|
2450
|
+
return [];
|
|
1876
2451
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
2452
|
+
/** The function test files call as `expect`. */
|
|
2453
|
+
create() {
|
|
2454
|
+
const expect = /* @__PURE__ */ __name((received) => new this.#Assertion(received), "expect");
|
|
2455
|
+
return Object.assign(expect, {
|
|
2456
|
+
...negatable(false),
|
|
2457
|
+
not: negatable(true),
|
|
2458
|
+
any: /* @__PURE__ */ __name((type) => new Any(type), "any"),
|
|
2459
|
+
anything: /* @__PURE__ */ __name(() => new Anything(void 0), "anything"),
|
|
2460
|
+
assertions: /* @__PURE__ */ __name((count) => {
|
|
2461
|
+
this.#expected = count;
|
|
2462
|
+
}, "assertions"),
|
|
2463
|
+
hasAssertions: /* @__PURE__ */ __name(() => {
|
|
2464
|
+
this.#needed = true;
|
|
2465
|
+
}, "hasAssertions"),
|
|
2466
|
+
extend: /* @__PURE__ */ __name((added) => {
|
|
2467
|
+
for (const [name, matcher] of Object.entries(added))
|
|
2468
|
+
Assertion.define(this.#Assertion, name, matcher);
|
|
2469
|
+
}, "extend")
|
|
2470
|
+
});
|
|
1879
2471
|
}
|
|
1880
2472
|
};
|
|
1881
2473
|
}
|
|
1882
2474
|
});
|
|
1883
2475
|
|
|
1884
2476
|
// packages/testero/src/reporter.ts
|
|
1885
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1886
|
-
import { dirname as
|
|
1887
|
-
import { fileURLToPath } from "node:url";
|
|
2477
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
2478
|
+
import { dirname as dirname3, isAbsolute, relative as relative3 } from "node:path";
|
|
2479
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1888
2480
|
var Reporter;
|
|
1889
2481
|
var init_reporter = __esm({
|
|
1890
2482
|
"packages/testero/src/reporter.ts"() {
|
|
1891
2483
|
"use strict";
|
|
2484
|
+
init_coverage();
|
|
1892
2485
|
init_assertion();
|
|
1893
2486
|
init_format();
|
|
1894
2487
|
init_transformer();
|
|
@@ -1907,7 +2500,7 @@ var init_reporter = __esm({
|
|
|
1907
2500
|
gray: 90
|
|
1908
2501
|
};
|
|
1909
2502
|
/** Stack frames from testero itself are left out of failures. */
|
|
1910
|
-
static #own =
|
|
2503
|
+
static #own = dirname3(fileURLToPath2(import.meta.url));
|
|
1911
2504
|
#root;
|
|
1912
2505
|
#verbose;
|
|
1913
2506
|
#color;
|
|
@@ -1985,6 +2578,66 @@ ${failed ? "\n" : ""}`);
|
|
|
1985
2578
|
].join("\n")
|
|
1986
2579
|
);
|
|
1987
2580
|
}
|
|
2581
|
+
/** The coverage of each file in a table like Jest's, under the totals. */
|
|
2582
|
+
coverage(files) {
|
|
2583
|
+
const row = /* @__PURE__ */ __name((name, functions, lines, uncovered) => [
|
|
2584
|
+
name,
|
|
2585
|
+
String(functions),
|
|
2586
|
+
String(lines),
|
|
2587
|
+
uncovered.length > 40 ? `${uncovered.slice(0, 37)}...` : uncovered
|
|
2588
|
+
], "row");
|
|
2589
|
+
const header = ["File", "% Funcs", "% Lines", "Uncovered Line #s"];
|
|
2590
|
+
const rows = [
|
|
2591
|
+
row(
|
|
2592
|
+
"All files",
|
|
2593
|
+
Coverage.percent(Coverage.sum(files.map(Coverage.functions))),
|
|
2594
|
+
Coverage.percent(Coverage.sum(files.map(Coverage.lines))),
|
|
2595
|
+
""
|
|
2596
|
+
),
|
|
2597
|
+
...files.map(
|
|
2598
|
+
(coverage) => row(
|
|
2599
|
+
` ${coverage.file}`,
|
|
2600
|
+
Coverage.percent(
|
|
2601
|
+
Coverage.functions(coverage),
|
|
2602
|
+
coverage.loaded
|
|
2603
|
+
),
|
|
2604
|
+
Coverage.percent(Coverage.lines(coverage), coverage.loaded),
|
|
2605
|
+
Coverage.uncovered(coverage)
|
|
2606
|
+
)
|
|
2607
|
+
)
|
|
2608
|
+
];
|
|
2609
|
+
const widths = header.map(
|
|
2610
|
+
(title, column) => Math.max(
|
|
2611
|
+
title.length,
|
|
2612
|
+
...rows.map((cells) => cells[column]?.length ?? 0)
|
|
2613
|
+
)
|
|
2614
|
+
);
|
|
2615
|
+
const line = /* @__PURE__ */ __name((cells, paint) => cells.map((cell, column) => {
|
|
2616
|
+
const width = widths[column] ?? 0;
|
|
2617
|
+
if (column === 1 || column === 2) {
|
|
2618
|
+
const padded = cell.padStart(width);
|
|
2619
|
+
if (!paint) return padded;
|
|
2620
|
+
const percent = Number(cell);
|
|
2621
|
+
return this.#paint(
|
|
2622
|
+
padded,
|
|
2623
|
+
percent >= 80 ? "green" : percent >= 50 ? "yellow" : "red"
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
return cell.padEnd(width);
|
|
2627
|
+
}).join(" | "), "line");
|
|
2628
|
+
const rule = widths.map((width) => "-".repeat(width)).join("-|-");
|
|
2629
|
+
this.#write(
|
|
2630
|
+
`${[
|
|
2631
|
+
"",
|
|
2632
|
+
rule,
|
|
2633
|
+
line(header, false),
|
|
2634
|
+
rule,
|
|
2635
|
+
...rows.map((cells) => line(cells, true)),
|
|
2636
|
+
rule
|
|
2637
|
+
].join("\n")}
|
|
2638
|
+
`
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
1988
2641
|
/** Explains that no test files were found. */
|
|
1989
2642
|
none(patterns) {
|
|
1990
2643
|
this.#write(
|
|
@@ -2053,7 +2706,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2053
2706
|
const match = /^\s+at (?:.*?\()?(.+?):(\d+):(\d+)\)?$/.exec(line);
|
|
2054
2707
|
if (!match) continue;
|
|
2055
2708
|
let file = match[1];
|
|
2056
|
-
if (file.startsWith("file://")) file =
|
|
2709
|
+
if (file.startsWith("file://")) file = fileURLToPath2(file);
|
|
2057
2710
|
if (!isAbsolute(file) || file.includes("node_modules") || file.startsWith(_Reporter.#own) || file.startsWith(Transformer.prefix))
|
|
2058
2711
|
continue;
|
|
2059
2712
|
frames.push({
|
|
@@ -2067,7 +2720,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2067
2720
|
/** The lines around a position, with a mark under it. */
|
|
2068
2721
|
#code({ file, line, column }) {
|
|
2069
2722
|
if (!existsSync2(file)) return "";
|
|
2070
|
-
const lines =
|
|
2723
|
+
const lines = readFileSync4(file, "utf8").split(/\r?\n/);
|
|
2071
2724
|
const first = Math.max(1, line - 2);
|
|
2072
2725
|
const last = Math.min(lines.length, line + 2);
|
|
2073
2726
|
const width = String(last).length;
|
|
@@ -2087,7 +2740,7 @@ ${failed ? "\n" : ""}`);
|
|
|
2087
2740
|
return out.join("\n");
|
|
2088
2741
|
}
|
|
2089
2742
|
#path(file) {
|
|
2090
|
-
const path =
|
|
2743
|
+
const path = relative3(this.#root, file);
|
|
2091
2744
|
return path.startsWith("..") ? file : path;
|
|
2092
2745
|
}
|
|
2093
2746
|
#paint(text, ...styles) {
|
|
@@ -2114,6 +2767,8 @@ var init_runner = __esm({
|
|
|
2114
2767
|
}
|
|
2115
2768
|
#settings;
|
|
2116
2769
|
#focused = false;
|
|
2770
|
+
/** Fails the test or hook that is running, if any. */
|
|
2771
|
+
#abort;
|
|
2117
2772
|
constructor(settings) {
|
|
2118
2773
|
this.#settings = settings;
|
|
2119
2774
|
}
|
|
@@ -2125,6 +2780,15 @@ var init_runner = __esm({
|
|
|
2125
2780
|
await this.#suite(root, [], results, errors);
|
|
2126
2781
|
return { results, errors };
|
|
2127
2782
|
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Fails the running test or hook with an error thrown outside of it, like
|
|
2785
|
+
* from a timer it started. Returns false when nothing is running.
|
|
2786
|
+
*/
|
|
2787
|
+
fail(error) {
|
|
2788
|
+
if (!this.#abort) return false;
|
|
2789
|
+
this.#abort(error);
|
|
2790
|
+
return true;
|
|
2791
|
+
}
|
|
2128
2792
|
/** Whether a test runs: not skipped, focused when others are, matching the name pattern. */
|
|
2129
2793
|
runs(test) {
|
|
2130
2794
|
return !test.skipped && (!this.#focused || test.focused) && (this.#settings.namePattern?.test(test.fullName) ?? true);
|
|
@@ -2202,6 +2866,7 @@ var init_runner = __esm({
|
|
|
2202
2866
|
async #call(body, timeout = this.#settings.timeout) {
|
|
2203
2867
|
let timer;
|
|
2204
2868
|
const expired = new Promise((_, reject) => {
|
|
2869
|
+
this.#abort = reject;
|
|
2205
2870
|
timer = setTimeout(
|
|
2206
2871
|
() => reject(
|
|
2207
2872
|
new Error(
|
|
@@ -2217,17 +2882,18 @@ var init_runner = __esm({
|
|
|
2217
2882
|
} catch (error) {
|
|
2218
2883
|
return error ?? new Error(`${String(error)} was thrown`);
|
|
2219
2884
|
} finally {
|
|
2885
|
+
this.#abort = void 0;
|
|
2220
2886
|
clearTimeout(timer);
|
|
2221
2887
|
}
|
|
2222
2888
|
}
|
|
2223
2889
|
/** Runs a body as a promise, with a `done` callback when it takes one. */
|
|
2224
2890
|
static #invoke(body) {
|
|
2225
2891
|
if (body.length === 0)
|
|
2226
|
-
return new Promise((
|
|
2892
|
+
return new Promise((resolve4) => resolve4(body(() => {
|
|
2227
2893
|
})));
|
|
2228
|
-
return new Promise((
|
|
2894
|
+
return new Promise((resolve4, reject) => {
|
|
2229
2895
|
body(
|
|
2230
|
-
(error) => error === void 0 ?
|
|
2896
|
+
(error) => error === void 0 ? resolve4(void 0) : reject(error)
|
|
2231
2897
|
);
|
|
2232
2898
|
});
|
|
2233
2899
|
}
|
|
@@ -2236,20 +2902,22 @@ var init_runner = __esm({
|
|
|
2236
2902
|
});
|
|
2237
2903
|
|
|
2238
2904
|
// packages/testero/src/testero.ts
|
|
2239
|
-
import {
|
|
2905
|
+
import { mkdirSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2906
|
+
import { isAbsolute as isAbsolute2, join as join4 } from "node:path";
|
|
2240
2907
|
var Testero;
|
|
2241
2908
|
var init_testero = __esm({
|
|
2242
2909
|
"packages/testero/src/testero.ts"() {
|
|
2243
2910
|
"use strict";
|
|
2244
2911
|
init_src();
|
|
2245
2912
|
init_collector();
|
|
2913
|
+
init_coverage();
|
|
2246
2914
|
init_expect();
|
|
2247
2915
|
init_globals();
|
|
2248
2916
|
init_mock();
|
|
2249
2917
|
init_reporter();
|
|
2250
2918
|
init_runner();
|
|
2251
2919
|
init_transformer();
|
|
2252
|
-
Testero = class {
|
|
2920
|
+
Testero = class _Testero {
|
|
2253
2921
|
static {
|
|
2254
2922
|
__name(this, "Testero");
|
|
2255
2923
|
}
|
|
@@ -2258,6 +2926,10 @@ var init_testero = __esm({
|
|
|
2258
2926
|
#reporter;
|
|
2259
2927
|
/** Errors thrown outside of tests, like from a forgotten timer. */
|
|
2260
2928
|
#stray = [];
|
|
2929
|
+
/** The runner of the file being run, which fails its running test with stray errors. */
|
|
2930
|
+
#runner;
|
|
2931
|
+
/** The test files and collectCoverageFrom, made when coverage first needs them. */
|
|
2932
|
+
#coverageIncludes;
|
|
2261
2933
|
constructor(config, options, reporter = new Reporter(config.root, { verbose: options.verbose })) {
|
|
2262
2934
|
this.#config = config;
|
|
2263
2935
|
this.#options = options;
|
|
@@ -2265,7 +2937,7 @@ var init_testero = __esm({
|
|
|
2265
2937
|
}
|
|
2266
2938
|
/** The test files to run, relative to the root. */
|
|
2267
2939
|
files() {
|
|
2268
|
-
const includes = new Includes(this.#options.testMatch);
|
|
2940
|
+
const includes = new Includes(this.#options.testMatch, "testMatch");
|
|
2269
2941
|
const patterns = this.#options.patterns.map(
|
|
2270
2942
|
(pattern) => new RegExp(pattern)
|
|
2271
2943
|
);
|
|
@@ -2281,11 +2953,18 @@ var init_testero = __esm({
|
|
|
2281
2953
|
this.#reporter.none(this.#options.patterns);
|
|
2282
2954
|
return this.#options.passWithNoTests;
|
|
2283
2955
|
}
|
|
2956
|
+
const coverage = this.#options.collectCoverage ? new Coverage(this.#config.root, (file) => this.#tracks(file)) : void 0;
|
|
2957
|
+
await coverage?.start();
|
|
2284
2958
|
process.setSourceMapsEnabled(true);
|
|
2285
|
-
const
|
|
2959
|
+
const runs = _Testero.#runs();
|
|
2960
|
+
const stray = /* @__PURE__ */ __name((error) => {
|
|
2961
|
+
if (runs.at(-1) !== stray) return;
|
|
2962
|
+
if (!this.#runner?.fail(error)) this.#stray.push(error);
|
|
2963
|
+
}, "stray");
|
|
2964
|
+
runs.push(stray);
|
|
2286
2965
|
process.on("uncaughtException", stray);
|
|
2287
2966
|
process.on("unhandledRejection", stray);
|
|
2288
|
-
const transformer = new Transformer();
|
|
2967
|
+
const transformer = new Transformer(coverage !== void 0);
|
|
2289
2968
|
const results = [];
|
|
2290
2969
|
try {
|
|
2291
2970
|
for (const file of files) {
|
|
@@ -2298,20 +2977,68 @@ var init_testero = __esm({
|
|
|
2298
2977
|
transformer.dispose();
|
|
2299
2978
|
process.off("uncaughtException", stray);
|
|
2300
2979
|
process.off("unhandledRejection", stray);
|
|
2980
|
+
runs.splice(runs.indexOf(stray), 1);
|
|
2981
|
+
}
|
|
2982
|
+
if (coverage) {
|
|
2983
|
+
const covered = await coverage.collect(
|
|
2984
|
+
transformer.bundles,
|
|
2985
|
+
this.#options.collectCoverageFrom.length > 0 ? [...this.#config.files()].filter(
|
|
2986
|
+
(file) => Transformer.loader(file) !== void 0 && this.#tracks(file)
|
|
2987
|
+
) : []
|
|
2988
|
+
);
|
|
2989
|
+
this.#reporter.coverage(covered);
|
|
2990
|
+
const directory = join4(
|
|
2991
|
+
this.#config.root,
|
|
2992
|
+
this.#options.coverageDirectory
|
|
2993
|
+
);
|
|
2994
|
+
mkdirSync(directory, { recursive: true });
|
|
2995
|
+
writeFileSync3(
|
|
2996
|
+
join4(directory, "lcov.info"),
|
|
2997
|
+
Coverage.lcov(this.#config.root, covered)
|
|
2998
|
+
);
|
|
2301
2999
|
}
|
|
2302
3000
|
this.#reporter.summary(results, performance.now() - start);
|
|
2303
3001
|
return !results.some(Reporter.failed);
|
|
2304
3002
|
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Whether coverage is reported for a file, relative to the root: one of
|
|
3005
|
+
* the project, not a test file nor installed, and in collectCoverageFrom
|
|
3006
|
+
* when it is set.
|
|
3007
|
+
*/
|
|
3008
|
+
#tracks(file) {
|
|
3009
|
+
if (file.startsWith("../") || isAbsolute2(file)) return false;
|
|
3010
|
+
if (this.#config.ignore.matches(file)) return false;
|
|
3011
|
+
if (file.split("/").includes("node_modules")) return false;
|
|
3012
|
+
this.#coverageIncludes ??= {
|
|
3013
|
+
tests: new Includes(this.#options.testMatch, "testMatch"),
|
|
3014
|
+
from: this.#options.collectCoverageFrom.length > 0 ? new Includes(
|
|
3015
|
+
this.#options.collectCoverageFrom,
|
|
3016
|
+
"collectCoverageFrom"
|
|
3017
|
+
) : void 0
|
|
3018
|
+
};
|
|
3019
|
+
const { tests, from } = this.#coverageIncludes;
|
|
3020
|
+
return !tests.matches(file) && (from?.matches(file) ?? true);
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* The stray error listeners of the runs in progress, innermost last.
|
|
3024
|
+
* Kept on globalThis, as nested runs may come from different copies of this module.
|
|
3025
|
+
*/
|
|
3026
|
+
static #runs() {
|
|
3027
|
+
const key = /* @__PURE__ */ Symbol.for("@aljazmc/testero.runs");
|
|
3028
|
+
const global = globalThis;
|
|
3029
|
+
global[key] ??= [];
|
|
3030
|
+
return global[key];
|
|
3031
|
+
}
|
|
2305
3032
|
async #file(file, transformer) {
|
|
2306
3033
|
const start = performance.now();
|
|
2307
|
-
const finish = /* @__PURE__ */ __name((
|
|
2308
|
-
|
|
3034
|
+
const finish = /* @__PURE__ */ __name((results, errors) => {
|
|
3035
|
+
errors.push(...this.#stray.splice(0));
|
|
2309
3036
|
Mocks.restoreAll();
|
|
2310
3037
|
Mocks.forget();
|
|
2311
3038
|
return {
|
|
2312
3039
|
file,
|
|
2313
|
-
results
|
|
2314
|
-
errors
|
|
3040
|
+
results,
|
|
3041
|
+
errors,
|
|
2315
3042
|
duration: performance.now() - start
|
|
2316
3043
|
};
|
|
2317
3044
|
}, "finish");
|
|
@@ -2330,7 +3057,7 @@ var init_testero = __esm({
|
|
|
2330
3057
|
};
|
|
2331
3058
|
Globals.install(collector, expect, settings);
|
|
2332
3059
|
try {
|
|
2333
|
-
await import(await transformer.compile(
|
|
3060
|
+
await import(await transformer.compile(join4(this.#config.root, file)));
|
|
2334
3061
|
} catch (error) {
|
|
2335
3062
|
return finish([], [error]);
|
|
2336
3063
|
} finally {
|
|
@@ -2341,10 +3068,13 @@ var init_testero = __esm({
|
|
|
2341
3068
|
[],
|
|
2342
3069
|
[new Error("Your test suite must contain at least one test.")]
|
|
2343
3070
|
);
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3071
|
+
this.#runner = new Runner(settings);
|
|
3072
|
+
try {
|
|
3073
|
+
const { results, errors } = await this.#runner.run(collector.root);
|
|
3074
|
+
return finish(results, errors);
|
|
3075
|
+
} finally {
|
|
3076
|
+
this.#runner = void 0;
|
|
3077
|
+
}
|
|
2348
3078
|
}
|
|
2349
3079
|
};
|
|
2350
3080
|
}
|
|
@@ -2370,6 +3100,9 @@ Options:
|
|
|
2370
3100
|
--listTests print the test files instead of running them
|
|
2371
3101
|
--passWithNoTests succeed when there are no test files
|
|
2372
3102
|
--testTimeout <ms> milliseconds a test or hook may take
|
|
3103
|
+
--coverage report which lines and functions the tests run
|
|
3104
|
+
--watch rerun the tests of changed files and what imports them
|
|
3105
|
+
--watchAll rerun every test when a file changes
|
|
2373
3106
|
--config [js] write the default testero.config.ts, or .js
|
|
2374
3107
|
-h, --help show this help
|
|
2375
3108
|
`;
|
|
@@ -2378,9 +3111,14 @@ Options:
|
|
|
2378
3111
|
namePattern;
|
|
2379
3112
|
listTests = false;
|
|
2380
3113
|
help = false;
|
|
3114
|
+
/** Reruns the tests of changed files, or all of them, when files change. */
|
|
3115
|
+
watch;
|
|
3116
|
+
/** The options, without the patterns and the watch ones, for the runs of watch mode. */
|
|
3117
|
+
flags = [];
|
|
2381
3118
|
constructor(argv) {
|
|
2382
3119
|
for (let index = 0; index < argv.length; index++) {
|
|
2383
3120
|
const argument = argv[index];
|
|
3121
|
+
const start = index;
|
|
2384
3122
|
const [name, inline] = argument.split(/=(.*)/s);
|
|
2385
3123
|
const value = /* @__PURE__ */ __name(() => {
|
|
2386
3124
|
const next = inline ?? argv[++index];
|
|
@@ -2393,14 +3131,32 @@ Options:
|
|
|
2393
3131
|
case "--testNamePattern":
|
|
2394
3132
|
this.namePattern = new RegExp(value());
|
|
2395
3133
|
break;
|
|
2396
|
-
case "--testTimeout":
|
|
2397
|
-
|
|
3134
|
+
case "--testTimeout": {
|
|
3135
|
+
const text = value();
|
|
3136
|
+
const timeout = Number(text);
|
|
3137
|
+
const valid = timeout > 0 && timeout < 2 ** 31;
|
|
3138
|
+
if (text.trim() === "" || !valid)
|
|
3139
|
+
throw new Error(
|
|
3140
|
+
`--testTimeout needs a positive number of milliseconds, not "${text}".`
|
|
3141
|
+
);
|
|
3142
|
+
this.rules.testTimeout = timeout;
|
|
2398
3143
|
break;
|
|
3144
|
+
}
|
|
2399
3145
|
case "--bail":
|
|
2400
3146
|
case "--verbose":
|
|
2401
3147
|
case "--passWithNoTests":
|
|
2402
3148
|
this.rules[name.slice(2)] = true;
|
|
2403
3149
|
break;
|
|
3150
|
+
case "--coverage":
|
|
3151
|
+
case "--collectCoverage":
|
|
3152
|
+
this.rules.collectCoverage = true;
|
|
3153
|
+
break;
|
|
3154
|
+
case "--watch":
|
|
3155
|
+
this.watch ??= "related";
|
|
3156
|
+
continue;
|
|
3157
|
+
case "--watchAll":
|
|
3158
|
+
this.watch = "all";
|
|
3159
|
+
continue;
|
|
2404
3160
|
case "--listTests":
|
|
2405
3161
|
this.listTests = true;
|
|
2406
3162
|
break;
|
|
@@ -2412,7 +3168,9 @@ Options:
|
|
|
2412
3168
|
if (name.startsWith("-"))
|
|
2413
3169
|
throw new Error(`Unknown option ${name}.`);
|
|
2414
3170
|
this.patterns.push(argument);
|
|
3171
|
+
continue;
|
|
2415
3172
|
}
|
|
3173
|
+
this.flags.push(...argv.slice(start, index + 1));
|
|
2416
3174
|
}
|
|
2417
3175
|
}
|
|
2418
3176
|
/** The configured rules, with the ones given on the command line instead. */
|
|
@@ -2445,7 +3203,10 @@ var init_config2 = __esm({
|
|
|
2445
3203
|
passWithNoTests: false,
|
|
2446
3204
|
clearMocks: false,
|
|
2447
3205
|
resetMocks: false,
|
|
2448
|
-
restoreMocks: false
|
|
3206
|
+
restoreMocks: false,
|
|
3207
|
+
collectCoverage: false,
|
|
3208
|
+
collectCoverageFrom: [],
|
|
3209
|
+
coverageDirectory: "coverage"
|
|
2449
3210
|
};
|
|
2450
3211
|
config_default = {
|
|
2451
3212
|
ignore: [".git", "node_modules"],
|
|
@@ -2454,8 +3215,166 @@ var init_config2 = __esm({
|
|
|
2454
3215
|
}
|
|
2455
3216
|
});
|
|
2456
3217
|
|
|
3218
|
+
// packages/testero/src/watcher.ts
|
|
3219
|
+
var Watcher, Dependencies;
|
|
3220
|
+
var init_watcher = __esm({
|
|
3221
|
+
"packages/testero/src/watcher.ts"() {
|
|
3222
|
+
"use strict";
|
|
3223
|
+
Watcher = class _Watcher {
|
|
3224
|
+
static {
|
|
3225
|
+
__name(this, "Watcher");
|
|
3226
|
+
}
|
|
3227
|
+
#options;
|
|
3228
|
+
#write;
|
|
3229
|
+
#current;
|
|
3230
|
+
#queued;
|
|
3231
|
+
#last = "all";
|
|
3232
|
+
#changed = /* @__PURE__ */ new Set();
|
|
3233
|
+
#timer;
|
|
3234
|
+
#unsubscribe;
|
|
3235
|
+
#closed = false;
|
|
3236
|
+
#keys = /* @__PURE__ */ __name((data) => this.#key(String(data)), "#keys");
|
|
3237
|
+
constructor(options) {
|
|
3238
|
+
this.#options = options;
|
|
3239
|
+
this.#write = options.write ?? ((text) => process.stdout.write(text));
|
|
3240
|
+
}
|
|
3241
|
+
/** Runs all tests, then watches. */
|
|
3242
|
+
start() {
|
|
3243
|
+
this.#unsubscribe = this.#options.subscribe(
|
|
3244
|
+
(path) => this.#change(path)
|
|
3245
|
+
);
|
|
3246
|
+
const input = this.#options.input;
|
|
3247
|
+
if (input) {
|
|
3248
|
+
input.setRawMode?.(true);
|
|
3249
|
+
input.on("data", this.#keys);
|
|
3250
|
+
input.resume();
|
|
3251
|
+
}
|
|
3252
|
+
this.#queue("all");
|
|
3253
|
+
}
|
|
3254
|
+
/** Stops watching and the run in progress. */
|
|
3255
|
+
close() {
|
|
3256
|
+
this.#closed = true;
|
|
3257
|
+
clearTimeout(this.#timer);
|
|
3258
|
+
this.#unsubscribe?.();
|
|
3259
|
+
this.#current?.run.stop();
|
|
3260
|
+
const input = this.#options.input;
|
|
3261
|
+
if (input) {
|
|
3262
|
+
input.off("data", this.#keys);
|
|
3263
|
+
input.setRawMode?.(false);
|
|
3264
|
+
input.pause();
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
#key(key) {
|
|
3268
|
+
if (key === "q" || key === "" || key === "") this.close();
|
|
3269
|
+
else if (key === "a") this.#queue("all");
|
|
3270
|
+
else if (key === "\r" || key === "\n") this.#queue(this.#last);
|
|
3271
|
+
}
|
|
3272
|
+
#change(path) {
|
|
3273
|
+
if (this.#closed || this.#options.ignores?.(path)) return;
|
|
3274
|
+
this.#changed.add(path);
|
|
3275
|
+
clearTimeout(this.#timer);
|
|
3276
|
+
this.#timer = setTimeout(
|
|
3277
|
+
() => void this.#flush(),
|
|
3278
|
+
this.#options.delay ?? 100
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
async #flush() {
|
|
3282
|
+
const changed = this.#changed;
|
|
3283
|
+
this.#changed = /* @__PURE__ */ new Set();
|
|
3284
|
+
if (this.#options.mode === "all") return this.#queue("all");
|
|
3285
|
+
try {
|
|
3286
|
+
const files = await this.#options.related(changed);
|
|
3287
|
+
if (files.length > 0) this.#queue(files);
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
this.#write(
|
|
3290
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
3291
|
+
`
|
|
3292
|
+
);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
/** Runs a selection, after stopping the run in progress, whose files run again too. */
|
|
3296
|
+
#queue(selection) {
|
|
3297
|
+
if (this.#closed) return;
|
|
3298
|
+
this.#queued = _Watcher.merge(this.#queued, selection);
|
|
3299
|
+
if (this.#current) {
|
|
3300
|
+
this.#queued = _Watcher.merge(this.#queued, this.#current.selection);
|
|
3301
|
+
this.#current.run.stop();
|
|
3302
|
+
} else this.#next();
|
|
3303
|
+
}
|
|
3304
|
+
#next() {
|
|
3305
|
+
const selection = this.#queued;
|
|
3306
|
+
if (selection === void 0 || this.#closed) return;
|
|
3307
|
+
this.#queued = void 0;
|
|
3308
|
+
this.#last = selection;
|
|
3309
|
+
if (this.#options.clear) this.#write("\x1B[2J\x1B[3J\x1B[H");
|
|
3310
|
+
const run = this.#options.run(
|
|
3311
|
+
selection === "all" ? void 0 : selection
|
|
3312
|
+
);
|
|
3313
|
+
this.#current = { selection, run };
|
|
3314
|
+
void run.done.then(() => {
|
|
3315
|
+
this.#current = void 0;
|
|
3316
|
+
if (this.#queued !== void 0) this.#next();
|
|
3317
|
+
else if (!this.#closed) this.#usage();
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
#usage() {
|
|
3321
|
+
this.#write(
|
|
3322
|
+
this.#options.input ? [
|
|
3323
|
+
"",
|
|
3324
|
+
"Watch Usage",
|
|
3325
|
+
" \u203A Press a to run all tests.",
|
|
3326
|
+
" \u203A Press Enter to run the last tests again.",
|
|
3327
|
+
" \u203A Press q to quit watch mode.",
|
|
3328
|
+
""
|
|
3329
|
+
].join("\n") : "\nWatching for changes, Ctrl+C quits.\n"
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
/** Two selections in one, all when either is. */
|
|
3333
|
+
static merge(a, b) {
|
|
3334
|
+
if (a === void 0) return b;
|
|
3335
|
+
if (a === "all" || b === "all") return "all";
|
|
3336
|
+
return [.../* @__PURE__ */ new Set([...a, ...b])].sort();
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
Dependencies = class {
|
|
3340
|
+
static {
|
|
3341
|
+
__name(this, "Dependencies");
|
|
3342
|
+
}
|
|
3343
|
+
#list;
|
|
3344
|
+
#inputs = /* @__PURE__ */ new Map();
|
|
3345
|
+
/** `list` gives the files a test file is bundled from, all relative to the root. */
|
|
3346
|
+
constructor(list) {
|
|
3347
|
+
this.#list = list;
|
|
3348
|
+
}
|
|
3349
|
+
async related(tests, changed) {
|
|
3350
|
+
const related = [];
|
|
3351
|
+
for (const test of tests) {
|
|
3352
|
+
const known = this.#inputs.get(test);
|
|
3353
|
+
const touched = /* @__PURE__ */ __name((inputs2) => inputs2 !== void 0 && [...inputs2].some((input) => changed.has(input)), "touched");
|
|
3354
|
+
let inputs = known;
|
|
3355
|
+
if (!known || changed.has(test) || touched(known))
|
|
3356
|
+
try {
|
|
3357
|
+
inputs = new Set(await this.#list(test));
|
|
3358
|
+
this.#inputs.set(test, inputs);
|
|
3359
|
+
} catch {
|
|
3360
|
+
this.#inputs.delete(test);
|
|
3361
|
+
related.push(test);
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
if (changed.has(test) || touched(known) || touched(inputs))
|
|
3365
|
+
related.push(test);
|
|
3366
|
+
}
|
|
3367
|
+
return related;
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
});
|
|
3372
|
+
|
|
2457
3373
|
// packages/testero/src/index.ts
|
|
2458
3374
|
var index_exports = {};
|
|
3375
|
+
import { spawn } from "node:child_process";
|
|
3376
|
+
import { watch } from "node:fs";
|
|
3377
|
+
import { join as join5, relative as relative4, sep as sep3 } from "node:path";
|
|
2459
3378
|
var init_index = __esm({
|
|
2460
3379
|
async "packages/testero/src/index.ts"() {
|
|
2461
3380
|
"use strict";
|
|
@@ -2463,16 +3382,70 @@ var init_index = __esm({
|
|
|
2463
3382
|
init_arguments();
|
|
2464
3383
|
init_config2();
|
|
2465
3384
|
init_testero();
|
|
3385
|
+
init_transformer();
|
|
3386
|
+
init_watcher();
|
|
2466
3387
|
try {
|
|
2467
3388
|
if (Config.init("testero", config_default)) process.exit();
|
|
2468
3389
|
const args = new Arguments(process.argv.slice(2));
|
|
2469
3390
|
if (args.help) process.stdout.write(Arguments.usage);
|
|
2470
3391
|
else {
|
|
2471
3392
|
const config = await Config.load("testero", config_default);
|
|
2472
|
-
const
|
|
3393
|
+
const options = args.options(config.rules);
|
|
3394
|
+
const testero = new Testero(config, options);
|
|
2473
3395
|
if (args.listTests)
|
|
2474
3396
|
for (const file of testero.files()) console.log(file);
|
|
2475
|
-
else if (
|
|
3397
|
+
else if (args.watch) {
|
|
3398
|
+
const entry = process.argv[1];
|
|
3399
|
+
const exactly = /* @__PURE__ */ __name((file) => `^${file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "exactly");
|
|
3400
|
+
const transformer = new Transformer();
|
|
3401
|
+
const dependencies = new Dependencies(
|
|
3402
|
+
async (file) => (await transformer.inputs(join5(config.root, file))).map(
|
|
3403
|
+
(input) => relative4(config.root, input).split(sep3).join("/")
|
|
3404
|
+
)
|
|
3405
|
+
);
|
|
3406
|
+
const coverage = new Pattern(`/${options.coverageDirectory}`);
|
|
3407
|
+
const watcher = new Watcher({
|
|
3408
|
+
mode: args.watch,
|
|
3409
|
+
run: /* @__PURE__ */ __name((files) => {
|
|
3410
|
+
const child = spawn(
|
|
3411
|
+
process.execPath,
|
|
3412
|
+
[
|
|
3413
|
+
...process.execArgv,
|
|
3414
|
+
entry,
|
|
3415
|
+
...args.flags,
|
|
3416
|
+
...files?.map(exactly) ?? args.patterns
|
|
3417
|
+
],
|
|
3418
|
+
{ stdio: ["ignore", "inherit", "inherit"] }
|
|
3419
|
+
);
|
|
3420
|
+
return {
|
|
3421
|
+
done: new Promise(
|
|
3422
|
+
(resolve4) => child.on("close", resolve4).on("error", resolve4)
|
|
3423
|
+
),
|
|
3424
|
+
stop: /* @__PURE__ */ __name(() => child.kill(), "stop")
|
|
3425
|
+
};
|
|
3426
|
+
}, "run"),
|
|
3427
|
+
related: /* @__PURE__ */ __name((changed) => dependencies.related(testero.files(), changed), "related"),
|
|
3428
|
+
subscribe: /* @__PURE__ */ __name((listener) => {
|
|
3429
|
+
const watching = watch(
|
|
3430
|
+
config.root,
|
|
3431
|
+
{ recursive: true },
|
|
3432
|
+
(_, name) => {
|
|
3433
|
+
if (name) listener(name.split(sep3).join("/"));
|
|
3434
|
+
}
|
|
3435
|
+
);
|
|
3436
|
+
return () => watching.close();
|
|
3437
|
+
}, "subscribe"),
|
|
3438
|
+
// sources and JSON, like Jest's moduleFileExtensions, not
|
|
3439
|
+
// the coverage a run writes
|
|
3440
|
+
ignores: /* @__PURE__ */ __name((path) => config.ignore.matches(path) || coverage.matches(path) || Transformer.loader(path) === void 0 && !path.endsWith(".json"), "ignores"),
|
|
3441
|
+
input: process.stdin.isTTY ? process.stdin : void 0,
|
|
3442
|
+
clear: process.stdout.isTTY
|
|
3443
|
+
});
|
|
3444
|
+
process.on("exit", () => transformer.dispose());
|
|
3445
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
3446
|
+
process.once(signal, () => watcher.close());
|
|
3447
|
+
watcher.start();
|
|
3448
|
+
} else if (!await testero.run()) process.exitCode = 1;
|
|
2476
3449
|
}
|
|
2477
3450
|
} catch (error) {
|
|
2478
3451
|
console.error(error instanceof Error ? error.message : error);
|